Docker:pid1 zombie and signal problem

在类Unix系统中,pid是1的进程是一个很特殊的进程。类Unix操作系统的进程管理采用树形结构,用户空间中的其他进程都直接或间接是pid 1进程的子进程。在Mac OSX中该进程是launchd,Linux中有多种选择,例如systemd,一般将其称作init进程。如果init进程意外退出,内核会panic。为了避免意外操作,pid为1的进程不会使用系统默认的信号处理函数,要处理信号,必须显式指定信号处理函数。

init进程还承担着一些其他职责。由于所有用户空间的进程都是其子进程,所以,当进程结束时,其需要处理进程的资源回收。另外,某些情况下,父进程可能先于子进程退出,此时子进程没有了父进程变成所谓的‘孤儿’进程,init负责将此类进程收养为自己的子进程,并负责后续的资源回收。

Linux pid namespace技术可以实现pid资源的隔离,广泛应用于各种容器技术中,例如docker。但是在docker的早期版本中对init进程处理不当引起了一些问题,尤其是当多进程程序运行在容器中时。

容器启动时,运行的程序默认会被分配为pid 1,但是一般情况下,我们都没有假设我们的程序会以pid 1来运行。

这里可能面临僵尸进程的问题。看下面的例子,该程序fork了两次,一共创建了3个进程:父亲、儿子和孙子进程。儿子进程先结束,由于父进程没有任何处理,儿子进程变成僵尸进程,孙子进程变成孤儿进程,等待被收养;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <unistd.h>
#include <stdio.h>

void main()
{
pid_t pid = fork();
if (pid == 0)
{
printf("[C] child process.\n");
//avoid buffer get into child process
fflush(stdout);
pid_t pid2 = fork();
if (pid2 == 0)
{
printf("[C] 2nd fork, child process.\n");
sleep(15);
}
else
{
printf("[P] 2nd fork, child process pid: %d.\n", pid2);
sleep(8);
}
}
else
{
printf("[P] child process pid: %d.\n", pid);
sleep(20);
}
}

以上代码进行编译后,生成的可执行文件位于/tmp/test/a.out。将该目录映射到容器的/root/目录。

docker run -itd –name foo –rm -v /tmp/test/:/root/ ubuntu /root/a.out

以上命令执行后,在另一个窗口执行

docker exec -it foo /bin/bash

这样会打开一个容器的bash界面,通过它可以观察容器内程序的执行情况。
一开始三个进程都在执行。

 PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
     0      8      8      8 pts/1        15 Ss       0   0:00 /bin/bash
     8     15     15      8 pts/1        15 R+       0   0:00  \_ ps -jxf
     0      1      1      1 pts/0         1 Ss+      0   0:00 /root/a.out
     1      6      1      1 pts/0         1 S+       0   0:00 /root/a.out
     6      7      1      1 pts/0         1 S+       0   0:00  \_ /root/a.out

pid 6儿子进程结束,僵尸状态,pid 7孙子进程被init进程收养,即pid 1进程。

  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
     0      8      8      8 pts/1        22 Ss       0   0:00 /bin/bash
     8     22     22      8 pts/1        22 R+       0   0:00  \_ ps -jxf
     0      1      1      1 pts/0         1 Ss+      0   0:00 /root/a.out
     1      6      1      1 pts/0         1 Z+       0   0:00 [a.out] 
     1      7      1      1 pts/0         1 S+       0   0:00 /root/a.out

pid 7孙子进程结束,由于pid 1进程没有对其回收资源,故也进入僵尸状态。

  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
     0      8      8      8 pts/1        23 Ss       0   0:00 /bin/bash
     8     23     23      8 pts/1        23 R+       0   0:00  \_ ps -jxf
     0      1      1      1 pts/0         1 Ss+      0   0:00 /root/a.out
     1      6      1      1 pts/0         1 Z+       0   0:00 [a.out] 
     1      7      1      1 pts/0         1 Z+       0   0:00 [a.out] 

最后当父进程结束后,整个容器结束,所有资源被清理。

解决以上问题,需要容器内部运行一个有效的init进程,使用systemd这种高级程序会使容器臃肿,可以使用一些很轻便的实现,甚至自己写一个满足自己需求的程序。

现有的解决方案:

  • 使用baseimage-docker
  • 使用docker内置的init工具tini,在docker run命令加上参数–init。

Ref