僵尸进程是已经退出、但父进程尚未读取其退出状态的子进程。它几乎不占 CPU 和业务内存,但会保留 PID 和少量进程表信息。数量持续增长时,最终可能耗尽 PID 或暴露父进程的回收逻辑缺陷。
一、确认僵尸进程数量
ps -eo stat | awk '$1 ~ /^Z/ {count++} END {print count+0}'
ps -eo pid,ppid,stat,lstart,cmd | awk '$3 ~ /^Z/'
top -b -n 1 | head -n 5STAT 中 Z 或进程名后的 <defunct> 表示僵尸。记录 PID、PPID、出现时间和数量增长速度。
二、按父进程聚合
ps -eo ppid=,stat= | awk '$2 ~ /^Z/ {count[$1]++} END {for (p in count) print count[p],p}' | sort -nr对僵尸最多的父 PID 继续检查:
ps -fp <ppid>
cat /proc/<ppid>/status
cat /proc/<ppid>/limits
ls -l /proc/<ppid>/exe
systemctl status <service> --no-pager如果父进程属于容器,还要从宿主机和容器内部确认 PID namespace,不能只根据宿主机 PID 判断业务归属。
三、理解为什么 kill 僵尸无效
僵尸进程已经执行完毕,没有可运行代码,因此对它执行 kill -9 <zombie_pid> 通常无法清除。真正需要处理的是父进程没有调用 wait()、waitpid() 或等效机制读取子进程状态。
可以向父进程发送 SIGCHLD 作为低风险尝试,但只有父进程实现了正确处理逻辑才会生效:
kill -s SIGCHLD <ppid>发送前确认父进程和应用行为,不要对未知系统进程批量发送信号。
四、定位父进程为何不回收
检查父进程日志、线程状态和系统调用:
journalctl _PID=<ppid> --since '-30 min' --no-pager
ps -L -p <ppid> -o pid,tid,stat,wchan:32,comm
strace -f -p <ppid> -e trace=wait4,waitid,clone,fork,vfork -ttstrace 会影响进程并产生大量输出,只应短时间使用,关键业务进程要先评估风险。
常见根因包括:
父进程没有注册 SIGCHLD 处理或没有调用 wait 系列函数。
只回收了一个退出子进程,没有循环回收全部子进程。
父进程线程死锁、事件循环阻塞或异常退出路径未执行回收。
Shell 脚本后台启动大量任务但没有
wait。容器的 PID 1 不是合格的 init,不能正确回收孤儿进程。
五、应急处理步骤
先确认当前 PID 使用情况:
cat /proc/sys/kernel/pid_max
ps -e --no-headers | wc -l如果数量稳定且 PID 空间充足,优先保留现场并安排应用修复。
如果持续快速增长,暂停产生子进程的任务或从入口限流。
对无状态服务做滚动重启,让父进程退出后由 init 接管并回收僵尸。
单机服务重启前确认依赖、流量、未完成任务和回滚方案。
不要为了消除僵尸直接重启整台服务器。多数情况下,只需受控重启有问题的父进程或容器。
六、代码与容器层修复
C/C++ 正确处理 SIGCHLD,并循环调用
waitpid(-1, &status, WNOHANG)。Python 管理
subprocess.Popen后调用wait()、communicate()或使用上下文管理。Shell 后台任务保存 PID,并使用
wait获取退出状态。容器为多进程模型时使用合适的 init,例如启用运行时 init 选项。
为子进程数量、退出速率和回收失败建立指标。
七、长期治理
监控 Z 状态数量、PID 使用率和进程创建失败。
压测子进程高频退出、并发退出和异常退出场景。
应用日志记录子进程 PID、退出码和回收结果。
容器镜像明确 PID 1 的职责,不让普通业务进程承担不完整的 init 行为。
处理僵尸进程的关键不是“杀死它”,而是找到没有履行回收职责的父进程。