容器正常启动后,使用docker exec contaienrID bash
进入容器后,使用ps
命令,一般有两个特殊进程:
- 1号进程 为容器首启动进程,容器的pid namespcae就是由1号进创建的,容内其余进程基本都是首启动进程的子孙进程。只要1号进程挂掉那容器便会关闭,pid namespace会被回收。
- 0号进程 为1号进程的父进程,也为
docker exec....
携带指令的父进程(即从外部向running容器内发起的指令)。当然你要干掉了0号进程,容器一样完蛋,
一个案例带你快速看一眼0号进程与1号进程
1、tail -f /dev/null是容器内的1号进程,而1号进程的父进程则是0号进程
2、docker exec执行的命令是sh,该命令是在容器已经running之后运行的,产生的sh进程的父进程为0号进程
3、在sh环境里执行的新命令如下所示sleep 1000 &,当然都是sh的儿子
| [root@test01 init5] |
| [root@test01 init5] |
| [root@test01 init5] |
| sh-4.2 |
| UID PID PPID C STIME TTY TIME CMD |
| root 1 0 0 04:27 ? 00:00:00 tail -f /dev/null |
| root 6 0 0 04:27 pts/0 00:00:00 sh |
| root 13 6 0 04:27 pts/0 00:00:00 ps -ef |
| sh-4.2 |
| sh-4.2 |
| [1] 14 |
| sh-4.2 |
| UID PID PPID C STIME TTY TIME CMD |
| root 1 0 0 04:27 ? 00:00:00 tail -f /dev/null |
| root 6 0 0 04:27 pts/0 00:00:00 sh |
| root 14 6 0 04:27 pts/0 00:00:00 sleep 1000 |
| root 15 6 0 04:28 pts/0 00:00:00 ps -ef |
| sh-4.2 |
=====>对于linux系统来说:
linux启动的第一个进程是0号进程,是静态创建的
在0号进程启动后会接连创建两个进程,分别是1号进程和2和进程。
1号进程最终会去调用可init可执行文件,init进程最终会去创建所有的应用进程。
2号进程会在内核中负责创建所有的内核线程
所以说0号进程是1号和2号进程的父进程;1号进程是所有用户态进程的父进程;2号进程是所有内核线程的父进程。
===========>对于容器来说:
在容器平台上,无论你是用k8s去删除一个pod,或者用docker关闭一个容器,都会用到Containerd这个服务,
-
在k8s里,创建pod会时,kubelet收到创建pod的请求后,会调用dockerDaemon
发起创建容器请求,然后由containerd
接收请求来创建containerd-shim
进程,然后containerd-shim去调用runc来启动容器
| 真正启动容器是通过 containerd-shim 去调用 runc 来启动容器的,runc 启动完容器后本身会直接退出,containerd-shim 则会成为容器进程的父进程, 负责收集容器进程的状态, 上报给 containerd, 并在容器中 pid 为 1 的进程退出后接管容器中的子进程进行清理, 确保不会出现僵尸进程。containerd,containerd-shim和容器进程(即容器主进程)三个进程,是有依赖关系的 |
- 如果只是用docker创建容器,那就是直接调用
dockerDaemon
发起创建容器请求,然后由containerd
接收请求来创建containerd-shim
进程,然后containerd-shim去调用runc来启动容器
containerd-shim
就是上面提到的0号进程,关于containerd-shim
需要掌握3个关键知识点
-
1、实际的创建容器、容器内执行指令等都其实都是由containerd-shim
进程在做。
-
2、containerd-shim
进程是容器的爹,具备回收僵尸儿子的功能,容器1号进程退出后,内核清理其下子孙进程,这些子孙进程就会被containerd-shim
收养并清理。但如果容器内的1号进程不被Kill,那么其下进程如果有僵尸进程,是无法被处理的。所以用户开发的容器首进程要注意回收退出进程
-
3、Containerd在stop停止容器的时候,会向容器的1号进程发送一个-15信号,如果容器内的1号进程没有信号转发能力,那在回收pid namespce时会向该namespace里的所有其他进程发送SIGKILL信号信号强制杀死。这是有问题的,后续我们将详细介绍
| |
| 当我们使用kill pid时,实际相当于kill -15 pid。也就是说默认信号为15。使用kill -15时,系统会发送一个SIGTERM的信号给对应的程序。当程序接收到该信号后,具体要如何处理自己可以决定。 |
| |
| 这时候,应用程序可以选择: |
| 1、立即停止程序 |
| 2、释放响应资源后停止程序 |
| 3、忽略该信号,继续执行程序 |
| 因为kill -15信号只是通知对应的进程要进行"安全、干净的退出",程序接到信号之后, |
| 退出前一般会进行一些"准备工作",如资源释放、临时文件清理等等,如果准备工作做完了,再进行程序的终止。 |
| |
| 但是,如果在"准备工作"进行过程中,遇到阻塞或者其他问题导致无法成功,那么应用程序可以选择忽略该终止信号。 |
| |
| 这也就是为什么我们有的时候使用kill命令是没办法"杀死"应用的原因, |
| 因为默认的kill信号是SIGTERM(15),而SIGTERM(15)的信号是可以被阻塞和忽略的。 |
| |
| 和kill -15相比,kill -9就相对强硬得多,系统会发出SIGKILL信号, |
| 他要求接收到该信号的程序应该立即结束运行,不能被阻塞或者忽略。 |
| |
| 所以,kill -9在执行时,应用程序是没有时间进行"准备工作"的,所以这通常会带来一些副作用, |
| 数据丢失或者终端无法恢复到正常状态等。 |
启动一个容器
| docker run -d --name test1 centos:7 sh -c "(sleep 10d &) ; tail -f /dev/null" |
如何查看容器内0号进程对应宿主机的pid号
| [root@node1 ~] |
| 09e4114ddd9d |
| |
| |
| [root@node1 ~] |
| root 5439 0.0 0.6 712056 12836 ? Sl 12:27 0:00 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 09e4114ddd9d9747244352637949ade8c61082627984b3381d37a589d92c4bc3 -address /run/containerd/containerd.sock |
| |
| 如果你干死了容器的containerd-shim进程,那站在操作系统角度,容器下的所有进程都被操作系统的init收养然后回收了 |
如何查看容器内1号进程对应物理机的pid号,我们后续会在物理机用strace命令追踪容器内1号进程收到的信号,需要提前知晓下述方式
| [root@node1 ~] |
| 09e4114ddd9d |
| [root@node1 ~] |
| "Pid": 5458, |
| "PidMode": "", |
| "PidsLimit": null, |
如何查看容器内其他进程在宿主机中的PID
| [root@node1 ~] |
| 09e4114ddd9d |
| |
| |
| [root@node1 ~] |
| 5458 |
| 5484 |
| 5485 |
| |
| |
| docker top 容器名/或ID |
| |
| |
| =============》宿主机 |
| [root@node1 ~] |
| root 5458 0.0 0.0 11688 1336 ? Ss 12:27 0:00 sh -c (sleep 10d &) ; tail -f /dev/null |
| [root@node1 ~] |
| root 5484 0.0 0.0 4364 356 ? S 12:27 0:00 sleep 10d |
| [root@node1 ~] |
| root 5485 0.0 0.0 4400 352 ? S 12:27 0:00 tail -f /dev/null |
| |
| =============》在容器内,看一眼与上面的对应关系 |
| 可以执行docker exec -ti test1 sh进入容器内执行ps -ef来查看与上面的的结果是一一对应的 |
| 补充: |
| 由于容器采用了Linux的namespace机制, 对pid进行了隔离. 因此容器内的pid将会从1开始重新编号, 并且不会看到其他容器或宿主机的进程pid。本质上容器就是宿主机上的一个普通的Linux进程, 因此在宿主机中是可以看到容器内进程的pid, 只不过这个pid是在宿主机上显示的, 而非容器内的(因为隔离了) |
| |
| [root@node1 ~] |
| sh-4.2 |
| F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD |
| 4 S root 1 0 0 80 0 - 2922 do_wai 04:27 ? 00:00:00 sh -c (sleep 10d &) ; tail -f /dev/null |
| 0 S root 8 1 0 80 0 - 1091 hrtime 04:27 ? 00:00:00 sleep 10d |
| 0 S root 9 1 0 80 0 - 1100 wait_w 04:27 ? 00:00:00 tail -f /dev/null |
| 4 S root 10 0 0 80 0 - 2956 do_wai 04:35 pts/0 00:00:00 sh |
| 4 R root 16 10 0 80 0 - 12933 - 04:35 pts/0 00:00:00 ps -elf |
小结:
-
每启动一个容器都会在宿主机产生一个docker-shim进程,它就是容器内的0号进程,是容器内1号进程的爹
-
当容器已经running之后,我们exec进入容器里执行命令产生的新进程,都是0号进程的儿子,而不是1号进程的儿子。所以说容器中的1号进程并不会像宿主机的1号进程那样直接或间接地领导所有其它进程;
-
docker top 显示的容器中的进程可能不太全,与是否该进程归属于1号进程没有任何关系;与进程是否最终归属于该容器的管理进程docker-containerd-shim也没有关系,如果是nsenter进入容器,则启动的进程在docker top中是看不到的,虽然该进程在容器中显示的ppid也是0,其实同样是0的ppid却可能不是同一个进程,因为,只要父进程在容器外部,则容器内部显示的ppid就统一为0; 为什么docker top可能看到的不全?docker top是如何实现的呢?参看: https://phpor.net/blog/post/4420
| |
| docker container run -d --name test centos:7 tail -f /dev/null |
| docker top test |
| |
| |
| docker exec -ti test sh |
| |
| |
| |
| nsenter -t 容器的pid --mount --uts --ipc --net --pid |
先不提容器,就以物理机上运行的那个完整的操作系统为例:
一个 Linux 操作系统的启动流程
1、通电后,执行 BIOS
2、找到启动盘
3、bios根据自己的配置,找到启动盘,读取第一个扇区512bytes,即mbr的内容,这里放的前446是boot-loader程序,后64是分区信息,后2字节是结束的标志位
4、 bootloader 负责把磁盘里的内核读入内存执行
Linux 内核执行文件一般会放在 /boot 目录下,文件名类似 vmlinuz*,如下
| [root@yq01-aip-aikefu19 base] |
| vmlinuz-0-rescue-53574fee080a44d49195c9f831019258 |
| vmlinuz-3.10.0-514.el7.x86_64 |
| vmlinuz-4.17.11-1.el7.elrepo.x86_64 |
5、在内核完成了系统的各种初始化之后,这个程序需要执行的第一个用户态程就是 init 进程,PID号为1,该进程是系统中所有其他进程的祖宗,在centos6中该祖宗进程称之为init,在centos7之后该祖宗进程名为systemd。
即:操作系统启动时是先执行内核态代码,然后在内核里调用1号进程的代码,从内核态切换到用户态
| ps:目前linux的好嗯多发行版,如红帽、debian等,都会把/sbin/init作为软连接指向Systemd,Systemd是目前最流行 |
| |
| 的linux init进程,在此之前还有SysVinit、UpStart等linux init进程 |
| |
| 但无论是哪种 Linux init 进程,它最基本的功能都是创建出 Linux 系统中其他所有的进 程,并且管理这些进程** |
| |
| 在 Linux 上有了容器的概念之后,一旦容器建立了自己的 Pid Namespace(进程命名空 间),这个 Namespace 里的进程号也是从 1 开始标记的。所以,容器的 init 进程也被称 为 1 号进程。 |
| |
| 1 号进程是第一个 用户态的进程,由它直接或者间接创建了 Namespace 中的其他进程。 |
总结操作系统的1号进程拥有如下特点
- 1、它是系统的第一个进程,负责产生其他所有用户进程。
- 2、init 以守护进程方式存在,是所有其他进程的祖先。
操作系统的1号进程拥有如下重要功能
然后我们再来说一下容器,容器内的操作系统来自于镜像,而镜像并非一个完整的操作系统(只拥有rootfs),通常docker的镜像为了节省空间,是没有安装systemd或者sysvint这类初始化系统的进程的,所以当启动容器时,我们CMD执行的命令是啥,容器里的1号进程就是啥,
| |
| FROM nginx |
| |
| ENTRYPOINT ["nginx", "-c"] |
| CMD ["/etc/nginx/nginx.conf"] |
容器内的1号进程与操作系统的1号进程的相同点与不同点如下
-
1、相同之处
只要容器里的1号进程停止,容器就会结束,就好比是操作系统的1号进程挂掉操作系统就挂掉了是一个道理。所以容器里的1号进程应该是一个一直运行不会停止的进程,而且必须在在前台运行,总结一下就是:1号进程需要在前台一直运行。
示例1
| |
| |
| echo "123" |
| |
| |
| FROM centos:7 |
| |
| ADD run.sh /opt |
| CMD sh /opt/run.sh |
| |
| |
| docker build -t test:v1 ./ |
| |
| |
| docker container run -d --name test111 test:v1 |
| |
| |
| [root@test01 test] |
| ...... Exited (0) 33 seconds ago ...... |
示例2
| |
| |
| while true;do echo 123 >> /tmp/a.log;sleep 1;done |
| |
| |
| FROM centos:7 |
| |
| ADD run.sh /opt |
| CMD sh /opt/run.sh & |
| |
| docker build -t test:v2 ./ |
| |
| |
| docker container run -d --name test222 test:v2 |
| |
| |
| [root@test01 test] |
| ...... Exited (0) 33 seconds ago ...... |
示范3:
| |
| |
| while true;do echo 123 >> /tmp/a.log;sleep 1;done |
| |
| |
| FROM centos:7 |
| |
| ADD run.sh /opt |
| CMD sh /opt/run.sh |
| |
| |
| docker build -t test:v3 ./ |
| |
| |
| docker container run -d --name test333 test:v3 |
| |
| |
| docker exec -ti test333 sh |
-
2 、不同之处
我们为容器启动的1号进程通常不具备操作系统的init进程一样的功能
- 比如收养孤儿,定期发起wait或waitpid系统调用来回收僵尸儿子
- 再比如信号转发的功能,我们接下来会先介绍回收僵尸儿子,然后再介绍信号转发。
补充说明:
我们建议容器设计原则是一个容器只运行一个进程,但在现实工作中往往做不到,还是会生出一些子进程,
操作系统的init进程是操作系统的开发者开发的,它有一个非常重要的功能就是:会充当孤儿院的作用去回收孤儿进程,并且会定期发起wait或者waitpid的系统调用去回收僵尸的儿子,但是你容器里的1号是你开发的,你摸着自己的良心问问自己有没有实现上面的功能,没有吧,所以你的容器里有僵尸进程而无法被回收也就一点也不奇怪了,那容器里一旦产生僵尸进程该如何应对呢?带着这些疑问往后看吧
在linux系统中,无论是进程还是线程,内核转给你都是用task_struct{}结构体来表示的,称之为任务task,task是linux里基本的调度单位,我们通过ps aux会查看到一系列进程的状态,其实就是task的状态。
进程的状态分为两大类,活着的与死亡的
- 一、活着的
- 1.1 运行着的进程
- (1)、运行态,正占用着cpu资源在运行着,状态为R
- (2)、就绪态,没有申请到cpu资源,处于运行队列中,一旦申请到cpu就可以立即投入运行,状态也为R
- 1.2 睡眠的进程
- (1)、可中断睡眠(TASK_INTERRUPTIBLE),状态为S,等待某个资源而进入的状态,比如等待本地或网络用户输入,也可以等待一个信号量(Semaphore),执行的IO操作可以得到硬件设备的响应
- (2)、不可中断睡眠(TASK_UNINTERRUPTIBLE),状态为D,处于睡眠状态,但是此刻进程是不可中断的,意思是不响应异步信号,执行的IO操作得不到硬件设备的响应(可能是因为硬件繁忙,因此导致内存里的一些缓存数据无法及时刷入磁盘,所以肯定不允许你中断该睡眠状态,并且你会发现处于D状态的进程kill -9竟然也杀不死,就是为了保证数据安全)
- 二、死亡的:即执行do_exit()结束进程
- (1)EXIT_DEAD:也就是进程真正结束退出那一瞬间的状态,通常我们看不到,因为很快就没了
- (2)EXIT_ZOMBIE,这个是进程进入EXIT_DEAD状态前的一个状态,该状态称之为僵尸进程,状态显示为Z,也就是说所有进程在死前都会进入僵尸进程的状态。
强调:我们接下来要讨论的,是僵尸进程的残留问题,而不是僵尸进程的产生问题,所有的进程在死前都会进入僵尸进程的状态,它的父进程负责回收该状态,若没有及时回收,就会残留,至于残留后有何影响、如何回收等问题详解如下
| |
| 操作系统负责管理进程 |
| 我们的应用程序若想开启子进程,都是在向操作系统发送系统调用 |
| 当一个子进程开启起来以后,它的运行与父进程是异步的,彼此互不影响,谁先死都不一定 |
| |
| linux操作系统的设计规定:父进程应该具备随时获取子进程状态的能力 |
| 如果子进程先于父进程运行完毕,此时若linux操作系统立刻把该子进程的所有资源全部释放掉,那么父进程来查看子进程状态时,会突然发现自己刚刚生了一个儿子,但是儿子没了!!! |
| 这就违背了linux操作系统的设计规定 |
| 所以,linux系统出于好心,若子进程先于父进程运行完毕/死掉,那么linux系统在清理子进程的时候,会将子进程占用的重型资源都释放掉(比如占用的内存空间、cpu资源、打开的文件等),但是会保留一部分子进程的关键状态信息,比如进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等,此时子进程就相当于死了但是没死干净,因而得名"僵尸进程",其实僵尸进程是linux操作系统出于好心,为父进程准备的一些子进程的状态数据,专门供父进程查阅,也就是说"僵尸进程"是linux系统的一种数据结构,所有的子进程结束后都会进入僵尸进程的状态 |
| |
| |
| 当然需要回收了,但是僵尸进程毕竟是linux系统出于好心,为父进程准备的数据,至于回收操作,应该是父进程觉得自己无需查看僵尸进程的数据了,父进程觉得留着僵尸进程的数据也没啥用了,然后由父进程发起一个系统调用wait / waitpid来通知linux操作系统说:哥们,谢谢你为我保存着这些僵尸的子进程状态,我现在用不上他了,你可以把他们回收掉了。然后操作系统再清理掉僵尸进程的残余状态,你看,两者配合的非常默契,但是,怕就怕在。。。 |
| |
| |
| 1、linux系统自带的一些优秀的开源软件,这些软件在开启子进程时,父进程内部都会及时调用wait/waitpid来通知操作系统回收僵尸进程,所以,我们通常看不到优秀的开源软件堆积僵尸进程,因为很及时就回收了,与linux系统配合的很默契 |
| |
| 2、一些水平良好的程序员开发的应用程序,这些程序员技术功底深厚,知道父进程要对子进程负责,会在父进程内考虑调用wait/waitpid来通知操作系统回收僵尸进程,但是发起系统调用wait/waitpid的时间可能慢了些,于是我们可以在linux系统中通过命令查看到僵尸进程状态 |
| [root@egon ~] |
| |
| 3、一些垃圾程序员,技术非常垃圾,只知道开子进程,父进程也不结束,就在那傻不拉几地一直开子进程,也压根不知道啥叫僵尸进程,至于wait/waitpid的系统调用更是没听说过,这个时候,就真的垃圾了,操作系统中会堆积很多僵尸进程,此时我们的计算机会进入一个奇怪的现象,就是内存充足、硬盘充足、cpu空闲,但是,启动新的软件就是无法启动起来,为啥,因为操作系统负责管理进程,每启动一个进程就会分配一个pid号,而pid号是有限的,正常情况下pid也用不完,但怕就怕堆积一堆僵尸进程,他吃不了多少内存,但能吃一堆pid |
| |
| |
| 针对情况3,只有一种解决方案,就是杀死父进程,那么僵尸的子进程会被linux系统中pid为1的顶级进程(init或systemd)接管,顶级进程很靠谱、它是一定会定期发起系统调用wait/waitpid来通知操作系统清理僵尸儿子的 |
| |
| 针对情况2,可以发送信号给父进程,通知它快点发起系统调用wait/waitpid来清理僵尸的儿子 |
| kill -CHLD 父进程PID |
| |
| |
| 僵尸进程是linux系统出于好心设计的一种数据结构,一个子进程死掉后,相当于操作系统出于好心帮它的爸爸保存它的遗体,之说以会在某种场景下有害,是因为它的爸爸不靠谱,儿子死了,也不及时收尸(发起系统调用让操作系统收尸) |
| |
| 说白了,僵尸进程本身无害,有害的是那些水平不足的程序员,他们总是喜欢写bug,好吧,如果你想看看垃圾程序员是如何写bug来堆积僵尸进程的,你可以看一下这篇博客https://www.cnblogs.com/linhaifeng/articles/13567273.html |
再次强调:父进程发起的wait或waitpid调用只能回收僵尸儿子,无法回收孙子辈,只能是儿子,只能是儿子,只能是儿子
再次强调:父进程发起的wait或waitpid调用只能回收僵尸儿子,无法回收孙子辈,只能是儿子,只能是儿子,只能是儿子
再次强调:父进程发起的wait或waitpid调用只能回收僵尸儿子,无法回收孙子辈,只能是儿子,只能是儿子,只能是儿子
再次强调:父进程发起的wait或waitpid调用只能回收僵尸儿子,无法回收孙子辈,只能是儿子,只能是儿子,只能是儿子
再次强调:父进程发起的wait或waitpid调用只能回收僵尸儿子,无法回收孙子辈,只能是儿子,只能是儿子,只能是儿子
先储备一个知识,看如下代码
| |
| from multiprocessing import Process |
| import os |
| import time |
| |
| def task(n): |
| print("father--->%s son--->%s" %(os.getppid(),os.getpid())) |
| time.sleep(n) |
| |
| if __name__ == "__main__": |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| for i in range(1000): |
| Process(target=task,args=(0,)).start() |
| |
| time.sleep(100000) |
上述代码你去执行一下,只会看到有几个僵尸进程残留。
综上,我们的用来产生僵尸进程的代码示范如下,先让子进程运行一小会不要立刻结束(否则立即产生的僵尸进程,极有可能会被正在跑着的主进程回收了),然后等所有儿子都开启了之后,主进程执行time.sleep(100000)后彻底停住、不会执行任何定期回收僵尸儿子的任务了。此时就可以静静等着撕掉的儿子,每死一个就产生一个僵尸
| =====================窗口1中====================== |
| [root@egon ~] |
| |
| from multiprocessing import Process |
| import os |
| import time |
| |
| def task(n): |
| print("father--->%s son--->%s" %(os.getppid(),os.getpid())) |
| time.sleep(n) |
| |
| if __name__ == "__main__": |
| p1=Process(target=task,args=(10,)) |
| p2=Process(target=task,args=(10,)) |
| p3=Process(target=task,args=(10,)) |
| p1.start() |
| p2.start() |
| p3.start() |
| print("main--->%s" %os.getpid()) |
| time.sleep(10000) |
| [root@egon ~] |
| [5] 104481 |
| [root@egon ~] |
| father--->104481 son--->104482 |
| father--->104481 son--->104483 |
| father--->104481 son--->104484 |
| |
| =====================窗口2中:大概过个十几秒后查看====================== |
| [root@egon ~] |
| USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND |
| root 104482 0.0 0.0 0 0 pts/2 Z 18:24 0:00 [python] <defunct> |
| root 104483 0.0 0.0 0 0 pts/2 Z 18:24 0:00 [python] <defunct> |
| root 104484 0.0 0.0 0 0 pts/2 Z 18:24 0:00 [python] <defunct> |
| root 104488 0.0 0.0 112828 960 pts/4 R+ 18:24 0:00 grep --color=auto Z |
| |
| =====================如下,我们可以看到僵尸对应的资源都已经没有了====================== |
| |
| |
| |
| |
| |
| 任何进程的退出都是调用do_exit()系统接口,do_exit()内部会释放进程task_struct里的mm/shm/sem/files等文件资源,只留下一个stask_struct instance空壳,如上所示 |
| |
| 并且,这个进程也已经不响应任何的信号了,无论 SIGTERM(15) 还是 SIGKILL(9) |
| kill -9 104482 |
| kill -15 104482 |
| ps aux |grep Z |
| |
| |
| 可以去查看p1.join()的代码,里面有一个关于wait的调用,在主进程里调用p1.join()的目的就是等子进程挂掉后 |
| 而回收它的尸体,所以python代码多进程编程,在主进程里建议在主进程里一个个join主子进程。 |
| 而上例中,主进程在进入sleep前,我们并没有调用join方法,于是僵尸进程就产生了,因为主进程一直停在原地,并没有发起wait系统调用的机会 |
说在最前面:孤儿进程指的是一个进程的父进程死掉了,它就成了孤儿,孤儿会被1号进程收养,而不是被它爷爷进程、或者太爷爷进程收养,这一点很关键
| 父进程先死掉,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被进程号为1的顶级进程(init或systemd)所收养,并由顶级进程对它们完成状态收集工作。 |
| 此处需要强调一句:不管子进程时什么状态,哪怕它是僵尸状态也一样,只要它的父进程挂掉了,它就会被1号进程收养。 |
| |
| 进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为顶级进程,而顶级进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,顶级进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。 |
| |
| 我们来测试一下(创建完子进程后,主进程所在的这个脚本就退出了,当父进程先于子进程结束时,子进程会被顶级进程收养,成为孤儿进程,而非僵尸进程),文件内容 |
| |
| import os |
| import sys |
| import time |
| |
| pid = os.getpid() |
| ppid = os.getppid() |
| print 'im father', 'pid', pid, 'ppid', ppid |
| pid = os.fork() |
| |
| |
| |
| |
| if pid > 0: |
| print 'father died..' |
| sys.exit(0) |
| |
| |
| time.sleep(1) |
| print 'im child', os.getpid(), os.getppid() |
| |
| 执行文件,输出结果: |
| im father pid 32515 ppid 32015 |
| father died.. |
| im child 32516 1 |
| |
| 看,子进程已经被pid为1的顶级进程接收了,所以僵尸进程在这种情况下是不存在的,存在只有孤儿进程而已,孤儿进程声明周期结束自然会被顶级进程来销毁。 |
最后我们来做一个小练习,把上面关于僵尸进程与孤儿进程的知识点串一下
在做练习前需要再次强调:我们说一个完整操作系统自带的init进程是会定期发起wait或waitpid系统调用来回收僵尸儿子的,这里需要强调的是
示例代码
| |
| from multiprocessing import Process |
| import os |
| import time |
| |
| def task1(n): |
| print("儿子,PID:%s PPID:%s" %(os.getpid(),os.getppid())) |
| pp1=Process(target=task2,args=(10,)) |
| pp2=Process(target=task2,args=(10,)) |
| pp3=Process(target=task2,args=(10,)) |
| pp1.start() |
| pp2.start() |
| pp3.start() |
| time.sleep(n) |
| |
| def task2(n): |
| print("孙子,PID:%s PPID:%s" %(os.getpid(),os.getppid())) |
| time.sleep(n) |
| |
| if __name__ == "__main__": |
| p=Process(target=task1,args=(10000,)) |
| p.start() |
| |
| print("爸爸,PID: %s" %os.getpid()) |
| time.sleep(10000) |
实验
| [root@test01 test2] |
| 63047 |
| |
| [root@test01 test2] |
| 爸爸,PID: 42801 |
| 儿子,PID:42802 PPID:42801 |
| 孙子,PID:42803 PPID:42802 |
| 孙子,PID:42804 PPID:42802 |
| 孙子,PID:42805 PPID:42802 |
| |
| |
| [root@test01 test2] |
| 0 S root 42801 63047 0 80 0 - 37800 poll_s 15:17 pts/1 00:00:00 python test.py |
| 1 S root 42802 42801 0 80 0 - 37800 poll_s 15:17 pts/1 00:00:00 python test.py |
| 1 Z root 42803 42802 0 80 0 - 0 do_exi 15:17 pts/1 00:00:00 [python] <defunct> |
| 1 Z root 42804 42802 0 80 0 - 0 do_exi 15:17 pts/1 00:00:00 [python] <defunct> |
| 1 Z root 42805 42802 0 80 0 - 0 do_exi 15:17 pts/1 00:00:00 [python] <defunct> |
| 0 S root 44990 6509 0 80 0 - 28182 pipe_w 15:21 pts/0 00:00:00 grep --color=auto python |
| |
| |
| 父进程 子进程 孙子进程 曾孙子进程 |
| 进程即bash,pid为63047--------》42801----》42802----》42803、42804、42805 |
| |
| |
| |
| |
| kill -9 42803 |
| kill -9 42804 |
| kill -9 42805 |
| |
| |
| [root@test01 test2] |
| 0 S root 42801 63047 0 80 0 - 37800 poll_s 15:17 pts/1 00:00:00 python test.py |
| 1 Z root 42802 42801 0 80 0 - 0 do_exi 15:17 pts/1 00:00:00 [python] <defunct> |
| |
| |
| [root@test01 test2] |
| [root@test01 test2] |
| 0 S root 50846 6509 0 80 0 - 28182 pipe_w 15:34 pts/0 00:00:00 grep --color=auto python |
linux进程管理更多内容:详见https://egonlin.com/?p=210
再次重申一个重点:
操作系统内有1号进程,容器内也有一个1号进程
区别是操作系统内的1号进程里的代码是别人开发的,开发者为其加入了两个个重要的功能
- 当进程称为孤儿进程后,1号进程会收养该孤儿、称为他的爹
- 定义发起wait或waitpid的系统调用去1号进程的僵尸儿子。
而容器里的1号进程的代码是你开发的,你并没有考虑发起wait与waitpid这个系统调用的操作。于是
在容器运行一段时间后,如果有子进程先挂掉了,它爹又没有负责回收,那么僵尸进程的状态就残留了下来,pid资源就被白白占住了
有人会问,那如果我关掉容器,或者删掉k8s里的pod,容器里的僵尸进程还会残留吗,答案是肯定不会残留,为什么呢?
| 在容器平台上,无论你是用k8s去删除一个pod,或者用docker关闭一个容器,都会用到Containerd这个服务 |
| |
| 1、创建容器时:kubelet调用`dockerDaemon`发起创建容器请求,然后由`containerd`接收并创建`containerd-shim`,`containerd-shim`即容器内的0号进程。所以实际的创建容器、容器内执行指令等都是此进程在做 |
| |
| 2、同时,`containerd-shim`具有回收僵尸进程的功能,容器1号进程退出后,内核清理其下子孙进程,这些子孙进程被`containerd-shim`收养并清理。 注意:如果1号进程不被Kill,那么其下进程如果有僵尸进程,是无法被处理的。所以用户开发的容器首进程要注意回收退出进程。 |
| |
| ps: 在所有容器都清理后,k8s中的pod也就被删除了。 |
所以说,你要知道的是,即便容器内的1号进程没有回收僵尸儿子的能力,0号进程是为其兜底的。
而我们接下来要讨论的不是容器内1号进程挂掉的情况,而是要讨论在1号进程活着的情况下,它若没有回收僵尸进程的能力容器内会产生何种现象,应该如何处理。听懂了没有,不是粗鲁地直接干掉容器来回收一切,这操作谁都会。
我们用python解释器作为1号进程来测试(强调,python解释器与bash解释一样都具备定期回收僵尸儿子的功能,但是我们再次用sleep将python父进程停住,它不动弹了也就不会发起回收了,我们也就能看到僵尸儿子了)
| |
| mkdir /test |
| cd /test |
| |
| |
| cat >> test.py << EOF |
| #coding:utf-8 |
| from multiprocessing import Process |
| import os |
| import time |
| |
| def task1(n): |
| print("儿子,PID:%s PPID:%s" %(os.getpid(),os.getppid())) |
| pp1=Process(target=task2,args=(10,)) |
| pp2=Process(target=task2,args=(10,)) |
| pp3=Process(target=task2,args=(10,)) |
| pp1.start() |
| pp2.start() |
| pp3.start() |
| time.sleep(n) |
| |
| def task2(n): |
| print("孙子,PID:%s PPID:%s" %(os.getpid(),os.getppid())) |
| time.sleep(n) |
| |
| if __name__ == "__main__": |
| p=Process(target=task1,args=(10000,)) |
| p.start() |
| |
| print("爸爸,PID: %s" %os.getpid()) |
| time.sleep(10000) |
| |
| EOF |
| |
| |
| cat > dockerfile << EOF |
| FROM centos:7 |
| |
| ADD test.py /opt |
| CMD python /opt/test.py |
| |
| EOF |
| |
| |
| docker build -t test_defunct:v1 ./ |
| docker run -d --name test1 test_defunct:v1 |
| |
| |
| [root@test01 test] |
| sh-4.2 |
| UID PID PPID C STIME TTY TIME CMD |
| root 1 0 0 07:50 ? 00:00:00 python /opt/test.py |
| root 7 1 0 07:50 ? 00:00:00 python /opt/test.py |
| root 8 7 0 07:50 ? 00:00:00 [python] <defunct> |
| root 9 7 0 07:50 ? 00:00:00 [python] <defunct> |
| root 10 7 0 07:50 ? 00:00:00 [python] <defunct> |
| root 11 0 0 07:50 pts/0 00:00:00 sh |
| root 18 11 0 07:51 pts/0 00:00:00 ps -ef |
| |
| 此时你是无法杀死那三个僵尸的,那我们杀死他们的爹,即pid为7的进程 |
| sh-4.2 |
| sh-4.2 |
| UID PID PPID C STIME TTY TIME CMD |
| root 1 0 0 07:50 ? 00:00:00 python /opt/test.py |
| root 7 1 0 07:50 ? 00:00:00 [python] <defunct> |
| root 8 1 0 07:50 ? 00:00:00 [python] <defunct> |
| root 9 1 0 07:50 ? 00:00:00 [python] <defunct> |
| root 10 1 0 07:50 ? 00:00:00 [python] <defunct> |
| root 11 0 0 07:50 pts/0 00:00:00 sh |
| root 19 11 0 07:52 pts/0 00:00:00 ps -ef |
| |
| 此时你会发现两个非常有趣的现象 |
| 1、又多一个僵尸进程 |
| 2、pid为7、8、9、10的进程的ppid都变成了1,也就是说他们都被1号进程收养了 |
| |
| 很简单,当我们杀死pid为7的进程的时候,它爹也就是1号进程在原地sleep呢,并不会回收它,所以它停留在僵尸状态 |
| 而pid为7的进程一旦撕掉,它的三个僵尸儿子们就成了孤儿,自然会被pid为1的进程收养,再说一句,这里的pid为1的进程被sleep住了,肯定 |
| 不会 回收他们,于是它们三也残留着,这就是你看到的4个僵尸进程 |
| |
| 最后,不能再杀了,而且你kill -9 1也是无效的,1号进程是无法被强制杀死的,这一点我们后面再说。 |