docker进程管理(1号进程,僵尸进程详解)

前沿

大家都知道对linux系统来说1号进程为init进程,是由0号进程(内核进程)通过调用系统init函数创建的第一个用户进程1进程,主要做用户态进程的管理,垃圾回收等动作。
对docker来讲1号进程大多数情况下都是服务进程,或者是用户自己开发的服务daemon进程,这也是瘦容器的理论,那服务进程作为1号进程有什么区别呢?
本文详细讲述1号进程的区别,如何规避僵尸进程等

docker的进程管理

docker进程管理的基础是LINUX内核中的PID命名空间技术,在不同PID名空间中,进程ID是独立的;即在两个不同名空间下的进程可以有相同的PID。
在Docker中,每个Container都是Docker Daemon的子进程,每个Container进程缺省都具有不同的PID名空间。通过名空间技术,Docker实现容器间的进程隔离。
当创建一个Docker容器的时候,就会新建一个PID名空间。容器启动进程在该名空间内PID为1。当PID1进程结束之后,Docker会销毁对应的PID名空间,并向容器内所有其它的子进程发送SIGKILL。

下面通过例子具体看docker进程情况:
容器内执行ps -ef,可以看到1号进程为服务daemon进程redisadmin及其子进程redis-server

[root@localhost ~]# docker exec 61570e71c903 ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:01 python3 /usr/local/bin/redisadmin -d startup -P 6379 -f
hitv      4398     1  0 07:48 ?        00:00:00 redis-server *:6379

在宿主机上通过docker top container_id可以看到容器内进程,从下面可以卡看到1号进程redisadmin在宿主机上父进程为docker daemon

[root@localhost ~]# docker top 61570e71c903
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
2000                436                 32147               0                   07:48               ?                   00:00:00            redis-server *:6379
2000                32147               32130               0                   06:50               pts/4               00:00:01            python3 /usr/local/bin/redisadmin -d startup -P 6379 -f
  • 知识1:通过exec执行的docker命令,父进程为0号进程,也就是docker daemon进程,容器内的0号进程,不是1号进程。 但是由exec启动的进程属于容器的namespace和相应的cgroup
   [root@localhost ~]# docker exec -ti 61570e71c903 bash   用bash进入容器
   [hitv@localhost data]$ ps -ef
   UID        PID  PPID  C STIME TTY          TIME CMD
   hitv         1     0  0 06:50 ?        00:00:01 python3 /usr/local/bin/redisadmi
   hitv      4398     1  0 07:48 ?        00:00:00 redis-server *:6379
   hitv      5056     0  0 07:57 ?        00:00:00 bash   发现bash的父进程为0号
   [hitv@localhost data]$ exit
   [root@localhost ~]# docker exec 61570e71c903 sleep 1000&  用exec到docker里后台执行sleep
   [root@localhost ~]# docker exec 61570e71c903 ps -ef
   UID        PID  PPID  C STIME TTY          TIME CMD
   hitv         1     0  0 06:50 ?        00:00:01 python3 /usr/local/bin/redisadmin -d startup -P 6379 -f
   hitv      4398     1  0 07:48 ?        00:00:00 redis-server *:6379
   hitv      5125     0  0 07:58 ?        00:00:00 sleep 1000    发现sleep进程的父进程也是0号

上面的这个结论在下面将僵尸进程时会用到,请留意

  • 知识点2:由于PID1进程的特殊性,Linux内核为他做了特殊处理。如果它没有提供某个信号的处理逻辑,那么与其在同一个PID名空间下的进程发送给它的该信号都会被屏蔽。这个功能的主要作用是防止init进程被误杀
    [root@localhost ~]# docker exec 61570e71c903 kill -9 1
    [root@localhost ~]# docker exec 61570e71c903 ps -ef
    UID        PID  PPID  C STIME TTY          TIME CMD
    hitv         1     0  0 06:50 ?        00:00:01 python3 /usr/local/bin/redisadmin -d startup -P 6379 -f
    hitv      4398     1  0 07:48 ?        00:00:00 redis-server *:6379
    hitv      5125     0  0 07:58 ?        00:00:00 sleep 1000
    hitv      5545     0  0 08:04 ?        00:00:00 ps -ef

上面可以看到执行kill -9是没用的,是杀不掉1号进程的,验证了上面的知识点2
如果要想能在容器内执行kill干掉1号进程,需要在1号进程中实现信号接收处理,比如收到kill -15执行exit操作,python例子如下
    def sigterm_handler(sig, frame):
        logging.info("get term signal({0})".format(sig))
        if sig == 15:
            logging.info("get kill -15, exit")
            sys.exit(1)

    signal.signal(signal.SIGTERM, sigterm_handler)
  • 附加知识点: 自从Docker 1.5之后,docker run命令引入了–pid=host参数来支持使用宿主机PID名空间来启动容器进程,这样可以方便的实现容器内应用和宿主机应用之间的交互:比如利用容器中的工具监控和调试宿主机进程。

指定docker的1号进程

可以被Dockerfile中的ENTRYPOINT或CMD指令所指明;也可以被docker run命令的启动参数所覆盖
这里主要描述在ENTRYPOINT和CMD指令中,提供两种不同的进程执行方式 shell 和 exec

  • shell 方式 CMD executable param1 param2
    CMD redisadmin -d startup -P 6379 -f
注意:如果redisadmin是shell脚本,则启动方式为/bin/sh -c ”redisadmin -d startup -P 6379 -f”,这样1号进程就为/bin/sh,1号进程中拉起的redisadmin -d startup -P 6379 -f
但是我的redisadmin为python的,首行写的#!/usr/bin/env python3,所以从上面例子可以看到1号进程为python3 /usr/local/bin/redisadmin -d startup -P 6379 -f
这样实际上跟exec方式一样了。
  • exec 方式 CMD [“executable”,“param1”,“param2”] 这种方式跟run命令的启动参数覆盖的1号进程一样,写的什么什么就是1号进程
  • 这两种方式的具体不同在哪里?
    • 这里就用到了进程管理那部分的知识,PID1进程对于操作系统而言具有特殊意义。操作系统的PID1进程是init进程,以守护进程方式运行,是所有其他进程的祖先,具有完整的进程生命周期管理能力。在Docker容器中,PID1进程是启动进程,它也会负责容器内部进程管理的工作。而这也将导致进程管理在Docker容器内部和完整操作系统上的不同。
    • 不同1: 到底谁负责进程管理,比如我redisadmin为1号,我在里面写了很多子进程处理的代码,但是shell方式他的1号进程是bash,就会导致无法做处理了
    • 不同2:Docker提供了两个命令docker stop和docker kill来向容器中的PID1进程发送信号
      • 当执行docker stop命令时,docker会首先向容器的PID1进程发送一个SIGTERM信号,用于容器内程序的退出。如果容器在收到SIGTERM后没有结束, 那么Docker Daemon会在等待一段时间(默认是10s)后,再向容器发送SIGKILL信号,将容器杀死变为退出状态。
        • 也就是说如果我在1号进程实现了SIGTERM(15)信号处理,比如不是上面那样简单的退出自己,而是先优雅的对redis执行shutdown(redis做bgsave保证数据不丢失),然后再sys.exit(1),这样就实现了容器优雅stop
      • 而docker kill可以向容器内PID1进程发送任何信号,缺省是发送SIGKILL信号来强制退出应用,当然这里并不是用exec执行的,上面提到过exec是干不掉1号进程的,这里在宿主机上对1号进程下发的kill

僵尸进程

当一个子进程终止后,它首先会变成一个“失效(defunct)”的进程,也称为“僵尸(zombie)”进程,等待父进程或系统收回(reap)。在Linux内核中维护了关于“僵尸”进程的一组信息(PID,终止状态,资源使用信息),从而允许父进程能够获取有关子进程的信息。如果不能正确回收“僵尸”进程,那么他们的进程描述符仍然保存在系统中,系统资源会缓慢泄露。
僵尸进程:终止的进程但是因为父进程没有垃圾回收功能导致的进程,跟孤儿进程的区别是孤儿进程知识父进程退出了,但是自己还未终止
孤儿进程:子进程未退出,但是父进程退出了,这种进程会变为孤儿进程,在Linux中Init进程(PID1)作为所有进程的父进程,会维护进程树的状态,一旦有某个子进程成为了“孤儿”进程后,init就会负责接管这个子进程。当一个子进程成为“僵尸”进程之后,如果其父进程已经结束,init会收割这些“僵尸”,释放PID资源。
下面主要讲解docker中1号进程为服务进程不是init进程时是什么表现,该怎么处理僵尸进程

情况1: exec启动的进程

[root@localhost ~]# docker exec 61570e71c903 ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:02 python3 /usr/local/bin/redisadmin -d startup -P 6379 -f
hitv      4398     1  0 07:48 ?        00:00:03 redis-server *:6379
[root@localhost ~]# docker exec 61570e71c903 sleep 1000 &
[root@localhost ~]# docker exec 61570e71c903 ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:02 python3 /usr/local/bin/redisadmin -d startup -P 6379 -f
hitv      4398     1  0 07:48 ?        00:00:03 redis-server *:6379
hitv      8557     0  3 08:45 ?        00:00:00 sleep 1000
[root@localhost ~]# docker exec 61570e71c903 kill -9 8557
[1]+  Exit 137                docker exec 61570e71c903 sleep 1000
[root@localhost ~]# docker exec 61570e71c903 ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:02 python3 /usr/local/bin/redisadmin -d startup -P 6379 -f
hitv      4398     1  0 07:48 ?        00:00:03 redis-server *:6379

由进程管理那部分我们得出的结论是exec执行的进程其父进程是0号进程也就是docker daemon,docker daemon进程有垃圾回收,所以不会产生僵尸进程

情况2: bash启动的进程,bash进程不退出

窗口1

[root@localhost ~]# docker exec -ti 61570e71c903 bash   进入容器
[hitv@localhost data]$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:02 python3 /usr/local/bin/redisadmin -d startup -P 6379 -f
hitv      4398     1  0 07:48 ?        00:00:03 redis-server *:6379
hitv      8785     0  2 08:48 ?        00:00:00 bash   这个就为进入容器的bash进程
[hitv@localhost data]$ sleep 1000&
[1] 8824
[hitv@localhost data]$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:02 python3 /usr/local/bin/redisadmin -d startup -P 6379 -f
hitv      4398     1  0 07:48 ?        00:00:03 redis-server *:6379
hitv      8785     0  0 08:48 ?        00:00:00 bash
hitv      8824  8785  0 08:48 ?        00:00:00 sleep 1000  启动sleep进程 发现父进程为bash进程

窗口2

[root@localhost ~]# docker exec -ti 61570e71c903 bash  进入容器
[hitv@localhost data]$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:02 python3 /usr/local/bin/redisadmi
hitv      4398     1  0 07:48 ?        00:00:03 redis-server *:6379
hitv      8785     0  0 08:48 ?        00:00:00 bash    窗口1的bash进程
hitv      8824  8785  0 08:48 ?        00:00:00 sleep 1000  窗口1bash拉起的sleep进程
hitv      8874     0  1 08:49 ?        00:00:00 bash  自己窗口的bash进程
[hitv@localhost data]$ kill -9 8824  kill掉sleep进程
[hitv@localhost data]$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:02 python3 /usr/local/bin/redisadmi
hitv      4398     1  0 07:48 ?        00:00:03 redis-server *:6379
hitv      8785     0  0 08:48 ?        00:00:00 bash
hitv      8874     0  0 08:49 ?        00:00:00 bash
hitv      8907  8874  0 08:49 ?        00:00:00 ps -ef

从窗口2kill掉sleep进程,发现sleep进程被彻底清理了,这是因为sleep的父进程窗口1的bash进程是有子进程垃圾回收机制的

情况3:bash启动的进程,bash进程退出

窗口1

[hitv@localhost data]$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:03 python3 /usr/local/bin/redisadmin -d startup -P 6379 -f
hitv      4398     1  0 07:48 ?        00:00:04 redis-server *:6379
hitv      8785     0  0 08:48 ?        00:00:00 bash  窗口1bash
hitv      8874     0  0 08:49 ?        00:00:00 bash  窗口2bash
[hitv@localhost data]$ sleep 1000&   窗口1启动sleep进程
[hitv@localhost data]$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:03 python3 /usr/local/bin/redisadmin -d startup -P 6379 -f
hitv      4398     1  0 07:48 ?        00:00:04 redis-server *:6379
hitv      8785     0  0 08:48 ?        00:00:00 bash
hitv      8874     0  0 08:49 ?        00:00:00 bash
hitv      9342  8785  0 08:55 ?        00:00:00 sleep 1000 父进程为窗口1bash

窗口2

[hitv@localhost data]$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:03 python3 /usr/local/bin/redisadmi
hitv      4398     1  0 07:48 ?        00:00:04 redis-server *:6379
hitv      8785     0  0 08:48 ?        00:00:00 bash   窗口1bash
hitv      8874     0  0 08:49 ?        00:00:00 bash   窗口2bash
hitv      9342  8785  0 08:55 ?        00:00:00 sleep 1000   窗口1bash启动的sleep
[hitv@localhost data]$ kill -9 8785   kill掉窗口1bash 让sleep1000成为孤儿进程
[hitv@localhost data]$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:03 python3 /usr/local/bin/redisadmi
hitv      4398     1  0 07:48 ?        00:00:04 redis-server *:6379
hitv      8874     0  0 08:49 ?        00:00:00 bash
hitv      9342     0  0 08:55 ?        00:00:00 sleep 1000  非常震惊的发现孤儿进程被0号进程接管而不是1号进程
hitv      9435  8874  0 08:56 ?        00:00:00 ps -ef 
[hitv@localhost data]$ kill -9 9342   kill掉孤儿进程
[hitv@localhost data]$ ps -ef    发现没有成为僵尸,因为0号进程docker daemon把终止的sleep进程回收了
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:03 python3 /usr/local/bin/redisadmi
hitv      4398     1  0 07:48 ?        00:00:04 redis-server *:6379
hitv      8874     0  0 08:49 ?        00:00:00 bash
hitv      9526  8874  0 08:57 ?        00:00:00 ps -ef

这里的疑问是为什么孤儿进程sleep没被1号进程接管,而是被0号进程接管了呢?
按照宿主机的理论这里应该要被1号进程接管才对,但是docker表现的是被0号接管了,通过查询资料发现:
Docker1.11版本之前孤儿进程是由容器内pid为1的进程接收,而1.11版本后是由docker-containerd-shim进程接收,docker-containerd-shim进程时有进程管理功能的,所以这时候kill掉sleep也不会出现僵尸进程
参考自:https://blog.csdn.net/liukuan73/article/details/78043928

由上面理论可见docker中产生僵尸进程的唯一一个点就是1号进程拉起的进程,在1号进程没退出且没有子进程管理时的场景
所以1号进程如果开辟多进程一定要有进程管理功能,下面继续做此验证

情况4:1号进程拉起的子进程,子进程退出的场景

[hitv@localhost data]$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:03 python3 /usr/local/bin/redisadmi
hitv      4398     1  0 07:48 ?        00:00:04 redis-server *:6379
hitv      8785     0  0 08:48 ?        00:00:00 bash   
[hitv@localhost data]$ kill -9 4398   kill掉redis进程,等待父进程回收
[hitv@localhost data]$ ps -ef    
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:03 python3 /usr/local/bin/redisadmi
hitv      4398     1  0 07:48 ?        00:00:04 [redis-server *:6379] <defunct>
hitv      8874     0  0 08:49 ?        00:00:00 bash

在1号进程没有没有进程管理时,发现redis进程果然成为了僵尸进程,这是docker出现僵尸进程最常见的场景,所以1号进程一定要有进程管理,下面介绍如何添加子进程清理

1号进程添加僵尸进程清理

python为例

def wait_child(sig, frame):
    try:
        while True:
            #收到信号就检查一下有没有子进程要处理, 其中os.WNOHANG表示不阻塞,就类似于wait()只是不阻塞
            child_pid, status = os.waitpid(-1, os.WNOHANG) 
            if child_pid == 0:
                logging.debug('No child process need to wait')
                break
            exitcode = status >> 8
            logging.debug('child process {0} exit, exitcode {1}'.format(child_pid, exitcode))
    except OSError as e:
        # 当没有要处理的子进程时会进入这里
        if e.errno == errno.ECHILD: 
            logging.debug("No child processes")
        else:
            logging.info("wait_child error: {0}".format(e))


signal.signal(signal.SIGCHLD, wait_child)  子进程退出时都会向主进程发送SIGCHLD信号,这里捕获这个信号给wait_child()处理

测试

[hitv@localhost data]$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:04 python3 /usr/local/bin/redisadmi
hitv      4398     1  0 07:48 ?        00:00:07 redis-server *:6379
hitv      8874     0  0 08:49 ?        00:00:00 bash
[hitv@localhost data]$ kill -9 4398
[hitv@localhost data]$ ps -ef   不再僵尸了
UID        PID  PPID  C STIME TTY          TIME CMD
hitv         1     0  0 06:50 ?        00:00:04 python3 /usr/local/bin/redisadmi
hitv      8874     0  0 08:49 ?        00:00:00 bash

总结

  • docker pid1与宿主机pid1有所不同
  • Docker1.11版本之前孤儿进程是由容器内pid为1的进程接收,而1.11版本后是由docker-containerd-shim进程接收,可以减少因1号进程没有子进程处理导致的僵尸进程
  • docker pid1进程的启动也有两种不同,鼓励使用exec避免造成了不符合预期的现象
  • docker exec产生的进程父进程是0号
  • docker pid1进程得实现下信号处理,不然无法在同PID名空间内向其发送信号退出
  • docker pid1进程得实现下子进程清理,避免出现僵尸进程

参考

https://www.cnblogs.com/ilinuxer/p/6188303.html
https://blog.csdn.net/liukuan73/article/details/78043928

  • 4
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值