您的位置:首页 > 运维架构 > Linux

Linux下如何防止僵尸进程的出现

2016-04-29 18:09 537 查看
今天碰到一个老问题,就是防止僵尸进程的出现,于是又在网上学习了一遍,写到这里,防止以后不再有一个地方重新学习,:)

一、什么是僵尸进程

这个首先要了解一下linux下的进程在结束的时候会做什么事情。linux下的进程在结束后,会向它的父进程发送一个SIGCHLD的信号,但是它仍然会在系统内存中保留一些数据,比如它占用的PID,以及它自己的结束状态(我猜测留下来的就是task_struct结构体,不过没有进行代码验证);保留这些信息的作用就是期待它的父进程调用wait()系统调用时,可以返回自己的结束状态;在父进程调用完wait()之后,该进程就真正地从内存中消失的无影无踪。

那么,僵尸进程就是指,已经结束,但是还没有被它的父进程调用wait/waitpid的进程。

二、产生僵尸进程的原因

如果父进程出于一些原因没有调用wait(),或者是因为程序bug,或者是父进程真的就不想长时间的等待子进程结束(一些进程池的实现,而且wait是blocking的调用),那么僵尸进程就会出现。

三、僵尸进程的影响

因为僵尸进程在内存中所保存的状态很少,所以如果不是大量的僵尸进程存在是不会影响系统性能的。不过,如果你的系统中僵尸进程的数目持续增长的话,那么的确不是什么好兆头。因为僵尸进程会保留PID,而PID这个资源对于linux来说又是非常宝贵的,如果僵尸进程的数目快速增长的话,它会快速消耗掉系统内可用的PID,那么你的系统就有可能不能再启动新的进程了。这意味着什么?那就是有可能你连
ls
命令都不能执行,因为
ls
也是个进程呀!

四、如何消灭掉系统内的僵尸进程

由于这些进程已经是僵尸进程了,是DEAD的了,你是不能通过
kill
命令清除掉这些僵尸进程的。那么唯一可以利用的就是系统的
init
进程,没错,就是那个PID为1的进程。

一般可以认为
init
进程没有什么bug,针对托管给它的僵尸进程,它可以正确调用wait()、waitpid()来终止掉这些进程。那么如何才能将这些僵尸进程托管给
init
进程呢?我想到的办法就是
kill
掉它们的父进程。如果有其他方法可以告诉我哈!

在系统中,你可以看到大量的僵尸进程,就是因为父进程没有wait它们,而且父进程现在还是alive的状态;如果连父进程也退出了,这些进程会直接托管给
init
进程,你是不会看到这么大量的僵尸进程的。所以这个时候父进程肯定存在,结束掉它就好了。

当然,光结束掉父进程就可以了吗?如果是程序中的bug呢?在写程序中要怎么才能防止僵尸进程的出现了。

五、如何避免僵尸进程的出现

两种解决僵尸进程的方式

5.1 double fork

void func()
{
pit_t pid1;
pit_t pid2;
int status;

if (pid1 = fork()) {
/* parent process A */
waitpid(pid1, &status, NULL);
} else if (!pit1) {
/* child process B */
if (pid2 = fork()) {
exit(0);
} else if (!pid2) {
/* child process C */
execvp("something");
} else {
/* error */
}
} else {
/* error */
}
}


在进程A中,使用waitpid来等待子进程结束。关键点在进程B中,进程B执行fork之后就直接退出了,进程B退出,连带的就是进程A的waitpid结束,进程A就可以做其他的事情了。对于进程B的子进程C,它是要做实事的。它的存在时间肯定要长于进程B,但是进程B已经退出了,进程C这时就成为了僵尸进程,它会托管给系统
init
进程。有
init
对它进程wait操作,避免了long-live zombie process的产生。

这种方法可以避免僵尸的产生,但是最开始的进程A是得不到进程C的状态的,也就是说如果这是一个进程池的实现的话,父进程A是不能管理它的子进程的。比如,如果由于子进程意外退出而导致进程数目不能满足进程池的配置要求,这个时候父进程要有能力再fork出新进程来满足进程池进程数目的要求。

那么我们来看第二种方法,

5.2 while True: waitpid

这种方法是在进程池中所使用了,比较有利的证明就是python中Gunicorn。

具体的详细代码可以在Gunicorn代码看到。我这里只列出它的一个函数。

def reap_workers(self):
"""\
Reap workers to avoid zombie processes
"""
try:
while True:
wpid, status = os.waitpid(-1, os.WNOHANG)
if not wpid:
break
if self.reexec_pid == wpid:
self.reexec_pid = 0
else:
# A worker said it cannot boot. We'll shutdown
# to avoid infinite start/stop cycles.
exitcode = status >> 8
if exitcode == self.WORKER_BOOT_ERROR:
reason = "Worker failed to boot."
raise HaltServer(reason, self.WORKER_BOOT_ERROR)
if exitcode == self.APP_LOAD_ERROR:
reason = "App failed to load."
raise HaltServer(reason, self.APP_LOAD_ERROR)
worker = self.WORKERS.pop(wpid, None)
if not worker:
continue
worker.tmp.close()
except OSError as e:
if e.errno != errno.ECHILD:
raise


最重要的结构就是无限循环里面的waitpid。

为什么使用while True

这里跟linux处理信号有关的,如果当前线程正在信号处理函数里面处理,而又有相同的信号到来时,linux并不保证所有的信号都被处理,增加了这个无线循环就可以保证处理所有发过来的信号。

为什么使用WNOHANG

根据linux.die.net,

WNOHANG

return immediately if no child has exited.

我们不希望信号处理函数被阻塞住,因为这个时候有可能有多个信号同时到达。

参考

http://thinkiii.blogspot.com/2009/12/double-fork-to-avoid-zombie-process.html

http://stackoverflow.com/questions/11322488/how-to-make-sure-that-waitpid-1-stat-wnohang-collect-all-children-process

ab1e
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  linux zombie