一次 Docker 容器内大量僵尸进程排查分析( 二 )


孤儿进程:不能同年同月同日生 , 也不会同年同月同日死接下来问一个问题 , 父进程挂掉时 , 子进程会挂掉吗?
想象现实中的场景 , 父亲不在了 , 儿子还可以活吗?答案是肯定的 。对应于进程 , 父进程退出时 , 子进程会继续运行 , 不会一起共赴黄泉 。
一个父进程已经终止的进程被称为孤儿进程(orphan process) 。操作系统这个大家长是比较人性化的 , 没有人管的孤儿进程会被进程 ID 为 1 的进程接管 。这个 PID 为 1 的进程后面还会再讲到 。
接下来对之前的代码稍作修改 , 让父进程 fork 子进程以后自杀退出 , 生成孤儿进程 。代码如下所示 。
#include <unistd.h>#include <stdio.h>#include <stdlib.h>int main() {printf("before fork, pid=%dn", getpid());pid_t childPid;switch (childPid = fork()) {case -1: {printf("fork error, %dn", getpid());exit(1);}case 0: {printf("in child process, pid=%dn", getpid());sleep(100000); // 子进程 sleep 不退出break;}default: {printf("in parent process, pid=%d, child pid=%dn", getpid(), childPid);exit(0); // 父进程退出}}return 0;}编译运行上面的代码
gcc fork_demo.c -o fork_demo; ./fork_demo输出结果如下 。
before fork, pid=21629in parent process, pid=21629, child pid=21630in child process, pid=21630可以看到父进程 id 为 21629 ,  生成的子进程 id 为 21630 。
使用 ps 查看当前进程信息 , 结果如下所示 。
UIDPIDPPIDC STIME TTYTIME CMDroot100 12月12 ?00:00:53 /usr/lib/systemd/systemd --system --deserialize 21ya2163010 19:26 pts/800:00:00 ./fork_demo【一次 Docker 容器内大量僵尸进程排查分析】可以看到此时孤儿子进程 21630 的父 ID 已经变为了顶层的 ID 为 1 的进程 。
僵尸进程父进程负责生 , 如果不负责养 , 那就不是一个好父亲 。子进程挂了 , 如果父进程不给子进程“收尸”(调用 wait/waitpid) , 那这个子进程小可怜就变成了僵尸进程 。
新建一个 make_zombie.c 文件 , 内容如下 。
#include <stdio.h>#include <stdlib.h>#include <unistd.h>int main() {printf("pid %dn", getpid());int child_pid = fork();if (child_pid == 0) {printf("-----in child process:%dn", getpid());exit(0);} else {sleep(1000000);}return 0;}编译运行上面的代码 , 就可以生成一个进程号为 22538 的僵尸进程 , 如下所示 。
UIDPIDPPIDC STIME TTYTIME CMDya22537 207590 19:57 pts/800:00:00 ./make_zombieya22538 225370 19:57 pts/800:00:00 [make_zombie] <defunct>CMD 名中的 defunct 表示这是一个僵尸进程 。
也使用 ps 命令查看进程的状态 , 显示为 "Z" 或者 "Z+" 表示这是一个僵尸进程 , 如下所示 。
ps -ho pid,state -p 2253822538 Z子进程退出后绝大部分资源已经被释放可供其他进使用 , 但是内核的进程表中的槽位没有释放 。
僵尸进程有一个很神奇的特性 , 使用 kill -9 必杀信号都没有办法杀掉僵尸进程 , 这样的设计利弊参半 , 好的地方是父进程可以总是有机会执行 wait/waitpid 等命令收割子进程 , 坏的地方是无法强制回收这种僵尸进程 。
PID 为 1 的进程linux 中内核初始化以后会启动系统的第一个进程 , PID 为 1 , 也可以称之为 init 进程或者根(ROOT)进程 。在我的 centos 机器上 , 这个 init 进程是 systemd , 如下所示 。
UIDPIDPPIDC STIME TTYTIME CMDroot100 12月12 ?00:00:54 /usr/lib/systemd/systemd --system --deserialize 21在我的 Mac 电脑上 , 这个进程为 launchd , 如下所示 。
UIDPIDPPIDC STIMETTYTIME CMD0100 六04下午 ??28:40.65 /sbin/launchdinit 进程有下面这几个功能

  • 如果一个进程的父进程退出了 , 那么这个 init 进程便会接管这个孤儿进程 。
  • 如果一个进程的父进程未执行 wait/waitpid 就退出了 , init 进程会接管子进程并自动调用 wait 方法 , 从而保证系统中的僵尸进程可以被移除 。
  • 传递信号给子进程 , 这点后面会介绍 。
为什么 Node.js 不适合做 Docker 镜像中 PID 为 1 的进程在 Node.js 的官方最佳实践里有写到 "Node.js was not designed to run as PID 1 which leads to unexpected behaviour when running inside of Docker." 。下图来自 github.com/nodejs/dock…。


推荐阅读