默认容器PID 1(如bash)无法处理孤儿进程,因其不监听SIGCHLD、不自动wait()、不收养孤儿进程,导致子进程退出后变成僵尸;需用dumb-init或tini等init进程接管PID 1,实现收养、信号转发与自动回收。

在 Docker 容器中,自动化脚本(如启动多个后台子进程的 shell 脚本)常因缺乏 proper init 机制,导致子进程在父脚本退出后变成孤儿进程——而容器 PID 1 进程若不具备收养能力,这些孤儿进程就无法被 wait() 回收,退出后会滞留为僵尸进程,长期积累可能耗尽进程表项或干扰监控。
为什么默认容器 PID 1 无法处理孤儿进程
Docker 默认将用户命令直接作为 PID 1 启动(例如 docker run ubuntu /bin/bash -c "sleep 10 &"),此时 bash 是 PID 1。但 bash 并非真正的 init:它不监听 SIGCHLD、不自动调用 wait()、也不收养孤儿进程。一旦其子进程先于它退出,又没被显式 wait,就会变成僵尸;若 bash 自己先退出,遗留的子进程虽被内核转交给宿主机 init(PID 1),但在容器命名空间内它们已“失管”,无法被容器内工具观测或统一清理。
使用 docker-init(如 dumb-init 或 tini)接管 PID 1
核心思路是让轻量级 init 进程成为容器真正的 PID 1,由它完成三件事:收养所有子进程、转发信号、自动回收退出的子进程(包括孤儿进程的终态)。
- 推荐使用官方支持的
--init标志(Docker 1.13+):docker run --init ubuntu /bin/sh -c "sleep 20 & echo 'started'; wait" - 等价的手动方式(兼容旧版):
docker run -it --rm --entrypoint /sbin/tini ubuntu:22.04 /bin/sh -c "sleep 15 & tail -f /dev/null" - 验证是否生效:进入容器执行
ps -o pid,ppid,comm,确认 PID 1 是tini或dumb-init,且你启动的子进程 PPID 均指向它
编写脚本时配合 init 的关键习惯
即使有了 init,脚本本身也需避免制造“不可回收”场景:
- 避免在后台运行无守护逻辑的长期子进程(如
python server.py &),应确保主进程能感知其生命周期;更稳妥的做法是用 exec 替换自身,或让 init 成为唯一父进程 - 若需多进程协作,建议用
exec启动主服务(如exec nginx -g "daemon off;"),使 init 直接管理该进程,而非 fork 出中间 shell - Shell 脚本中慎用
wait不带参数——它只等待当前 shell 的直系子进程;而 init 会 wait 所有被收养进程,因此可放心交由它兜底
验证孤儿进程是否被正确回收
运行一个典型测试容器:
docker run --init -it --rm alpine sh -c 'echo $$; sleep 2 & echo "child started with PID $!"; sleep 1; exit'几秒后容器退出,立即检查宿主机:docker ps -a | grep exited —— 应无残留;
再进容器日志或用 docker inspect 查看 ExitCode,确认为 0 且无僵尸迹象。若用 strace -f 跟踪 init 进程,还能看到它对子进程调用 wait4() 的系统调用记录。











