Linux 系统进程僵尸与孤儿处理

7次阅读

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

Linux 系统进程僵尸与孤儿处理

僵尸进程怎么查和清理

僵尸进程本身已死,只留内核里的一个 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 僵尸进程,它早死了
  • 极端情况可尝试向父进程发 SIGCHLDkill -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 直接跑命令,而非用 tinisupervisord),所有子进程退出后都会变成僵尸,且永远没人收养。

现象比宿主机更明显:ps 看到一堆 Zdocker 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 调用位置、时机、是否循环,全得抠到系统调用层面才敢说稳了。

text=ZqhQzanResources