孤儿进程、僵尸进程与 Docker

未经授权,不得全文转载。转载前请先阅读本站版权声明

前言(Intro)

我们知道当子进程还在运行时,其直接父进程被 kill 掉以后,这个子进程会变成孤儿进程(orphaned process),然后它会被 init 进程接管。

在 docker 容器中运行的进程,一般是没有 init 进程的。你可以进入容器使用 ps 查看,会发现 pid 为 1 的进程并不是 init,而是容器的主进程。

那么问题来了,如果容器中产生了孤儿进程,谁来接管这个进程?

孤儿进程如何被接管?

其实“孤儿进程是会被 init 进程接管的”并不是真相,只不过结论上来说是这样。
网上也有人说是孤儿进程会被 pid 为 1 的进程接管,而 init 进程刚好就是那个 pid 为 1 的进程。这说的不是很准确。

不如直接来看 linux 内核源码

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
30
31
32
33
34
35
36
37
38
/*
* When we die, we re-parent all our children, and try to:
* 1. give them to another thread in our thread group, if such a member exists
* 2. give it to the first ancestor process which prctl'd itself as a
* child_subreaper for its children (like a service manager)
* 3. give it to the init process (PID 1) in our pid namespace
*/
static struct task_struct *find_new_reaper(struct task_struct *father,
struct task_struct *child_reaper)
{
struct task_struct *thread, *reaper;

thread = find_alive_thread(father);
if (thread)
return thread;

if (father->signal->has_child_subreaper) {
/*
* Find the first ->is_child_subreaper ancestor in our pid_ns.
* We start from father to ensure we can not look into another
* namespace, this is safe because all its threads are dead.
*/
for (reaper = father;
!same_thread_group(reaper, child_reaper);
reaper = reaper->real_parent) {
/* call_usermodehelper() descendants need this check */
if (reaper == &init_task)
break;
if (!reaper->signal->is_child_subreaper)
continue;
thread = find_alive_thread(reaper);
if (thread)
return thread;
}
}

return child_reaper;
}

接管分三步,第一步是找到相同线程组里其他可用的线程,如果没有找到则进行第二步。至于线程和进程的关系,这个回答非常形象。

对于第二步的原理,可以参考 prctl 的 PR_SET_CHILD_SUBREAPER 参数 里的描述。
prctl 是 linux 内核暴露的函数,用来查看或修改进程和线程的信息。PR_SET_CHILD_SUBREAPER 可以给某个进程标识为 child_subreaper,它有这么一段说明:

When a process is marked as a child subreaper, all of the children
that it creates, and their descendants, will be marked as
having a subreaper.

翻译即:当一个进程被标记为 child_subreaper,这个进程所创建的所有子进程,包括子进程的子进程,都将被标记拥有一个 subreaper。

那么标记 subreaper 有什么作用呢?

Upon termination of a process that is orphaned (i.e., its immediate parent has already terminated)
and marked as having a subreaper, the nearest still living ancestor subreaper will receive a SIGCHLD
signal and be able to wait(2) on the process to discover its termination status.

也就是说:当一个进程成为了孤儿进程,并且被标记为拥有一个 subreaper。那么会沿着它的进程树向祖先进程找一个最近的是 child_subreaper 并且运行着的进程,这个进程将会接管这个孤儿进程。

如果第二步也没有找到,则进行第三步,即使用 pid 为 1 的进程来接管孤儿进程。

而 init 进程通常第一个启动的进程,其他所有进程都是 init 的子进程,所以一般来说最后所有孤儿进程都将被 init 进程接管。
举个特殊情况的例子,比如使用 docker 执行 docker run --name ubuntu -d ubuntu /bin/bash -c 'sleep 1000'(这里使用 ubuntu 官方镜像),然后你进入容器中执行 ps 就会发现 pid 为 1 的进程是 sleep。

不过也可以做到人为创建一个进程,将它标记为 child_subreaper,使其接管其创建的所有孤儿进程,从而不必让 init 进程接管。(具体实现,可以参考 krallin/tini

孤儿进程被接管的流程大致就是这样。

僵尸进程的危害与应对

当子进程退出时,没有被父进程通过 wait/waitpid 来回收的话,该子进程就变成了僵尸进程(zombie process)。
僵尸进程虽然已经释放了大部分它占用的文件,和占用的内存,但仍有少部分信息仍然在占用,比如进程号、进程的退出状态、资源占用信息列表。
它仍然会给系统带来负担。有可能导致不能创建新进程。

所以要避免产生僵尸进程,主要要做的是在父进程上,保证不忽略 SIGCHLD 信号,用 wait/waitpid 来等待子进程结束之类的。
然而 docker 有一些坑,比如《Docker and the PID 1 zombie reaping problem》 就是很典型的一例。
这篇文章的作者,也写了一个应对方法:phusion/baseimage-docker,其中自带了它自己的 init 系统,用来保证在容器内部产生的孤儿进程一定会被接管。

当然,你也可以使用其他轻量级的 init 系统:

结论(Conclusion)

如果容器中产生了孤儿进程,谁来接管这个进程?

答案是容器中 pid 为 1 的那个进程。

参考(Bibliographies)

引用(References)

Star Blog on GitHub