天天看点

Docker和孤儿进程、僵尸进程Docker和孤儿进程、僵尸进程

在unix/linux系统中,正常情况下,子进程是通过父进程fork创建的。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束。 当一个进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。

父进程先于子进程退出,那么子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)接管,并由init进程对它完成状态收集(wait/waitpid)工作。

运行结果如图: 父进程退出后,子进程的父进程(ppid)变为1,被init进程接管

Docker和孤儿进程、僵尸进程Docker和孤儿进程、僵尸进程

子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵尸进程

运行结果如图:子进程(pid=2158)成为了僵尸进程

Docker和孤儿进程、僵尸进程Docker和孤儿进程、僵尸进程

在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括进程号、退出状态、运行时间等)。直到父进程通过wait / waitpid来取时才释放。 如果父进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,系统所能使用的进程号是有限的,如果大量的产生僵尸进程,可能导致系统不能产生新的进程.

在docker容器中运行的进程,一般是没有init进程的。可以进入容器使用 ps 查看,会发现 pid 为 1 的进程并不是 init,而是容器的主进程。如果容器中产生了孤儿进程,谁来接管这个进程?

找到相同线程组里其它可用线程

沿着它的进程树向祖先进程找一个最近的child_subreaper并且运行着的进程

该namespace下进程号为1的进程

docker daemon从1.11版后从架构上发生了比较大的变化,由原来的一个模块拆分为4个独立的模块:engine、containerd、runc、containerd-shim,将容器的生命周期管理交给containerd, containerd再使用runc运行容器。

Docker和孤儿进程、僵尸进程Docker和孤儿进程、僵尸进程

架构上的变化也改变了docker容器运行时的进程树的结构,这里运行一个简单的docker镜像,并通过<code>ps xf -o pid,ppid,stat,args</code>查看进程树,从进程树中也可以看出docker daemon架构的变化

docker 1.11之后

Docker和孤儿进程、僵尸进程Docker和孤儿进程、僵尸进程

docker 1.11之前

Docker和孤儿进程、僵尸进程Docker和孤儿进程、僵尸进程

准备两个文件parent.sh、child.sh

运行docker,此时sleep进程的为容器首进程,pid为1

进入容器,并运行parent.sh

在容器中通过<code>ps xf -o pid,ppid,stat,args</code>查看进程树可以看到进程结构如下, sleep作为容器启动命令,它的进程号为1,根据上一节关于linux接收孤儿进程的描述,当没有其他符合条件的进程接收时,该进程就会成为孤儿进程的接收者

Docker和孤儿进程、僵尸进程Docker和孤儿进程、僵尸进程

接下来通过<code>kill -9</code>杀死运行parent.sh的进程,此时运行child.sh的进程就成为了孤儿进程,这个时候docker容器是如何处理孤儿进程的接收的呢?docker 1.11之前和之后版本的处理是有所区别的

先来看下docker 1.11版之前容器内的进程树(如下图),可以看到运行child.sh的进程的父进程变为了1(sleep进程)

Docker和孤儿进程、僵尸进程Docker和孤儿进程、僵尸进程

再来看下docker 1.11版之后版本容器内的进程树(如下图),可以看到child.sh进程的父进程变成了0,与sleep处于同一个层级,那么是谁接收了这个孤儿进程呢?

Docker和孤儿进程、僵尸进程Docker和孤儿进程、僵尸进程

此时需要查看主机的进程树才能确定孤儿进程到底是被谁接收了,在主机上运行<code>ps xf -o pid,ppid,stat,args</code>,结果如下图

docker1.11版本之前孤儿进程是由容器内pid为1的进程接收,而1.11版本后是由docker-containerd-shim进程接收

关于僵尸进程的概念以及产生的原因上面已经阐述过了,僵尸进程是指子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。我们这里只讨论docker中的孤儿进程机制是否会导致僵尸进程的产生,这个也是docker早期版本被诟病的问题。

1.11版本前,孤儿进程是被容器内pid为1的进程所接收。上面关于孤儿进程的实验中,容器中pid为1的进程为sleep进程,而sleep进程是不会对子进程退出进行wait/waitpid操作的,所以我们kill掉child.sh进程就会产生僵尸进程(如下图)

上图可以看到运行child.sh的进程和sleep进程都成为了僵尸进程,这里sleep进程成为僵尸进程是由于sleep进程是child.sh的子进程,当child.sh退出时,sleep进程成为了孤儿进程并被pid为1的sleep进程所接收,当sleep运行结束时(这里运行的是sleep 10)退出,pid为1的sleep进程不进行wait/waitpid操作,就使得sleep进程成为僵尸进程

1.11版本后,孤儿进程是被docker-containerd-shim进程接收,如果docker-containerd-shim在子进程退出时调用wait/waitpid就不会产生僵尸进程,反之就会产生僵尸进程。这里也进行相同的操作,kill掉运行child.sh的进程,结果如下图

Docker和孤儿进程、僵尸进程Docker和孤儿进程、僵尸进程

从结果上看child.sh和sleep(child.sh的子进程)进程都正常退出(进程树上看不到),并没有产生僵尸进程。所以docker-containerd-shim会在子进程退出时调用wait/waitpid。从源码中看下docker-containerd-shim的处理

docker1.11之前的版本,孤儿进程是否有可能成为僵尸进程取决于容器内pid为1的进程是否在子进程退出时调用wait/waitpid, docker1.11版本之后孤儿进程不会成为僵尸进程