PID 1 进程必须处理 SIGCHLD 回收子进程,否则会导致僵尸进程;推荐使用 tini(最轻量)、s6-overlay(复杂编排)或自研 init 方案解决。

在 Docker 容器中,PID 1 进程承担着特殊职责:它必须能正确回收子进程(即处理 SIGCHLD),否则子进程退出后会变成僵尸进程(zombie),长期积累可能耗尽 PID 表。而默认的 shell(如 /bin/sh 或 /bin/bash)**不处理 SIGCHLD**,导致其启动的子进程一旦退出就滞留为僵尸——这是容器中僵尸进程的根本原因。
为什么 PID 1 在容器里特别关键?
Docker 容器的 init 进程(PID 1)是所有其他进程的父进程。Linux 内核要求 PID 1 必须主动调用 waitpid(-1, ...) 或类似机制来“收割”已终止的子进程。普通进程退出时,若父进程未 wait,该子进程就变成僵尸;而 PID 1 若不处理,僵尸将永远无法被清理。
常见误区:以为只要主进程没挂,僵尸就“无害”。实际上,僵尸仍占用内核中的进程表项,在长时间运行或频繁启停子进程的容器(如日志采集、定时任务、多线程服务)中,可能触发 fork: Cannot allocate memory 错误。
三种主流初始化方案对比与实操
解决思路统一:让 PID 1 具备 init 功能(收尸 + 信号转发)。推荐以下三种成熟方案,按轻量到完备排序:
-
tini(最轻量推荐):
官方镜像(如node:18、python:3.11)已内置 tini。启用只需在Dockerfile中加一行:
ENTRYPOINT ["tini", "--"]
或运行时加参数:
docker run --init ...(Docker 1.13+ 原生支持,底层即调用 tini) -
s6-overlay(适合复杂服务编排):
提供进程监控、日志接管、依赖管理。适用于需多服务协同(如 Nginx + PHP-FPM + Cron)的容器。
使用方式:在Dockerfile中安装 s6-overlay,重写ENTRYPOINT为/init,并将应用脚本放入/etc/services.d/myapp/run。 -
自研简单 init(仅限极简场景):
若仅需基础收尸 + 透传信号,可用几行 C 或 Bash 实现。例如 Bash 版(需确保wait -n可用):
#!/bin/sh
exec /path/to/your/app "$@" &
while wait -n; do :; done
⚠️ 注意:Bash 的wait -n在 Alpine 的 busybox ash 中不可用,慎用于最小化镜像。
验证僵尸是否真正消失
进入运行中的容器检查:
ps aux | grep 'Z '(注意末尾空格,过滤状态为 Z 的进程)
或统计僵尸数量:
ps axo stat | grep -c '^Z'
更彻底的方法:在容器内启动一个快速退出的子进程(如 sh -c 'sleep 0.1' &),反复执行多次,再检查僵尸数是否始终为 0。若使用 --init 或 tini,该值应稳定为 0。
额外注意事项
- Docker 的
--init参数等价于用 tini 封装,无需额外安装,开发和 CI 环境首选; - Alpine Linux 用户避免直接用
apk add tini后硬编码路径,建议用--init或升级到较新 base 镜像(如alpine:3.19+已预装); - 若应用本身是多进程且自带子进程管理(如 Supervisor、Runit),需确认其是否以 PID 1 运行并正确处理 SIGCHLD——否则仍需外层 init;
- Go 应用若用
cmd.Process.Wait()或exec.Command().Run()启动子进程,自身可做收尸,此时不一定需要外部 init,但需严格测试覆盖所有退出路径。

















