在Linux中,进程退出后,分配的绝大部分资源将被回收,除了task_struct
结构及少数资源外。此时的进程已经“死亡”
,但task_struct
结构还保存在进程列表中,半死不活
,故称为“僵尸进程”
。
在回收僵尸进程之前,如果父进程退出了,则僵尸进程变为“孤儿进程”
,进而被init进程接管、回收。
僵尸进程的状态为
EXIT_ZOMBIE
,缩写Z
,ps命令也会打印僵尸进程,但无法使用kill杀死。
为什么需要僵尸进程(保留task_struct
)?
之所以保留task_struct
,是因为task_struct
里面保存了进程的pid、退出码、以及一些统计信息,父进程很可能会关心这些信息。比如$?
变量就保存了最近一个退出的前台进程的退出码,这个退出码就来自于僵尸进程的task_struct
结构。
为什么要处理僵尸进程
僵尸进程的task_struct
中保存了进程的pid、退出码等。尤其是pid,如果僵尸进程过多,最终耗尽了pid,那么将无法创建新的进程。
如何处理僵尸进程?
父进程可以通过wait系列的系统调用(如wait4、waitpid等,以下用wait指代)来等待某个或某些子进程的退出,并获取它的退出信息,然后顺便回收子进程的“尸体”(如task_struct
),然后子进程转入EXIT_DEAD
状态(X
),等待被操作系统彻底回收。
子进程退出后,父进程未调用wait回收尸体前,子进程将保持僵尸状态。
方案1:父进程调用wait
很自然的,如果父进程主动调用wait,也就消灭了僵尸进程。
但wait调用是阻塞的,如果调用wait时子进程还没有退出,将阻塞住父进程,影响性能。
方案2:kill父进程(产生“孤儿进程”)
如果父进程回收僵尸进程前就退出了,则僵尸进程变为“孤儿进程”。通常会将“孤儿进程”委托给init进程(pid等于1),init进程将在一个死循环中等待其子进程(包括这些僵尸进程)的退出事件,并调用wait回收子进程的尸体。
因此,找到僵尸进程的父进程,kill掉,也是一个没有办法时的办法。
方案3:通过信号机制异步回收
编写程序时,子进程退出前向父进程发送SIGCHLD
信号,父进程收到SIGCHLD
信号后(通过signal(SIGCHLD, sig_child)
绑定信号处理器),调用wait回收子进程的尸体。
与方案1相比,方案3不需要阻塞父进程,是最理想的方式。
为什么会出现少数僵尸进程一直不被回收
在实际工作中,总会碰到少数僵尸进程一直不被回收。
显然,如果父进程没有绑定SIGCHLD
信号处理函数调用wait或waitpid等待子进程结束,那么僵尸进程就会一直存在。如果这时候父进程结束了,那么init进程会自动接手这个子进程,还是能被清除掉的。但是,如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是系统中为什么有时候会有很多的僵尸进程。
参考: