在 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 进程会自动接手这个子进程, 还是能被清除掉的. 但是, 如果父进程是一个循环, 不会结束, 那么子进程就会一直保持僵尸状态, 这就是系统中为什么有时候会有很多的僵尸进程.
参考:
研究经典面试题: 孤儿进程和僵尸进程 http://www.sohu.com/a/120145657_220533
来源: http://www.tuicool.com/articles/6BbuEzJ