03|理解进程(2):为什么我的容器里有这么多僵尸进程?

你好,我是程远。今天我们来聊一聊容器里僵尸进程这个问题。

说起僵尸进程,相信你并不陌生。很多面试官经常会问到这个知识点,用来考察候选人的操作系统背景。通过这个问题,可以了解候选人对Linux进程管理和信号处理这些基础知识的理解程度,他的基本功扎不扎实。

所以,今天我们就一起来看看容器里为什么会产生僵尸进程,然后去分析如何怎么解决。

通过这一讲,你就会对僵尸进程的产生原理有一个清晰的认识,也会更深入地理解容器init进程的特性。

问题再现

我们平时用容器的时候,有的同学会发现,自己的容器运行久了之后,运行ps命令会看到一些进程,进程名后面加了<defunct>标识。那么你自然会有这样的疑问,这些是什么进程呢?

你可以自己做个容器镜像来模拟一下,我们先下载这个例子,运行 make image 之后,再启动容器。

在容器里我们可以看到,1号进程fork出1000个子进程。当这些子进程运行结束后,它们的进程名字后面都加了标识。

从它们的Z stat(进程状态)中我们可以知道,这些都是僵尸进程(Zombie Process)。运行top命令,我们也可以看到输出的内容显示有 1000 zombie 进程。

# docker run --name zombie-proc -d registry/zombie-proc:v1
02dec161a9e8b18922bd3599b922dbd087a2ad60c9b34afccde7c91a463bde8a
# docker exec -it zombie-proc bash
# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0   4324  1436 ?        Ss   01:23   0:00 /app-test 1000
root         6  0.0  0.0      0     0 ?        Z    01:23   0:00 [app-test] <defunct>
root         7  0.0  0.0      0     0 ?        Z    01:23   0:00 [app-test] <defunct>
root         8  0.0  0.0      0     0 ?        Z    01:23   0:00 [app-test] <defunct>
root         9  0.0  0.0      0     0 ?        Z    01:23   0:00 [app-test] <defunct>
root        10  0.0  0.0      0     0 ?        Z    01:23   0:00 [app-test] <defunct>

…

root       999  0.0  0.0      0     0 ?        Z    01:23   0:00 [app-test] <defunct>
root      1000  0.0  0.0      0     0 ?        Z    01:23   0:00 [app-test] <defunct>
root      1001  0.0  0.0      0     0 ?        Z    01:23   0:00 [app-test] <defunct>
root      1002  0.0  0.0      0     0 ?        Z    01:23   0:00 [app-test] <defunct>
root      1003  0.0  0.0      0     0 ?        Z    01:23   0:00 [app-test] <defunct>
root      1004  0.0  0.0      0     0 ?        Z    01:23   0:00 [app-test] <defunct>
root      1005  0.0  0.0      0     0 ?        Z    01:23   0:00 [app-test] <defunct>
root      1023  0.0  0.0  12020  3392 pts/0    Ss   01:39   0:00 bash

# top
top - 02:18:57 up 31 days, 15:17,  0 users,  load average: 0.00, 0.01, 0.00
Tasks: 1003 total,   1 running,   2 sleeping,   0 stopped, 1000 zombie
…

那么问题来了,什么是僵尸进程?它们是怎么产生的?僵尸进程太多会导致什么问题?想要回答这些问题,我们就要从进程状态的源头学习,看看僵尸进程到底处于进程整个生命周期里的哪一环。

知识详解

Linux的进程状态

无论进程还是线程,在Linux内核里其实都是用 task_struct{}这个结构来表示的。它其实就是任务(task),也就是Linux里基本的调度单位。为了方便讲解,我们在这里暂且称它为进程。

那一个进程从创建(fork)到退出(exit),这个过程中的状态转化还是很简单的。

下面这个图是 《Linux Kernel Development》这本书里的Linux进程状态转化图。

我们从这张图中可以看出来,在进程“活着”的时候就只有两个状态:运行态(TASK_RUNNING)和睡眠态(TASK_INTERRUPTIBLE,TASK_UNINTERRUPTIBLE)。

那运行态和睡眠态这两种状态分别是什么意思呢?

运行态的意思是,无论进程是正在运行中(也就是获得了CPU资源),还是进程在run queue队列里随时可以运行,都处于这个状态。

我们想要查看进程是不是处于运行态,其实也很简单,比如使用ps命令,可以看到处于这个状态的进程显示的是R stat。

睡眠态是指,进程需要等待某个资源而进入的状态,要等待的资源可以是一个信号量(Semaphore), 或者是磁盘I/O,这个状态的进程会被放入到wait queue队列里。

这个睡眠态具体还包括两个子状态:一个是可以被打断的(TASK_INTERRUPTIBLE),我们用ps查看到的进程,显示为S stat。还有一个是不可被打断的(TASK_UNINTERRUPTIBLE),用ps查看进程,就显示为D stat。

这两个子状态,我们在后面的课程里碰到新的问题时,会再做详细介绍,这里你只要知道这些就行了。

除了上面进程在活的时候的两个状态,进程在调用do_exit()退出的时候,还有两个状态。

一个是 EXIT_DEAD,也就是进程在真正结束退出的那一瞬间的状态;第二个是 EXIT_ZOMBIE状态,这是进程在EXIT_DEAD前的一个状态,而我们今天讨论的僵尸进程,也就是处于这个状态中。

限制容器中进程数目

理解了Linux进程状态之后,我们还需要知道,在Linux系统中怎么限制进程数目。因为弄清楚这个问题,我们才能更深入地去理解僵尸进程的危害。

一台Linux机器上的进程总数目是有限制的。如果超过这个最大值,那么系统就无法创建出新的进程了,比如你想SSH登录到这台机器上就不行了。

这个最大值可以我们在 /proc/sys/kernel/pid_max这个参数中看到。

Linux内核在初始化系统的时候,会根据机器CPU的数目来设置pid_max的值。

比如说,如果机器中CPU数目小于等于32,那么pid_max就会被设置为32768(32K);如果机器中的CPU数目大于32,那么pid_max就被设置为 N*1024 (N就是CPU数目)。

对于Linux系统而言,容器就是一组进程的集合。如果容器中的应用创建过多的进程或者出现bug,就会产生类似fork bomb的行为。

这个fork bomb就是指在计算机中,通过不断建立新进程来消耗系统中的进程资源,它是一种黑客攻击方式。这样,容器中的进程数就会把整个节点的可用进程总数给消耗完。

这样,不但会使同一个节点上的其他容器无法工作,还会让宿主机本身也无法工作。所以对于每个容器来说,我们都需要限制它的最大进程数目,而这个功能由pids Cgroup这个子系统来完成。

而这个功能的实现方法是这样的:pids Cgroup通过Cgroup文件系统的方式向用户提供操作接口,一般它的Cgroup文件系统挂载点在 /sys/fs/cgroup/pids。

在一个容器建立之后,创建容器的服务会在/sys/fs/cgroup/pids下建立一个子目录,就是一个控制组,控制组里最关键的一个文件就是pids.max。我们可以向这个文件写入数值,而这个值就是这个容器中允许的最大进程数目。

我们对这个值做好限制,容器就不会因为创建出过多进程而影响到其他容器和宿主机了。思路讲完了,接下来我们就实际上手试一试。

下面是对一个Docker容器的pids Cgroup的操作,你可以跟着操作一下。

# pwd
/sys/fs/cgroup/pids
# df ./
Filesystem     1K-blocks  Used Available Use% Mounted on
cgroup                 0     0         0    - /sys/fs/cgroup/pids
# docker ps
CONTAINER ID        IMAGE                      COMMAND                  CREATED             STATUS              PORTS               NAMES
7ecd3aa7fdc1        registry/zombie-proc:v1   "/app-test 1000"         37 hours ago        Up 37 hours                             frosty_yalow

# pwd
/sys/fs/cgroup/pids/system.slice/docker-7ecd3aa7fdc15a1e183813b1899d5d939beafb11833ad6c8b0432536e5b9871c.scope

# ls
cgroup.clone_children  cgroup.procs  notify_on_release  pids.current  pids.events  pids.max  tasks
# echo 1002 > pids.max
# cat pids.max
1002

解决问题

刚才我给你解释了两个基本概念,进程状态和进程数目限制,那我们现在就可以解决容器中的僵尸进程问题了。

在前面Linux进程状态的介绍里,我们知道了,僵尸进程是Linux进程退出状态的一种。

从内核进程的do_exit()函数我们也可以看到,这时候进程task_struct里的mm/shm/sem/files等文件资源都已经释放了,只留下了一个stask_struct instance空壳。

就像下面这段代码显示的一样,从进程对应的/proc/<pid> 文件目录下,我们也可以看出来,对应的资源都已经没有了。

# cat /proc/6/cmdline
# cat /proc/6/smaps
# cat /proc/6/maps
# ls /proc/6/fd

并且,这个进程也已经不响应任何的信号了,无论SIGTERM(15)还是SIGKILL(9)。例如上面pid 6的僵尸进程,这两个信号都已经被响应了。

# kill -15 6
# kill -9 6
# ps -ef | grep 6
root         6     1  0 13:59 ?        00:00:00 [app-test] <defunct>

当多个容器运行在同一个宿主机上的时候,为了避免一个容器消耗完我们整个宿主机进程号资源,我们会配置pids Cgroup来限制每个容器的最大进程数目。也就是说,进程数目在每个容器中也是有限的,是一种很宝贵的资源。

既然进程号资源在宿主机上是有限的,显然残留的僵尸进程多了以后,给系统带来最大问题就是它占用了进程号。这就意味着,残留的僵尸进程,在容器里仍然占据着进程号资源,很有可能会导致新的进程不能运转。

这里我再次借用开头的那个例子,也就是一个产生了1000个僵尸进程的容器,带你理解一下这个例子中进程数的上限。我们可以看一下,1个init进程+1000个僵尸进程+1个bash进程 ,总共就是1002个进程。

如果pids Cgroup也限制了这个容器的最大进程号的数量,限制为1002的话,我们在pids Cgroup里可以看到,pids.current == pids.max,也就是已经达到了容器进程号数的上限。

这时候,如果我们在容器里想再启动一个进程,例如运行一下ls命令,就会看到 Resource temporarily unavailable 的错误消息。已经退出的无用进程,却阻碍了有用进程的启动,显然这样是不合理的。

具体代码如下:

### On host
# docker ps
CONTAINER ID        IMAGE                      COMMAND             CREATED             STATUS              PORTS               NAMES
09e6e8e16346        registry/zombie-proc:v1   "/app-test 1000"    29 minutes ago      Up 29 minutes                           peaceful_ritchie

# pwd
/sys/fs/cgroup/pids/system.slice/docker-09e6e8e1634612580a03dd3496d2efed2cf2a510b9688160b414ce1d1ea3e4ae.scope

# cat pids.max
1002
# cat pids.current
1002

### On Container
[root@09e6e8e16346 /]# ls
bash: fork: retry: Resource temporarily unavailable
bash: fork: retry: Resource temporarily unavailable

所以,接下来我们还要看看这些僵尸进程到底是怎么产生的。因为只有理解它的产生机制,我们才能想明白怎么避免僵尸进程的出现。

我们先看一下刚才模拟僵尸进程的那段小程序。这段程序里,父进程在创建完子进程之后就不管了,这就是造成子进程变成僵尸进程的原因。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

 

int main(int argc, char *argv[])
{
       int i;
       int total;

       if (argc < 2) {
              total = 1;
       } else {
              total = atoi(argv[1]);
       }

       printf("To create %d processes\n", total);

       for (i = 0; i < total; i++) {
              pid_t pid = fork();
 
              if (pid == 0) {
                      printf("Child => PPID: %d PID: %d\n", getppid(),
                             getpid());
                      sleep(60);
                      printf("Child process exits\n");
                      exit(EXIT_SUCCESS);
              } else if (pid > 0) {
                      printf("Parent created child %d\n", i);
              } else {
                      printf("Unable to create child process. %d\n", i);
                      break;
              }
       }

       printf("Paraent is sleeping\n");
       while (1) {
              sleep(100);
       }

       return EXIT_SUCCESS;
}

前面我们通过分析,发现子进程变成僵尸进程的原因在于父进程“不负责”,那找到原因后,我们再想想,如何来解决。

其实解决思路很好理解,就好像熊孩子犯了事儿,你要去找他家长来管教,那子进程在容器里“赖着不走”,我们就需要让父进程出面处理了。

所以,在Linux中的进程退出之后,如果进入僵尸状态,我们就需要父进程调用wait()这个系统调用,去回收僵尸进程的最后的那些系统资源,比如进程号资源。

那么,我们在刚才那段代码里,主进程进入sleep(100)之前,加上一段wait()函数调用,就不会出现僵尸进程的残留了。

      for (i = 0; i < total; i++) {
            int status;
            wait(&status);
      }

而容器中所有进程的最终父进程,就是我们所说的init进程,由它负责生成容器中的所有其他进程。因此,容器的init进程有责任回收容器中的所有僵尸进程。

前面我们知道了wait()系统调用可以回收僵尸进程,但是wait()系统调用有一个问题,需要你注意。

wait()系统调用是一个阻塞的调用,也就是说,如果没有子进程是僵尸进程的话,这个调用就一直不会返回,那么整个进程就会被阻塞住,而不能去做别的事了。

不过这也没有关系,我们还有另一个方法处理。Linux还提供了一个类似的系统调用waitpid(),这个调用的参数更多。

其中就有一个参数WNOHANG,它的含义就是,如果在调用的时候没有僵尸进程,那么函数就马上返回了,而不会像wait()调用那样一直等待在那里。

比如社区的一个容器init项目tini。在这个例子中,它的主进程里,就是不断在调用带WNOHANG参数的waitpid(),通过这个方式清理容器中所有的僵尸进程。

int reap_zombies(const pid_t child_pid, int* const child_exitcode_ptr) {
        pid_t current_pid;
        int current_status;

        while (1) {
                current_pid = waitpid(-1, &current_status, WNOHANG);

                switch (current_pid) {
                        case -1:
                                if (errno == ECHILD) {
                                        PRINT_TRACE("No child to wait");
                                        break;
                                }

…

重点总结

今天我们讨论的问题是容器中的僵尸进程。

首先,我们先用代码来模拟了这个情况,还原了在一个容器中大量的僵尸进程是如何产生的。为了理解它的产生原理和危害,我们先要掌握两个知识点:

  • Linux进程状态中,僵尸进程处于EXIT_ZOMBIE这个状态;
  • 容器需要对最大进程数做限制。具体方法是这样的,我们可以向Cgroup中 pids.max这个文件写入数值(这个值就是这个容器中允许的最大进程数目)。

掌握了基本概念之后,我们找到了僵尸进程的产生原因。父进程在创建完子进程之后就不管了。

所以,我们需要父进程调用wait()或者waitpid()系统调用来避免僵尸进程产生。

关于本节内容,你只要记住下面三个主要的知识点就可以了:

  1. 每一个Linux进程在退出的时候都会进入一个僵尸状态(EXIT_ZOMBIE);
  2. 僵尸进程如果不清理,就会消耗系统中的进程数资源,最坏的情况是导致新的进程无法启动;
  3. 僵尸进程一定需要父进程调用wait()或者waitpid()系统调用来清理,这也是容器中init进程必须具备的一个功能。

思考题

如果容器的init进程创建了子进程B,B又创建了自己的子进程C。如果C运行完之后,退出成了僵尸进程,B进程还在运行,而容器的init进程还在不断地调用waitpid(),那C这个僵尸进程可以被回收吗?

欢迎留言和我分享你的想法。如果你的朋友也被僵尸进程占用资源而困扰,欢迎你把这篇文章分享给他,也许就能帮他解决一个问题。

精选留言

  • 莫名

    2020-11-20 08:36:06

    C 应该不会被回收,waitpid 仅等待直接 children 的状态变化。

    为什么先进入僵尸状态而不是直接消失?觉得是留给父进程一次机会,查看子进程的 PID、终止状态(退出码、终止原因,比如是信号终止还是正常退出等)、资源使用信息。如果子进程直接消失,那么父进程没有机会掌握子进程的具体终止情况。一般情况下,程序逻辑可能会依据子进程的终止情况做出进一步处理:比如 Nginx Master 进程获知 Worker 进程异常退出,则重新拉起来一个 Worker 进程。
    作者回复

    谢谢 @莫名! 很好的解释!

    2020-11-20 12:40:01

  • JianXu

    2020-11-28 17:10:26

    问题一:在Kubernetes 的情况下,是不是该节点上所有的容器都是kubelet 的子进程?不然kubelet 怎么来清理这些容器产生的僵尸进程呢?

    问题二:在Docker 的场景下,容器第一个进程是用户自己写的进程,而该进程是不能保证在回收子进程资源上的质量的,所以才有Tinit 等工具,那为什么docker runtime 不默认把这样的回收功能做了呢?

    问题三:Linux 为什么不设计成可以kill -9 杀死僵尸进程呢?现在把希望都寄托在父亲进程的代码质量上,而要init 回收,就得把init 到 僵尸进程之间的血缘进程全部杀死。为什么要做这样的设计呢?
    作者回复

    > 问题一
    在kuberenetes下,kubelet还是调用 containerd/runc去启动容器的,每个容器的父进程是containerd-shim, 最终shim可以回收僵尸进程。

    > 问题二
    docker倒是也做了这件事。 用docker启动容器的时候 加--init参数,起来的容器就强制使用tini作为init进程了。

    > 问题三
    Linux进程要响应SIGKILL并且执行signal handler,只有在被进程调度到的时候才可以做。对于zombie进程,它已经是不可被调度的进程了。

    2020-11-29 16:48:15

  • 上邪忘川

    2020-11-22 22:06:30

    总结一下这节课相关的东西
    1.,父进程在创建完子进程之后就不管了,而每一个 Linux 进程在退出的时候都会进入一个僵尸状态,这时这些进入僵尸状态的进程就因为无法回收变成僵尸进程。
    2.僵尸进程是无法直接被kill掉的,需要父进程调用wait()或watipid()回收。
    3.清理僵尸进程的两个思路
    (1)kill掉僵尸进程的父进程,此时僵尸进程会归附到init(1)进程下,而init进程一般都有正常的wait()或watipid()回收机制。
    (2)利用dumb-init/tini之类的小型init服务来解决僵尸进程
    作者回复

    @上邪忘川, 谢谢你的总结

    2020-11-23 11:47:00

  • 水蒸蛋

    2020-11-22 11:20:07

    老师您的意思是僵尸线程默认都不会自动关闭的,全靠父进程回收,如果产生大量僵尸进程说明父进程相关回收策略有问题是吗
    作者回复

    对的

    2020-11-22 14:13:13

  • 朱雯

    2020-11-22 21:09:03

    最后我作为一个运维工程师,我还是不知道怎么处理僵死进程,第一我可能不能直接杀死他们的父进程,因为可能有用,第二,我无法kill掉他们,第三我无法修改代码,代码本身对我是黑盒子。
    作者回复

    我觉得刚才 @上邪忘川的回复挺好的

    3.清理僵尸进程的两个思路
    (1)kill掉僵尸进程的父进程,此时僵尸进程会归附到init(1)进程下,而init进程一般都有正常的wait()或watipid()回收机制。
    (2)利用dumb-init/tini之类的小型init服务来解决僵尸进程

    2020-11-23 11:54:41

  • Helios

    2020-11-20 23:33:13

    僵尸进程也是进程,就是资源没有被回收,父进程还活着就不会被init回收。
    补充一点
    子进程推出的时候会给父进程发送个信号,如果父进程不处理这个信号就会变味僵尸进程。现在一般只会出现在c这种需要手动垃圾回收得语言了。

    老师是踩过坑呢,感觉这个坑不好踩,一是因为高级语言会处理信号,就像上一节说的。还有就是啥业务场景能搞三万多进程
    作者回复

    @Helios,
    对于容器或者说pod, 我们加了pids cgroup的限制,pids.max 对于每个容器一般就是以千为单位了,这个值还是很容易达到上限的。

    我们在线上看到的大量Z进程,实际的情况要复杂一些,一个进程有多个线程,主进程处于Z状态,而还有一个线程处于D状态,但是从表象查看进程状态的时候,看到都是<defunct>进程了(Z)。由于有了D的线程在里面,这时候waitpid(), 任何信号对这些进程都无效了。
    这一讲,我是把Z进程的概念单独说了一下,对于D进程,它会引起其他的一些现象,我会在后面讲到。

    2020-11-21 09:28:03

  • Delia

    2020-11-25 09:51:55

    我是一个Docker新手,请教一下老师,经常看到一些容器僵尸,状态栏显示:Exited (2) 10 days ago,Exited (1) 10 days ago,Exited (100) 10 days ago等等,这些容器为啥不能被回收呢?目前只能docker rm清理掉。
    作者回复

    docker 自己没有自动清理的功能。如果是kubernetes/kubelet是会做清理。

    2020-11-25 15:07:03

  • nuczzz

    2021-01-12 10:51:21

    k8s+kata集群会有这个问题吗?感觉kata做了内核隔离,容器里的僵尸进程应该影响不到宿主机了
    作者回复

    kata是基于VM的,它里面的僵尸进程不会影响到宿主机。

    2021-01-18 08:42:47

  • 1900

    2020-11-20 11:17:32

    老师你好 关于设置容器的Cgroup 中 pids.max配置 ,是跟业务进程一起运行在容器中然后修改当前的容器配置,是否还有其他优雅的方式呢 ?
    作者回复

    对容器设置pids Cgroup限制,一般需要容器云平台来做,在启动容器的时候就自动的设置好,类似cpu/memory Cgroup的设置。

    像Kubernetes的类似这么做,
    https://github.com/kubernetes/kubernetes/commit/ecd6361ff0e8421332a50e55fcba17b823d5d338

    2020-11-20 12:36:23

  • MiracleWong

    2021-12-07 15:50:55

    记录一下:
    OS:Ubuntu 20.04 LTS
    Docker: 20.10.11

    pids.max 的地址路径为:/sys/fs/cgroup/pids/docker/docker的id号
    举例:我的为:/sys/fs/cgroup/pids/docker/3a60099ad574e6fc4e72a0d3e8b292150f5e540af1eb09546de8bd757db81c6e

    大家可以在/sys/fs/cgroup/下,通过命令:find . -name *你的实际Docker Id*
    如:find . -name *3a60099* 查找即可。

    通过命令: echo 1002 > pids.max
    可以复现现象:
    bash: fork: retry: Resource temporarily unavailable

    更改回去:echo max > pids.max ,再执行命令回归正常。

    在 /sys/fs/cgroup/pids/docker/ 下(非docker id路径下),也有一个pids.max 和 pids.currrent。尝试了下,此处 echo 1002 > pids.max 也可以复现 “Resource temporarily unavailable”现象。

    这里应该是docker 路径下pids.max 的总控,docker id 路径下(如3a60099ad574e6fc4e72a0d3e8b292150f5e540af1eb09546de8bd757db81c6e)的pids.max 则是单独容器的控制
  • 争光 Alan

    2021-03-17 09:50:38

    老师,我最近发现ulimit也会限制进程数量,这两个有什么区别吗?在容器内具体是哪个在生效?
    作者回复

    ulimit对于shell session中的总进程数量做限制,pids cgroup对于控制块中的cgroup.procs/tasks 中的进程数目做限制。都会起效,看哪个先达到限制值。

    2021-03-20 21:44:58

  • 北眸

    2021-03-11 11:21:00

    线程id会消耗/proc/sys/kernel/pid_max吗?
    作者回复

    会的

    2021-03-13 20:22:33

  • 疯琴

    2021-06-03 16:51:16

    老师,用 docker run --init 启动 registry/sig-proc:v1 以后的确可以在容器内通过 kill 1 退出,但是我看SigCgt 仍然是 0000000000000000 并没有注册 sigterm 的 handler,请问这是为什么?
    作者回复

    可以看一下tini的代码

    2021-08-13 22:33:21

  • 心随缘

    2021-03-09 19:18:18

    子进程在结束前不一定都需要经过一个僵尸状 EXIT_ZOMBIE过程,主要在于其父进程是否显示忽略它的退出信号,如果不显示忽略,那么在父进程处理其退出信号或者父进程结束前,子进程都是处于 EXIT_ZOMBIE状态。当父进程退出时,子进程都会过继给 init进程,僵尸子进程也一样, init进程会负责清理这些僵尸进程。
  • 佳伦

    2021-01-31 22:32:57

    我一般喜欢用/bin/bash启动容器,貌似没啥好的办法
  • Shone

    2021-01-10 13:36:04

    老师,请教一下。之前我们在istio-proxy sidecar上通过exec去做readinessprobe就遇到了这个问题,是因为istio sidecar的1号进程是pilot-agent,它不能回收僵尸进程,后来的改动是用shell脚本包了一下pilot-agent,而 bash是可以回收僵尸进程的。我有两个问题:
    1,是什么样的情况会导致僵尸进程?这个问题之前没有普遍出现,只有个别的pod出现了。
    2,这个看起来也和kubelet通过exec做健康检查有关系,它隐含了一个条件是要容器自己负责回收僵尸进程,有没有可能改进kubelet来解决这个问题?
    作者回复

    @Shone
    >1
    你说的个别pod里出现僵尸进程,这个还要看一下pilot-agent下的那写进程的行为,是不是只是个别pod中的子进程在某些条件下退出了,而其他pod里并没有进程退出的状况。

    >2
    kubelet的Health check应该和僵尸进程没有关系。回收僵尸进程更多的是父进程的责任。kubelet 让containerd去创建容器,它不在容器进程树结构上。Kubelet可以负责回收残留的容器,但是在容器活着的时候,它不用负责容器namespace中的残留进程。

    2021-01-13 23:19:41

  • Action

    2021-01-06 14:24:55

    老师 这个没太懂什么意思:“从内核进程的 do_exit() 函数我们也可以看到,这时候进程 task_struct 里的 mm/shm/sem/files 等文件资源都已经释放了,只留下了一个 stask_struct instance 空壳“
    作者回复

    task_struct 是内核中描述进程的结构,每个进程申请的内存,打开的文件等信息都记录在这个结构里。当进程退出的时候,需要先把进程里的这些资源释放,最后再释放task_struct结构。

    2021-01-06 23:01:40

  • 朱雯

    2020-11-22 21:03:55

    关于思考题:那肯定是不行的,因为进程的父进程还在,必须好似父进程调用waitpid来操作,其他人是无权的,子进程的子进程不是我的子进程,除非父进程死去,死去后应该是由于init进程直接收养,也不会被父进程的父进程收养。
  • 朱雯

    2020-11-22 21:02:15

    老师好,
    问题1:修改容器的pid.max怎么修改,我看到您直接修改宿主机的一个目录,但是我并没有类似的目录名称。
    问题2: for (i = 0; i < total; i++) { int status; wait(&status); } status这个int类型的值都没有被赋值,wait又是如何知道他要回收哪个呢,同样的问题,也在于waitpid。
    问题3:既然知道了pid.maxs这个文件,那我有个想法,那就是把宿主机的这个文件无限的改大,那岂不是无限进程启动了,虽然我在宿主机上没找到这个文件。
    最后,这居然是一门操作系统的课程,我之前挺害怕os的,现在感觉找到一切切入点了,可以尝试一下学一些原理了,谢谢老师。
    作者回复

    @朱雯
    > 问题1
    你可以到 /sys/fs/cgroup/pids 目录下试试搜索一下container id, 或者带docker关键字的目录。

    # pwd
    /sys/fs/cgroup/pids
    # find . -name *7bab7f79d70c*
    ./system.slice/docker-7bab7f79d70cbf7a59344856eecfb52c1e1d4706d3fbe3e4b74c5172ea7af541.scope
    # find . -name docker*
    ./system.slice/docker.service
    ./system.slice/docker-7bab7f79d70cbf7a59344856eecfb52c1e1d4706d3fbe3e4b74c5172ea7af541.scope
    ./system.slice/docker.socket

    > 问题2
    wait()是不指定要回收哪个pid的,只要是它的子进程退出就可以回收。waitpid()是有参数pid的,可以指定要回收的进程。

    > 问题3
    把pid.maxs改成无限大,那么如果真的有海量进程在系统中,那么从内核内存,进程的调度,都会出现更多的问题或者系统直接就死了。

    > 最后,这居然是一门操作系统的课程,我之前挺害怕os的,现在感觉找到一切切入点了
    的确是这样的,很多容器的问题就是归结到OS的问题。学习容器也是一个很好的学习OS的切入点!

    >

    2020-11-23 13:32:33

  • 水蒸蛋

    2020-11-20 17:39:44

    老师,我还是没看明白僵尸线程的产生,它就是卡在exit zomble状态的进程,那这个产生是必然的吗,就和垃圾一样运行时间长了肯定会有一些垃圾产生,需要做的就是用主线程清理,我平时碰到这个都是重启对应的服务
    作者回复

    僵尸进程是进程的一个状态, 只要进程退出就会有这状态。清理的僵尸进程的是它的父进程。
    这个和服务进程运行时间长了之后,通过重启进程去清理泄漏的资源,比如内存或者文件fd, 是两个概念。

    2020-11-20 20:57:08