ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

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

2021-05-30 15:01:47  阅读:281  来源: 互联网

标签:03 00 容器 0.0 进程 pids root


本文仅作为学习记录,非商业用途,侵删,如需转载需作者同意。

一、问题再现

例子:https://github.com/chengyli/training/tree/master/init_proc/zombie_proc

目的:在容器中弄出来很多个僵尸进程(Zombie Process)。


# 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
…

二、知识详解

2.1、Linux的进程状态

Linux 内核中都是用 task_struct{} 这个结构来表示,进程和线程,其实就是任务(task),Linux 里基本的调度单位。

进程“活着”时只有两个状态:
运行态(TASK_RUNNING)
睡眠态(TASK_INTERRUPTIBLE,TASK_UNINTERRUPTIBLE)

在这里插入图片描述

运行态:
运行中(获得了CPU资源);
进程在run queue 队列里随时可以运行

ps 查看进程 R stat 就表示处于运行态

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

睡眠态分为两个子状态:
1、可以被打断的(TASK_INTERRUPTIBLE), ps
查看显示 S stat
2、不可被打断的(TASK_UNINTERRUPTIBLE),ps 查看 显示 D stat


进程在调用 do_exit() 退出的时候,还有两个状态:
1、EXIT_DEAD 进程真正结束退出的一瞬间的状态
2、EXIT_ZOMBIE ,这是进程在 EXIT_DEAD 前的一个状态,今天讨论的僵尸进程就是处于这个状态中。

2.2、限制容器中进程数目

Linux 进程总数是有限制的,超过最大值,系统就无法创建新的进程,例如SSH 登录也不行了。

如下可以文件位置,可以查看进程总数最大值

[root@jyzx-tower2 ~]# cat /proc/sys/kernel/pid_max 
32768
[root@jyzx-tower2 ~]# 

Linux 内核在初始化系统的时候,会根据CPU的数目来设置 pid_max的值。
CPU 数目小于32,pid_max 就会被设置为 32768
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 ,这个文件中写入数值就是容器允许的最大进程数目。

pids.max 中是 max 时表示没做任何限制。


# 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

2.3、解决问题

僵尸进程是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)

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

当多个容器运行在同一个宿主机上的时候,为了避免一个容器消耗完整个宿主机的进程号资源,会配置 pids Cgroup 来限制每个容器的最大进程数目。

如果僵尸进程太多,pids.current == pids.max 新的进程就运行不了了。

例如这个时候运行一个 ls 命令,会报错:


### 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()系统调用有个问题:是一个阻塞的调用,如果没有子进程是僵尸进程的话,这个调用就一直不会返回。那么整个进程就会被阻塞住,而不能去做别的事情。


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


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

https://github.com/krallin/tini


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()系统调用来避免僵尸进程产生。

  • 每个Linux 进程在退出的时候都会进入一个僵尸状态(EXIT_ZOMBIE)

  • 僵尸进程如果不清理,就会消耗系统中的进程数资源,最坏的情况是导致新的进程无法启动

  • 僵尸进程一定需要父进程调用 wait() 或者 waitpid()系统调用来清理,这也是容器中 init 进程必须具备的一个功能。

四、评论

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

回答:
C 不会被回收,waitpid 仅等待 children 的状态变化。

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

标签:03,00,容器,0.0,进程,pids,root
来源: https://blog.csdn.net/u012271526/article/details/117261943

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有