08入门:容器里的进程

第 8 关 容器里的进程
本关的产出:看清容器里那个「1 号进程」是谁、由什么决定,以及它为什么会决定容器能不能优雅地停下来。
8.1 停一个容器,走的是什么流程
docker stop <容器> 不是直接把容器掐掉,它先发一个信号:
- 给容器里的 1 号进程发
SIGTERM——「请你退下」。 - 等它自己退,默认最多等 10 秒。
- 还不退,再发
SIGKILL——直接打死,不给商量的余地。
这解释了那个常见现象:同一个命令 docker stop,有的容器秒停,有的一卡就十秒。差别就在 1 号进程收到 SIGTERM 之后做了什么。
关键在「1 号进程」四个字。 信号只发给它一个,不会挨个通知容器里的所有进程。谁坐在这把椅子上,谁才有机会优雅退出。
8.2 谁坐在 1 号那把椅子上
第 3 关摆过那两行启动配置——ENTRYPOINT 和 CMD。容器起来时,Docker 把它们拼起来跑,跑起来的第一个进程就是容器里的 1 号。
问题来了。第 3 关说过 nginx 镜像的 ENTRYPOINT 是一段准备脚本(第 2 关留的那个口子)。那么脚本跑起来,1 号就是它——nginx 跑在哪?
看脚本的末尾:
docker run --rm --entrypoint tail nginx:latest -3 /docker-entrypoint.sh要观察:最后一行是 exec "$@"。那串 "$@" 就是上面那条 CMD。所以这一行干的事是——把脚本自己换成 nginx,接着跑。
exec 是 shell 的一个动作:用新程序替换掉自己。进程号不变,跑的东西换了。于是 nginx 接管了那个进程号,成了 1 号;发信号就直接打在它身上。
起一个真 nginx 容器,docker top ngx 一看便知:1 号是 nginx: master process,底下那一排 worker 都是它 fork 出来的子进程。
8.3 一个字的差别:exec
假如那一行写的不是 exec "$@",而是普通的 "$@",会怎样?表面上看不出任何区别——nginx 照样起来、照样服务,你盯着终端、浏览器都察觉不到。差别藏在进程结构里。
做个对照,拿 sleep 当个占位的长跑程序(换成谁都一样):
docker run -d --name with-exec --entrypoint sh nginx:latest -c 'exec sleep 1000'docker run -d --name no-exec --entrypoint sh nginx:latest -c 'sleep 1000'两条只差一个 exec。看各自的进程:
docker top with-exec # 一行:sleep 1000docker top no-exec # 两行:sh -c ...,以及它的子进程 sleep 1000- 有
exec:sleep直接顶上 1 号,容器里就它一个进程。 - 没有
exec:sh占着 1 号,sleep是它的子进程。
这直接决定了停止时的待遇——因为 SIGTERM 只发给 1 号:
- 有
exec:信号打在sleep上,它能收尾(把手上正在处理的请求做完、关掉连接)再退。 - 没有
exec:信号打在sh上,永远到不了sleep。sleep没有半点收尾机会,sh一退,它就跟着被内核带走(收到的是SIGKILL,不是SIGTERM)。
易踩的坑:别拿 docker stop 快不快当判据。 上面这两种写法,容器都是一秒就停——壳自己被打发走了,容器照样秒停,但里面那个程序是被硬拽走的、没做任何收尾。信号到底有没有传到真程序,看进程结构(docker top),不是看停得快慢。
这就是「表面对、代价在别处」:差一个 exec,肉眼看不出来,却决定了容器是优雅退出还是被硬拽走。
8.4 CMD 的两种写法,差的就是这个
第 3 关把 CMD 的两种写法摆出来时说过,差别归本关讲:
CMD ["nginx", "-g", "daemon off;"] # 逐项写开CMD nginx -g "daemon off;" # 整句话交给 shell第二种写法,Docker 实际跑的是 /bin/sh -c "nginx -g \"daemon off;\""——它替你套了一层 shell。那层 sh 就是 1 号,nginx 又成了子进程,和 8.3 里「没写 exec」是同一回事。
所以两种写法的真正差别不在好不好看,在于:第二种多套了一层壳,信号到不了真正的程序。
易踩的坑:规矩倒过来也成立——想让哪个程序优雅退出,就得让它当 1 号。要么用 exec 形式(CMD 数组写法、或 ENTRYPOINT 脚本结尾 exec),要么它自己就是 1 号。中间夹一层壳,信号就断了。
8.5 1 号进程还多一份差事
1 号进程在 Linux 里有特殊地位,除了上面那条,还有一条:它要负责回收自己那些跑完的子进程。
普通进程跑完退出,得有人替它「收尸」它才算真正消失;没人收,它就变成僵尸挂在那儿(ps 里显示成 defunct)。平时这活儿有系统那个 1 号 init 兜底。
容器里没有那一层 init——你的 1 号进程就是容器里的 init,收尸的活儿落到了它头上。它要是只管干活、不理会这件事,跑完的子进程就会攒成一片僵尸。这不是每关都要操心的事,但它是「容器不适合随随便便跑多进程」的原因之一。
(这套「1 号为什么特殊」的机制,跟第 15 关的 namespace 是近邻——但那关讲的是内核怎么把进程隔开,本关讲的是进程自己的规矩。)
8.6 回头改第 2 关那句话
第 2 关 2.4 的坑里写过一句:
换掉启动命令之后,容器的主进程就成了你写的这个,它一退出,容器就停。
这句要收窄:容器停不停,取决于 1 号进程;你写的命令是不是 1 号,取决于那个 exec。
- 结尾
exec了 → 你的命令就是 1 号,那句话成立。 - 没有
exec→ 1 号还是那层壳,你的命令只是它的子进程,它退出容器未必停。
菜鸟阶段先给结论没错,现在把这一层补上。
这一关的产出:看懂「信号只发给 1 号进程」,以及 exec 为什么值钱——它决定你的程序是不是 1 号、能不能优雅退出。下一关回到镜像本身:怎么让它更小、构建得更快。
本关命令速查
进程与信号,见 命令速查表 · 进程与信号。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!





