进程是操作系统基础的调度单位,我们日常接触了很多,自然不必多说。但有时,一个进程的状态变成了 Z,我们杀不死它,它持有的资源我们也不能回收,这显然是一个棘手的问题。
那么,进程究竟有哪些状态?Z 状态又意味着什么?我们怎么去避免这样的情况发生?这就是本文将要讲述的重点。
在 linux 系统中,进程共有如下六种状态:
有时,我们会看到进程状态码的后面紧跟着一位,这一位就是额外的状态码标识,说明了更多的状态信息:
在 linux 系统中,进程都是由父进程创建的,当父进程执行 fork 系统调用完成子进程创建后,子进程和父进程就独立存在了,但两者又有着密切的关系,按照标准的流程,父进程要在子进程完成执行后,调用 wait 或 waitpid 系统调用来为子进程回收系统资源(包括进程 id、进程退出状态、进程运行时间)。
这样一来,父进程在子进程的完整生命周期内,可以在任何时刻获得子进程的基本信息,直到它不再需要为止,也就是到父进程主动调用 wait 或 waitpid 为止。
但这个过程存在两个问题,那就是如果父进程先于子进程退出了怎么办?以及子进程退出以后,父进程始终没有调用 wait 或 waitpid 怎么办?这就产生了两种进程:孤儿进程与僵尸进程。
既然所有进程都是父进程创建的,那就会发生无限回溯的问题,所以必须要有一个最初的进程,来担任所有进程的祖先,这个进程就是 init 进程。
当一个父进程退出,而他有若干子进程仍然在执行,那么,这些子进程就变成了孤儿进程。它们会自动被共同的祖先 -- init 进程收养,从而自动完成它们的状态收集工作。
另一种情况下,父进程仍然在执行,但没有通过调用 wait 或 waitpid 系统调用来完成子进程的状态收集工作,那么,这个虽然已经退出,但仍然占用着 pid,留存有进程状态信息的进程就变成了“不死不活”的状态,也就是僵死状态,或成为僵尸状态。
显然,这是一个很大的问题,首先,系统能够分配的 pid 数量是有限的,能够存储进程状态信息的资源同样是有限的,如果短时间产生大量僵尸进程,这会造成系统资源的浪费甚至导致系统无法创建新的进程。
从另一方面来说,当我们执行 ps 查看进程时,如果发现有大量 Z 状态的进程,对于我们监控系统运行状况、排查一些问题都会带来很大的影响。
既然僵尸进程是我们不希望看到的,那么如何避免产生僵尸进程呢?
如上文所述,子进程死后,会发送 SIGCHLD 信号给父进程,只要父进程收到此信号后执行 wait/waitpid 函数为子进程收尸即可,子进程就会顺利从僵死状态变为彻底消失。
父进程也可以显式忽略子进程的结束信号,系统会自动释放子进程资源而避免使子进程成为僵死进程。
由于父进程死后,子进程以及僵死进程会成为孤儿进程,从而会被过继给 init 进程,init 进程就会负责清理僵死进程。
在建立子进程时,使用 2 次 fork,让所建立的子进程成为父进程的孙子进程,而实际中的子进程则随即推出,和第三条相同,由于孙子进程的父进程已经退出,所以在孙子进程会被自动过继给守护进程,由守护进程负责为该进程回收资源。
#include <stdio.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
pid_t pid;
if ((pid = fork()) < 0)
{
fprintf(stderr,"Fork error!\n");
exit(-1);
}
else if (pid == 0) /* first child */
{
if ((pid = fork()) < 0)
{
fprintf(stderr,"Fork error!\n");
exit(-1);
}
else if (pid > 0)
exit(0); /* parent from second fork == first child */
sleep(2);
printf("Second child, parent pid = %d\n", getppid());
exit(0);
}
if (waitpid(pid, NULL, 0) != pid) /* wait for first child */
{
fprintf(stderr,"Waitpid error!\n");
exit(-1);
}
exit(0);
}
Docker 旨在提供一个经过封装和隔离的独立环境:
https://techlog.cn/article/list/10183736
通过 docker 的原理我们知道,实际上 docker 的所有进程都是我们指定的 ENTRYPOINT 这个 docker 进程的子进程,但我们不能保证这个 ENTRYPOINT 进程能够内置接管其孤儿子孙进程的能力,实际上几乎没人会真的这么做。这也就意味着,在我们的 docker 中,如果某一层的进程退出,那么他的所有子孙进程在结束后都会变成僵尸进程。
如何解决这个问题呢?我们可以将各个 linux 发行版官方提供的镜像作为基础镜像,从而让我们的 docker 中可以模拟整个系统,或者在 docker 中安装 systemd 或者 sysvint 这类初始化系统的进程,但这无疑要消耗比较大的磁盘资源,所以一般我们并不会采用这样的方法。
实际上,还有另一个选择,那就是 Bash 进程,Bash 进程内置了过继孤儿进程的能力,这样一来,只要我们让 docker 的 ENTRYPOINT 进程是通过 bash 启动的进程,然后所有其他进程都作为这个进程的子孙,孤儿进程就会自动被 Bash 进程过继。
但这么做的问题在于,Bash 不会将信号转发给子进程,也就是说,当我们要结束 docker 时,只有 bash 进程会被终止,而他的子孙进程的资源将无法得到有效回收。
另一方面,通过 bash 创建出来的进程,无论其执行结果如何,bash 都会以 0 作为返回状态退出,这样一来,如果实际执行的子进程是异常崩溃,我们就没有办法获取到这个进程的返回码了,而 docker 也会因为错误地判断了进程的执行状态而执行错误的重启策略,因为在 docker 看来,ENTRYPOINT 进程永远都是正常退出的,因为它返回了 0。
如今,已经有很多开源解决方案解决这个问题,比如 Phusion 写的 baseimage-docker 项目:
https://github.com/phusion/baseimage-docker
这个项目的目标是构建一个 ubuntu 系统的最小化基础镜像,因此他自然实现了 ubuntu 的 init 进程来自动过继孤儿进程。
尽管 baseimage-docker 已经比原生的 ubuntu 镜像小了很多,但可能你仍然觉得它有些过度庞大,也许你仅仅是需要一个能够过继孤儿进程的守护进程而已,那么,tini 这个项目就会非常适合你:
https://github.com/krallin/tini
tini 是模拟 init 进程的简单系统,专门用来执行一个子程序,并等待子程序结束,即便子程序已经变成僵尸程序也能捕捉到,同时也能转送 Signal 给子程序。
下面是一个使用 tini 的 dockerfile:
FROM nginx
RUN export TINI_VERSION=0.9.0 && \
export TINI_SHA=fa23d1e20732501c3bb8eeeca423c89ac80ed452 && \
curl -fsSL https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-static -o /bin/tini && \
echo 'Calculated checksum: '$(sha1sum /bin/tini) && \
chmod +x /bin/tini && echo "$TINI_SHA /bin/tini" | sha1sum -c
ENTRYPOINT ["/bin/tini","--","/opt/nginx/docker-entrypoint.sh"]
ENTRYPOINT ["nginx", "-c"]
CMD ["/etc/nginx/nginx.conf"]
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有