一.僵尸进程的产生
一个进程终止的方法很多,进程终止后有些信息对于父进程和内核还是很有用的,例如进程的ID号、进程的退出状态、进程运行的CPU时间等。因此进程在终止时,回收所有内核分配给它的内存、关闭它打开的所有文件等等,但是还会保留以上极少的信息,以供父进程使用。父进程可以使用 wait/waitpid 等系统调用来为子进程收拾,做一些收尾工作。
因此,一个僵尸进程产生的过程是:父进程调用fork创建子进程后,子进程运行直至其终止,它立即从内存中移除,但进程描述符仍然保留在内存中(进程描述符占有极少的内存空间)。子进程的状态变成EXIT_ZOMBIE,并且向父进程发送SIGCHLD 信号,父进程此时应该调用 wait() 系统调用来获取子进程的退出状态以及其它的信息。在 wait 调用之后,僵尸进程就完全从内存中移除。因此一个僵尸存在于其终止到父进程调用 wait 等函数这个时间的间隙,一般很快就消失,但如果编程不合理,父进程从不调用 wait 等系统调用来收集僵尸进程,那么这些进程会一直存在内存中。
二.检测系统中的僵尸进程
1.我们可以通过命令top来初步查看系统中僵尸进程的数目:
$ top
top - 09:58:31 up 3 min, 2 users, load average: 0.76, 0.45, 0.19
Tasks: 212 total, 1 running, 210 sleeping, 0 stopped, 1 zombie
%Cpu(s): 6.4 us, 3.1 sy, 0.6 ni, 78.6 id, 11.2 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem: 4037576 total, 1664952 used, 2372624 free, 82416 buffers
KiB Swap: 1998844 total, 0 used, 1998844 free. 916128 cached Mem
可以看到,我们系统中有一个僵尸进程(1 zombie)。
查看具体信息可以用ps(僵尸进程的stat为Z):
$ ps aux | grep -w 'Z'
#或者只查看特定的栏目:
$ ps -A -o stat,ppid,pid,cmd | grep -e '^[Zz]'
一般僵尸进程很难直接用kill杀死,因为僵尸进程是已经死掉的进程,它不能再接收任何信号。
一个可选的解决方法是,杀死父进程(需要谨慎……),于是僵尸进程成为”孤儿进程”,它由给1号进程init收养,init 进程会周期性地去调用 wait 系统调用来清除它的僵尸孩子。因此,你会发现父进程死掉之后,僵尸进程也跟着消失,其实是 init 进程在为其收尸!
三.避免产生僵尸进程
1.一般,为了防止产生僵尸进程,在fork子进程之后我们都要wait它们;同时,当子进程退出的时候,内核都会给父进程一个SIGCHLD信号,所以我们可以建立一个捕获SIGCHLD信号的信号处理函数,在函数体中调用wait(或waitpid),就可以清理退出的子进程以达到防止僵尸进程的目的。
(注意,建立信号处理函数并在其中调用wait并不足以防止出现僵尸进程,其原因在于:可能有多个信号在信号处理函数执行之前产生,而信号处理函数只执行一次,因为Unix信号一般是不排队的!正确的解决办法是调用waitpid而不是wait,这个办法为:信号处理函数中,在一个循环内调用waitpid,以获取所有已终止子进程的状态。我们必须指定WNOHANG选项,他告知waitpid在有尚未终止的子进程在运行时不要阻塞。)
2.在产生子进程的时候使用两次fork(),而且紧跟着使子进程直接退出,于是孙子进程成为孤儿进程,从而init进程将负责清除这个孤儿进程。(当然,这里需要注意,父进程仍然需要循环调用waitpid来等待子进程的结束,我们调用2次fork的好处是,孙子进程与父进程脱离了关系,子进程fork之后立即返回了,所有任务都交给了孙子进程去完成,这样一来,父进程就基本不用花时间在等待子进程上了)
例子:
/* create a new process and return if not the child */
if ( fork() != 0 )
{
while (waitpid(-1, NULL, 0) > 0); /*wait for all children*/
return;
}
/*avoid zombie*/
if ( fork() != 0)
exit(0);
/*
**do something
*/