在Linux VPS上看到进程状态为Z或命令末尾出现<defunct>,表示子进程已经结束,但父进程还没有通过wait()或waitpid()读取退出状态。内核暂时保留PID、退出码和少量记账信息,等待父进程回收,这就是僵尸进程。
僵尸进程已经不再执行代码,所以对它反复发送SIGTERM或SIGKILL通常不能解决问题。真正需要检查的是父进程为什么没有回收子进程,以及Z进程数量是否持续增长。
先判断是单个短暂Z进程,还是持续泄漏
子进程退出到父进程完成回收之间可能有极短窗口,偶尔观察到一个Z状态不一定代表故障。风险主要来自数量持续增加、同一父进程不断产生僵尸,最终消耗进程表和PID资源。
先做只读盘点:
ps -eo pid,ppid,state,stat,etime,cmd | awk '$3 ~ /^Z/'
ps -eo ppid=,state= | awk '$2 ~ /^Z/ {count[$1]++} END {for (p in count) print p, count[p]}' | sort -k2,2nr
第一条列出僵尸PID、父PID、状态、存在时间和命令;第二条按父PID汇总数量。命令输出为空,表示采样时没有Z进程。若每隔一段时间采样,数量一直上升,就应尽快定位父进程。
Z进程占用CPU和内存吗
僵尸进程不再运行,也不保留正常进程的用户态内存、打开文件和执行线程,因此通常不是CPU飙高或大块内存占用的直接原因。它保留的是内核为父进程读取退出结果所需的最小记录。
但“大量且持续增长”仍然危险:每个僵尸占用一个进程表位置和PID,可能让系统或某个用户无法继续创建进程。此时应用会出现fork失败、任务无法启动或服务异常,不能因为单个Z进程资源很少就忽略趋势。
怎样准确找到父进程
拿到僵尸PID后,可读取父进程关系:
ps -o pid,ppid,state,etime,cmd -p ZOMBIE_PID
ps -o pid,ppid,stat,lstart,cmd -p PARENT_PID
cat /proc/ZOMBIE_PID/status
把示例中的PID替换为只读盘点得到的实际数字。/proc/PID/status中的State可确认Z状态,PPid显示父PID。也可使用pstree -p PARENT_PID观察父子关系,但不要只凭命令名称决定重启;同名进程可能属于不同服务、容器或用户。
随后确认父进程由谁管理:
- systemd服务:用
systemctl status SERVICE和服务日志检查; - 容器:确认容器ID、容器内PID与宿主机PID映射;
- 定时任务:检查Cron、systemd timer或任务队列的启动与超时逻辑;
- 手工脚本:确认启动用户、终端、Shell和后台运行方式;
- 进程管理器:检查Supervisor、PM2、Gunicorn、Celery等父进程策略。
为什么kill -9对僵尸进程没有用
信号需要由正在运行或可调度的进程处理。僵尸进程的执行已经结束,剩下的只是内核记录,没有代码可以再被“杀一次”。因此kill -9 ZOMBIE_PID即使返回操作结果,也不会让父进程完成回收。
可行方向只有两个:让父进程执行等待系统调用,或让有问题的父进程结束,使孤儿由合适的收养进程接管并回收。第二种方式可能中断业务,必须在确认服务、保存日志和准备恢复方案后进行。
从低风险到高风险处理
第一步:保存证据
记录僵尸PID、父PID、首次出现时间、增长速度、父进程命令、服务状态和相关日志。应用如果自动重启,最早的异常可能很快被后续重启信息覆盖。
第二步:检查父进程是否仍健康
父进程可能正在忙、死锁、等待IO,或代码逻辑遗漏子进程回收。检查它的CPU、内存、线程、文件描述符、日志和最近部署变化。若Z进程来自短命工作任务,还应核对任务超时、并发和异常分支。
第三步:使用服务管理器有序重启
如果父进程属于可安全重启的systemd服务,先确认是否有冗余、队列或活动请求,再执行有序重启并观察Z数量。不要直接按PID发送SIGKILL作为第一选择,因为这会绕过应用的清理流程,也可能触发重启风暴。
重启后必须检查服务是否稳定运行、端口是否恢复、任务是否继续处理,以及新僵尸是否再次出现。如果很快复发,重启只清除了症状,代码或运行方式仍需修复。
第四步:处理失控父进程
父进程完全无响应且正常停止失败时,才考虑更强制的终止方式。执行前应确认PID没有重用、对象确实是目标服务,并评估数据写入、数据库事务和请求中断。容器环境下优先通过编排或容器运行时管理,而不是在宿主机随意杀命名相似的进程。
代码层为什么必须调用wait或waitpid
创建子进程的程序负责读取其退出状态。同步场景可在合适位置调用wait()或waitpid();异步并发程序要设计SIGCHLD处理、事件循环或专门回收线程,并处理多个子进程同时退出的情况。
常见缺陷包括:
- 只在成功路径等待,错误或超时路径直接返回;
- 收到SIGCHLD后只回收一个子进程,遗漏同批其他退出项;
- 父进程长时间阻塞,未执行非阻塞回收循环;
- Shell脚本把后台任务不断放入后台,却从不等待;
- 容器主进程不是合格的init,不能正确转发信号和回收孤儿;
- 任务框架超时终止工作进程,但管理进程未读取退出状态。
修复后应增加异常退出、超时、并发退出和父进程终止测试,而不是只测试子进程正常完成。
容器里的Z进程为什么更常见
容器内PID 1承担特殊职责。若应用直接作为容器PID 1运行,却没有实现信号转发和子进程回收,短命任务可能积累为僵尸。可以让应用正确实现回收,也可在设计允许时使用轻量init机制。是否添加init应结合镜像、入口脚本和编排方式验证,不能用它掩盖应用自身的子进程管理缺陷。
排查时还要区分容器内PID和宿主机PID。容器显示的PPID为1,不一定代表宿主机系统的真实父子编号;应从对应命名空间和容器运行时视角核对。
怎样验证已经真正修好
- 处理前记录Z进程总数和主要父PID分布;
- 恢复服务后确认旧僵尸消失或被回收;
- 重复触发此前会产生子进程的正常、失败和超时任务;
- 连续观察一段覆盖业务周期的窗口,确认Z数量不再单调增长;
- 检查服务端口、队列、错误率和资源指标没有因重启恶化;
- 为僵尸数量、fork失败和父进程重启建立告警。
如果服务在修复过程中又进入频繁失败重启,可参考极跃圈的systemd start-limit-hit与重启风暴排查,先保存最早错误再恢复服务。
结论
VPS上的Z或defunct表示子进程已经退出但尚未被父进程回收。单个短暂僵尸通常影响有限,持续增长才是需要处理的泄漏信号。正确顺序是统计趋势、定位PPID、确认服务或容器归属、保存日志、让父进程有序恢复,并最终修复wait/waitpid或PID 1回收逻辑。反复kill僵尸PID既无效,也会耽误真正根因的定位。






