子进程的结束和父进程的运行是异步的——父进程永远无法预测子进程什么时候结束。那么,会不会因为父进程太忙来不及 wait 子进程,或者不知道子进程何时结束,而丢失子进程结束时的状态信息呢?答案是:不会。
UNIX 提供了一种机制来保证:只要父进程想获取子进程结束时的状态信息,就可以得到。这个机制是——每个进程退出时,内核释放该进程所有的资源(打开的文件、占用的内存等),但仍保留一定的信息(包括进程号、退出状态、运行时间等),直到父进程通过 wait/waitpid 来取时才释放。
但这带来了一个问题:如果进程不调用 wait/waitpid,那么保留的信息就不会释放,其进程号会一直被占用。而系统可用的进程号是有限的,如果大量产生僵死进程,就会因为进程号耗尽而导致系统无法产生新进程——这正是僵尸进程的危害,应当避免。
僵尸进程的处理
子进程结束后为什么要进入僵尸状态?
因为父进程可能要取得子进程的退出状态等信息。
僵尸状态是每个子进程必经的状态吗?
是的。任何一个子进程(init 除外)在 exit() 之后,并非马上就消失,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理——这是每个子进程在结束时都要经过的阶段。
如果子进程在 exit() 之后父进程没有来得及处理,这时用 ps 命令就能看到子进程的状态是 “Z”;如果父进程能及时处理,可能就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。
如果父进程在子进程结束之前退出,则子进程由 init 接管,init 会以父进程的身份处理僵尸状态的子进程。
如何查看僵尸进程?
$ ps -el
其中,标记为 Z 的进程就是僵尸进程:
S:休眠状态D:不可中断的休眠状态R:运行状态Z:僵死状态T:停止或跟踪状态
僵尸进程变为孤儿进程
父进程死后,僵尸进程成为孤儿进程,过继给 1 号进程 init。init 始终会负责清理僵尸进程,它产生的所有僵尸进程也跟着消失。
基本概念
在 Unix/Linux 中,正常情况下子进程由父进程创建,子进程再创建新的进程。当一个进程完成工作终止之后,它的父进程需要调用 wait() 或 waitpid() 系统调用取得子进程的终止状态。
| 概念 | 定义 |
|---|---|
| 孤儿进程 | 一个父进程退出,而它的一个或多个子进程还在运行,那些子进程就成为孤儿进程。孤儿进程会被 init 进程(进程号 1)收养,并由 init 完成状态收集 |
| 僵尸进程 | 一个进程用 fork 创建子进程后,如果子进程退出而父进程没有调用 wait/waitpid 获取其状态,那么子进程的进程描述符仍保留在系统中,这种进程称为僵死进程 |
问题及危害
孤儿进程:孤儿进程是没有父进程的进程,这个责任落到 init 进程身上——每当出现一个孤儿进程,内核就把它的父进程设置为 init,而 init 会循环地 wait() 它已经退出的子进程。因此孤儿进程并不会有什么危害。
僵尸进程的危害场景:例如某个进程定期地产生一个子进程,这个子进程需要做的事情很少,做完就退出,生命周期很短;但父进程只管生成新子进程,对子进程退出之后的事情一概不闻不问。这样运行一段时间后,系统中就会积累大量僵死进程,用 ps 命令能看到很多状态为 Z 的进程。
严格来说,僵死进程并不是问题的根源,罪魁祸首是产生大量僵死进程的那个父进程。因此,要消灭系统中大量的僵死进程,办法就是终止那个”元凶”父进程(通过 kill 发送 SIGTERM 或 SIGKILL 信号)。父进程终止后,它产生的僵死进程就变成孤儿进程,被 init 接管;init 会 wait() 这些孤儿进程,释放它们占用的系统进程表资源,这些僵死进程就随之清理了。
僵尸进程的解决办法
通过信号机制
子进程退出时向父进程发送 SIGCHLD 信号,父进程处理 SIGCHLD 信号,在信号处理函数中调用 wait 处理僵尸进程。
fork 两次
《Unix 环境高级编程》8.6 节有非常详细的说明。原理是将子进程变为孤儿进程,使其父进程变为 init 进程,从而由 init 进程处理僵尸进程。