先记住这个答案
僵尸进程是子进程已终止、父进程未调用 wait/waitpid 获取其退出状态,使内核无法释放进程表项。定位时用 ps -eo pid,ppid,stat,comm,STAT 为 Z 的行就是僵尸,PPID 即父进程。处理只能让父进程调用 wait 回收,或让父进程退出由 PID 1 接管清理;kill -9 对僵尸本身无效,因为它无调度上下文。
- 僵尸由父进程 wait 或退出后 init 回收
- ps 查 Z 状态和 PPID 定位父进程
- kill 僵尸本身无效,需处理父进程
僵尸状态的本质与 wait 的作用
子进程调用 exit 后,内核不会立刻释放其进程表项,而是保留 PID、退出状态与资源计数等记录,直到父进程调用 wait 获取退出状态。此时进程处于僵尸状态,ps 的 STAT 列为 Z。僵尸已无内存映像和可执行代码,几乎不占资源,且无法被信号杀掉,因为信号只能作用于可调度的进程。
定位方法为执行 ps -eo pid,ppid,stat,comm,找到 STAT 为 Z 的行,其 PPID 即为父进程。若 PPID 为 1,说明 init/systemd 已接管,通常很快清理;否则必须联系父进程处理。ps aux 中僵尸行末会带 <defunct>,但需用 -o 明确输出 PPID 字段才能准确获取。
批量僵尸的定位与应急处理
假设某后端服务父进程 PID 2048 fork 出的 worker 未回收,ps -eo pid,ppid,stat,comm 显示大量 Z 且 PPID 均为 2048,系统 PID 接近上限。先确认服务无新请求后,向父进程发送 SIGTERM 触发优雅退出。父进程退出后,这些僵尸被 PID 1 继承并随后清理,服务通过重启脚本恢复。
若父进程有持久状态不能直接终止,则需修改父进程代码,在主循环中调用 waitpid(-1, &status, WNOHANG) 回收已退出 worker,或设置 SIGCHLD handler。随后滚动重启父进程,观察僵尸是否消失。此做法不中断正在处理的请求,代价是代码变更和一次滚动发布。
容易失效的条件与正确处理边界
不要对僵尸本身执行 kill -9,因为它已无调度上下文,信号无法传递,命令成功但进程不消失。也不应无脑强杀父进程,若父进程还有运行中的子进程,强杀会使其变孤儿,由 init 接管;若父进程持锁或共享内存,可能留下脏数据。必须先确认父进程无状态且可重建。
另一种误判是父进程已通过 signal(SIGCHLD, SIG_IGN) 显式忽略子进程退出,此时内核自动回收子进程,不会留下 Z。若在容器中,1 号进程未实现 wait 回收,僵尸会持续堆积,此时应更换为 tini 等专用 init,而不是直接 kill 父进程,否则容器可能整体退出。
容易答错的地方
- kill -9 能杀死僵尸进程
- 僵尸进程已经没有可调度的上下文,kill -9 发送的信号不会产生任何效果,因为信号只能由运行中的进程处理。必须由父进程调用 wait 或父进程退出后 init 接管清理。
- 僵尸进程消耗大量 CPU 和内存
- 僵尸进程只保留内核进程表项,内存占用极小,也不占 CPU 时间。真正风险是大量僵尸耗尽 PID 数量和进程表上限,导致无法 fork 新进程。应监控僵尸数量而非资源占用。
面试官还会怎么问?
父进程设置 SIGCHLD 为 SIG_IGN,子进程还会变僵尸吗?
不会。父进程调用 signal(SIGCHLD, SIG_IGN) 后,内核对子进程退出直接回收,不保留进程表项。但这会让父进程丢失获取退出状态的能力,适合不关心子进程结果的长服务。
如果僵尸进程的父进程是 PID 1(systemd),该怎么处理?
systemd 作为 init 会在子进程退出后自动调用 waitpid 清理,但若其逻辑异常或进程表损坏,僵尸可能滞留。此时检查 systemd 版本和配置,必要时重启 systemd 或整机,极少出现。
如何防止脚本 fork 的子进程变成僵尸?
脚本中 fork 子进程后必须 wait,或设置 SIGCHLD 忽略。对 shell 脚本,可在脚本末尾用 wait 命令等待所有后台子进程;服务程序用 waitpid 循环回收。也可使用 supervisor 等进程管理器统一回收。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。