僵尸进程需用ps aux | grep ' z '定位,其父进程若未调用wait()则需重启父进程或发sigchld信号;孤儿进程由systemd收养但不自动清理,容器中须用tini等init进程避免僵尸堆积。

僵尸进程怎么查和清理
僵尸进程本身已死,只留内核里的一个 task_struct 条目,不占 CPU、内存,但会卡住进程 ID 和少量内核资源。它无法被 kill 命令干掉,强行发信号没用。
常见错误现象:ps aux 里看到状态为 Z 的进程;top 右上角显示 Z 数量持续不降;父进程长期不调用 wait() 或 waitpid() 是根本原因。
- 先用
ps aux | grep ' Z '或ps aux --forest | grep 'Z'定位僵尸进程及其父进程 PID(看PPID列) - 检查父进程是否还在运行:如果父进程已退出,init(PID 1)或 systemd 通常会自动收养并清理——但某些容器环境或自定义 init 里可能不会
- 若父进程仍在且写法有缺陷(比如没处理子进程退出信号),只能重启父进程;别试
kill -9僵尸进程,它早死了 - 极端情况可尝试向父进程发
SIGCHLD:kill -s SIGCHLD <ppid></ppid>,部分父进程会因此触发wait()
孤儿进程谁来收养?systemd 下表现异常吗
孤儿进程指父进程先于子进程退出,按 POSIX 规定,这类进程会被 init 进程(传统是 PID 1 的 init,现代多数是 systemd)收养。收养后,它的 PPID 变成 1,继续运行不受影响。
关键点在于:收养 ≠ 清理。systemd 收养后不会主动等它退出,也不会自动回收其退出状态——所以如果该孤儿进程后来变成僵尸,而 systemd 没调用 wait(),它就真卡住了。
- 验证是否被收养:查
ps -o pid,ppid,comm -p <pid></pid>,PPID 是 1 就说明已被收养 - systemd 默认行为是“不干预”,除非该进程是它直接启动的服务(即通过
.service文件启动)。这种情况下,systemd 会监控生命周期并自动 wait - 在 shell 脚本中后台启动的子进程(如
sleep 100 &),若父 shell 退出,该sleep成为孤儿并被 systemd 收养,但没人等它——100 秒后它退出,就会变成僵尸,直到 systemd 下次做清理(实际依赖具体版本和配置) - 避免方式:脚本里用
wait显式等待,或用setsid让子进程彻底脱离会话(如setsid sleep 100 &)
fork 后不 wait 的典型代码坑
C/C++ 里 fork() 创建子进程后,若父进程不调用 wait() 或 waitpid() 获取子进程退出状态,子进程终止后必然变成僵尸。这不是 bug,是 Unix 设计使然——内核必须保留退出码等信息,等父进程来取。
容易被忽略的是:即使父进程忘了 wait,只要它后续正常退出,init/systemd 会收养并最终清理所有子进程(包括已僵尸的)。但若父进程长年运行(比如守护进程、服务程序),这些僵尸就一直挂着。
- 信号处理是常见疏漏点:注册
SIGCHLD处理函数时,必须在 handler 里循环调用waitpid(-1, &status, WNOHANG),否则一次只清理一个子进程,多个子退出时仍有漏网之鱼 -
wait()和waitpid()参数差异:前者阻塞等待任意子进程,后者可指定 PID 或用-1等价于wait();加WNOHANG标志才能非阻塞轮询 - 多线程程序中,只有创建子进程的那个线程能
wait它;其他线程调用会失败(errno = ECHILD) - Go/Python 等高级语言通常封装了这一层,但用
os.fork()或syscall.ForkExec()时,仍需手动处理,不能依赖 runtime 自动 wait
容器里僵尸进程为什么特别难搞
容器默认 PID namespace 隔离,init 进程(PID 1)不是宿主机的 systemd,而是你镜像里启动的第一个进程。如果它不处理子进程退出(比如用 /bin/sh 直接跑命令,而非用 tini 或 supervisord),所有子进程退出后都会变成僵尸,且永远没人收养。
现象比宿主机更明显:ps 看到一堆 Z,docker stats 显示进程数持续上涨,但 CPU/内存无变化;kill -9 宿主机上对应容器的 PID 没用,因为那只是宿主机视角的 pause 进程。
- 根本解法:容器入口进程必须是真正的 init,推荐用
tini(Docker 官方支持)或docker run --init启动 - 自己写启动脚本时,避免直接
exec $@;应先启动tini,再让它执行主程序:exec tini -- $@ - Alpine 镜像里默认没有
tini,要手动apk add --no-cache tini并设为 ENTRYPOINT - Kubernetes 中可通过
securityContext.procMount: "default"和启用initContainers辅助清理,但不如从镜像源头解决干净
真正麻烦的从来不是单个僵尸,而是父进程逻辑里混着信号处理、多线程、容器化部署这三者的组合——这时候 wait 调用位置、时机、是否循环,全得抠到系统调用层面才敢说稳了。










