容器中的 0 号进程和 1 号进程
发表于|更新于
|字数总计:3.1k|阅读时长:13分钟
驳斥《把数据库放入Docker是一个好主意吗?》埋了一个坑,就是需要讲一讲用了容器技术和不用容器技术,运行一组进程到底有什么区别。
Linux 内核
那就让我回顾一下 Linux 内核的各个子系统吧,我们先从最右边的进程管理子系统说起。今天的话题就是容器中的0 号和 1 号进程。
Linux 0/进程
大家都知道对 Linux 系统来说 1 号进程为 init 进程,是由 0 号进程(内核进程)通过调用系统 init 函数创建的第一个用户进程 1 进程,主要做用户态进程的管理,垃圾回收等动作。
0 号进程
0 号进程,通常也被称为 idle 进程,或者也称为 swapper 进程。0 号进程是 Linux 启动的第一个进程,它的 task_struct 的 comm 字段为 “swapper”,所以也称为 swpper 进程。当系统中所有的进程起来后,0 号进程也就蜕化为 idle 进程,当一个 core 上没有任务可运行时就会去运行 idle 进程。
1 号进程
我们通常将 init 称为 1 号进程,Systemd 是目前使用最广泛的 init 进程,它会作为 1 号进程出现在操作系统中。
2 号进程
2号进程,是由 0 号进程创建的,它是所有内核线程父进程。
问题:那容器中是否存在 0 号进程和 1 号进程呢?它们有什么用?
Docker 的进程管理
Docker 进程管理的基础是 Linux 内核中的 PID 命名空间技术:
在不同 PID 名空间中,进程 ID 是独立的
即在两个不同名空间下的进程可以有相同的 PID。
在 Docker 中,每个 Container 都是 Docker Daemon 的子进程,每个 Container 进程缺省都具有不同的 PID 名空间。通过命名空间技术,Docker 实现容器间的进程隔离。
当创建一个 Docker 容器的时候,就会新建一个PID名空间。容器启动进程在该名空间内 PID 为 1。当 PID 1 进程结束之后,Docker 会销毁对应的 PID 名空间,并向容器内所有其它的子进程发送 SIGKILL。
由于 1 号进程的特殊性,Linux 内核为它做了特殊处理(无法在容器中使用 kill -9 杀死 1 号进程,主机上是可以的)。如果它没有提供某个信号的处理逻辑,那么与其在同一个 PID 名空间下的进程发送给它的该信号都会被屏蔽。这个功能的主要作用是防止 init 进程被误杀。
Docker 提供了两个命令 docker stop 和 docker kill 来向容器中的PID1进程发送信号:
docker stop:docker 会首先向容器的 PID 1 进程发送一个 SIGTERM 信号,用于容器内程序的退出。如果容器在收到 SIGTERM 后没有结束, 那么 Docker Daemon 会在等待一段时间(默认是10s)后,再向容器发送 SIGKILL 信号,将容器杀死变为退出状态。也就是说如果我在 1 号进程实现了 SIGTERM(15) 信号处理,就实现了优雅停止。
docker kill:可以向容器内 PID 1 进程发送任何信号,缺省是发送 SIGKILL 信号来强制退出应用(这里在宿主机上对 1 号进程下发的 kill)。
总结:
进程号 |
进程来源 |
作用 |
0 |
Docker daemon 进程 |
负责容器中进程的管理 |
1 |
可以被 Dockerfile 中的 ENTRYPOINT 或 CMD 指令所指明;也可以被 docker run 命令的启动参数所覆盖 |
负责启动业务进程 |
准备工作
我们准备做一下验证工作
Dockerfile
from CentOS
WORKDIR /home/app
ADD source-test.sh . ADD test.sh . ADD prometheus-test-demo-0.0.1-SNAPSHOT.jar .
EXPOSE 19991 ENTRYPOINT ["/home/app/source-test.sh"]
|
相关脚本
$ cat source-test.sh #!/bin/bash echo "开始执行" path=$(pwd) echo "$path" su - yarn -c /bin/bash -c "$path/test.sh" echo "执行结束" #---------------------------------------------- $ cat test.sh #!/bin/bash echo "Start to run java app!" # 切换用户需要切换目录 cd /home/app path=$(pwd) echo "$path" java -jar -Dserver.port=19991 "$path/prometheus-test-demo-0.0.1-SNAPSHOT.jar" echo "End!"
|
查看进程
容器中查看进程
容器化启动一个进程,然后通过 docker exec 进入到容器中。
ps -elf F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD 4 S root 1 0 0 80 0 - 3708 do_wai 16:39 ? 00:00:00 /bin/bash /home/app/source-test.sh 4 S root 8 1 0 80 0 - 26191 do_wai 16:39 ? 00:00:00 su - yarn -c /bin/bash -c /home/app/test.sh 4 S yarn 9 8 0 80 0 - 3177 do_wai 16:39 ? 00:00:00 /bin/bash /home/app/test.sh 4 S yarn 29 9 99 80 0 - 9806596 futex_ 16:39 ? 00:00:49 java -jar -Dserver.port=19991 /home/app/prometheus-test-demo-0.0.1-SNAPSHOT.jar 4 S root 109 0 1 80 0 - 3741 do_wai 16:40 pts/0 00:00:00 /bin/bash 4 R root 125 109 0 80 0 - 11896 - 16:40 pts/0 00:00:00 ps -elf
|
以下为进程的父子关系(由于镜像中没有安装 pstree,所以就手工描述一下):
0 → 1 → 8 → 9 → 29 → 109 → 125
|
由此我们可以看出:
存在 0 号进程,它是容器中所有进程的祖先
存在 1 号进程,它的父进程是 0 号进程
主机上查看进程
因为 Java 进程监听了一个 19991 端口,所以就比较容易找到它的进程号,从而可以通过 pstree 查看进程关系
ps -ef | grep 19991 ituser 1797120 1797100 14 10:13 ? 00:00:51 java -jar -Dserver.port=19991 /home/app/prometheus-test-demo-0.0.1-SNAPSHOT.jar root 1825278 1794967 0 10:19 pts/0 00:00:00 grep --color=auto 19991 pstree -psla 1797120 systemd,1 --system --deserialize 26 └─containerd-shim,1797064 -namespace moby -id b215bb19322776ce3498c278ce992fdf3d65a086a4ffcbfd0df03a7f8475fd81 -address /run/containerd/containerd.sock └─source-test.sh,1797086 /home/app/source-test.sh └─su,1797099 - yarn -c /bin/bash -c /home/app/test.sh └─test.sh,1797100 /home/app/test.sh └─java,1797120 -jar -Dserver.port=19991 /home/app/prometheus-test-demo-0.0.1-SNAPSHOT.jar pstree -psla 1797064 systemd,1 --system --deserialize 26 └─containerd-shim,1797064 -namespace moby -id b215bb19322776ce3498c278ce992fdf3d65a086a4ffcbfd0df03a7f8475fd81 -address /run/containerd/containerd.sock ├─bash,1804934 ├─source-test.sh,1797086 /home/app/source-test.sh │ └─su,1797099 - yarn -c /bin/bash -c /home/app/test.sh │ └─test.sh,1797100 /home/app/test.sh │ └─java,1797120 -jar -Dserver.port=19991 /home/app/prometheus-test-demo-0.0.1-SNAPSHOT.jar │ ├─{java},1797121 │ ├─{java},1797122 │ .... ├─{containerd-shim},1797065 ├─{containerd-shim},1797066 ├─{containerd-shim},1797067 ├─{containerd-shim},1797068 ├─{containerd-shim},1797069 ├─{containerd-shim},1797070 ├─{containerd-shim},1797071 ├─{containerd-shim},1797072 ├─{containerd-shim},1797073 └─{containerd-shim},1797074 docker,1804892 exec -it b215bb19322776c /bin/bash systemd,1 --system --deserialize 26 └─sshd,1416 -D └─sshd,1804089 └─bash,1804201 └─docker,1804892 exec -it b215bb19322776c /bin/bash
|
我们可以看出,容器中的 0 号进程实际上就是 containerd-shim,它负责容器进程的管理。
containerd-shim,1797064 -namespace moby -id b215bb19322776ce3498c278ce992fdf3d65a086a4ffcbfd0df03a7f8475fd81 -address /run/containerd/containerd.sock
|
另外我们可以看到,通过 docker exec 启动的 bash 进程,它并不是 containerd-shim 的子进程,而是通过某种方式 attach 到了容器进程上(另外讲解)。
Kill 操作
容器内
在容器中执行 kill -9 8 或者 kill -9 9 都会让容器退出。
ps -elf F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD 4 S root 1 0 0 80 0 - 3708 do_wai 16:39 ? 00:00:00 /bin/bash /home/app/source-test.sh 4 S root 8 1 0 80 0 - 26191 do_wai 16:39 ? 00:00:00 su - yarn -c /bin/bash -c /home/app/test.sh 4 S yarn 9 8 0 80 0 - 3177 do_wai 16:39 ? 00:00:00 /bin/bash /home/app/test.sh 4 S yarn 29 9 99 80 0 - 9806596 futex_ 16:39 ? 00:00:49 java -jar -Dserver.port=19991 /home/app/prometheus-test-demo-0.0.1-SNAPSHOT.jar 4 S root 109 0 1 80 0 - 3741 do_wai 16:40 pts/0 00:00:00 /bin/bash 4 R root 125 109 0 80 0 - 11896 - 16:40 pts/0 00:00:00 ps -elf kill -9 8 #直接退出了 [root@1767079587ea app]# [rootps -a | grep Exited | grep "/home/app/source" 1767079587ea cmdexec:v1 "/home/app/source-te…" About a minute ago Exited (0) 16 seconds ago wonderful_cerf #再来一遍 docker exec -it f1d62a60c24 /bin/bash [root@f1d62a60c242 app]# ps -elf F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD 4 S root 1 0 0 80 0 - 3708 do_wai 16:43 ? 00:00:00 /bin/bash /home/app/source-test.sh 4 S root 8 1 0 80 0 - 26191 do_wai 16:43 ? 00:00:00 su - yarn -c /bin/bash -c /home/app/test.sh 4 S yarn 9 8 0 80 0 - 3177 do_wai 16:43 ? 00:00:00 /bin/bash /home/app/test.sh 4 S yarn 29 9 99 80 0 - 9806596 futex_ 16:43 ? 00:00:48 java -jar -Dserver.port=19991 /home/app/prometheus-test-demo-0.0.1-SNAPSHOT.jar 4 S root 109 0 1 80 0 - 3741 do_wai 16:44 pts/0 00:00:00 /bin/bash 4 R root 125 109 0 80 0 - 11896 - 16:44 pts/0 00:00:00 ps -elf kill -9 9 #直接退出了 [root@f1d62a60c242 app]docker ps -a | grep Exited | grep "/home/app/source" f1d62a60c242 cmdexec:v1 "/home/app/source-te…" About a minute ago Exited (0) 6 seconds ago exciting_heisenberg #在主机上杀也一样效果
|
看起来 containerd-shim 进程处理的逻辑就是:
1 号进程主要就是用于启动业务进程(当然自己就是业务进程就最好)
1 号进程结束(正常结束或者被杀),整个容器就会退出,这个符合容器不可变特性(此处需要研究一下 contained shim 进程管理的逻辑)
即使容器中有僵尸进程,容器退出的时候,都会做清理(僵尸进程的 PPID 是 1 号进程,最终会由 containerd-shim 进行清理
容器内进程管理的行为与主机上进程管理的因为略有不同,我们在主机上来验证一下。
主机上
非容器化运行,如果杀死父进程时,子进程没有接收到相关信号(代码中没有指定),则子进程被 1 号进程接管。
systemd,1 --system --deserialize 26 └─sshd,1416 -D └─sshd,753789 └─bash,754008 └─source-test.sh,943606 ./source-test.sh └─su,943608 - mysql -c /bin/bash -c /home/grissom/test.sh └─test.sh,943609 /home/grissom/test.sh └─java,943631 -jar -Dserver.port=19991 /home/grissom/prometheus-test-demo-0.0.1-SNAPSHOT.jar kill -15 943608 #除了 Java 进程以外,其他 shell 进程都被杀死,Java 进程被 systemd 接管 systemd,1 --system --deserialize 26 └─java,943631 -jar -Dserver.port=19991 /home/grissom/prometheus-test-demo-0.0.1-SNAPSHOT.jar Session terminated, killing shell... ...已杀死。 执行结束 #bash 进程情况(shell 没有退出) pstree -p 754008 -sla systemd,1 --system --deserialize 26 └─sshd,1416 -D └─sshd,753789 └─bash,754008 #再来一次 systemd,1 --system --deserialize 26 └─sshd,1416 -D └─sshd,753789 └─bash,754008 └─source-test.sh,966904 ./source-test.sh └─su,966906 - mysql -c /bin/bash -c /home/grissom/test.sh └─test.sh,966907 /home/grissom/test.sh └─java,966935 -jar -Dserver.port=19991 /home/grissom/prometheus-test-demo-0.0.1-SNAPSHOT.jar kill -9 966904 #看起来 kill -9 使得进程没有时间做清理 systemd,1 --system --deserialize 26 └─su,966906 - mysql -c /bin/bash -c /home/grissom/test.sh └─test.sh,966907 /home/grissom/test.sh └─java,966935 -jar -Dserver.port=19991 /home/grissom/prometheus-test-demo-0.0.1-SNAPSHOT.jar
|
写的脚本均没有用 trap 来对信号做处理,我们可以观测到 kill -15 和 kill -9 的差异如下:
总结
这里的 PID 为 7 进程的 PPID 是 0,不是 1。因此杀死 parnt 之后,孤儿进程会被 0 号进程接收。
容器 pid 1 进程的启动也有两种不同,鼓励使用 exec 避免造成了不符合预期的现象(另外讨论)。
容器 exec 产生的进程的父进程是 0 号进程。
容器 pid 1 进程得实现下信号处理,不然无法在同 PID 名空间内向其发送信号退出。
容器 pid 1 进程需要实现下子进程清理,避免出现僵尸进程(因此强烈不建议在容器中动态创建进程!!!)。
参考