前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >详解僵尸进程与孤儿进程

详解僵尸进程与孤儿进程

作者头像
用户3147702
发布2022-12-21 17:52:37
1.7K0
发布2022-12-21 17:52:37
举报
文章被收录于专栏:小脑斧科技博客

1. 引言

进程是操作系统基础的调度单位,我们日常接触了很多,自然不必多说。但有时,一个进程的状态变成了 Z,我们杀不死它,它持有的资源我们也不能回收,这显然是一个棘手的问题。

那么,进程究竟有哪些状态?Z 状态又意味着什么?我们怎么去避免这样的情况发生?这就是本文将要讲述的重点。

2. 进程状态

2.1 进程状态码

在 linux 系统中,进程共有如下六种状态:

  • D: 不可中断 Uninterruptible sleep,通常是正在进行 IO 操作;
  • R: 正在运行中,或者在调度器队列中,但已经是就绪状态;
  • S: 正处于休眠状态;
  • T: 停止或者被追踪状态;
  • Z: 僵尸进程;
  • X: 已经死掉,操作系统正在进行。

2.2 额外的进程状态码标识

有时,我们会看到进程状态码的后面紧跟着一位,这一位就是额外的状态码标识,说明了更多的状态信息:

  • <: 高优先级
  • N: 低优先级
  • L: 该进程的某些页被锁进内存
  • s: 包含有子进程;
  • +: 位于后台的进程组;
  • l: 该进程是一个线程

3. 什么是僵尸进程与孤儿进程

在 linux 系统中,进程都是由父进程创建的,当父进程执行 fork 系统调用完成子进程创建后,子进程和父进程就独立存在了,但两者又有着密切的关系,按照标准的流程,父进程要在子进程完成执行后,调用 wait 或 waitpid 系统调用来为子进程回收系统资源(包括进程 id、进程退出状态、进程运行时间)。

这样一来,父进程在子进程的完整生命周期内,可以在任何时刻获得子进程的基本信息,直到它不再需要为止,也就是到父进程主动调用 wait 或 waitpid 为止。

但这个过程存在两个问题,那就是如果父进程先于子进程退出了怎么办?以及子进程退出以后,父进程始终没有调用 wait 或 waitpid 怎么办?这就产生了两种进程:孤儿进程与僵尸进程。

3.1 孤儿进程

既然所有进程都是父进程创建的,那就会发生无限回溯的问题,所以必须要有一个最初的进程,来担任所有进程的祖先,这个进程就是 init 进程。

当一个父进程退出,而他有若干子进程仍然在执行,那么,这些子进程就变成了孤儿进程。它们会自动被共同的祖先 -- init 进程收养,从而自动完成它们的状态收集工作。

3.2 僵尸进程

另一种情况下,父进程仍然在执行,但没有通过调用 wait 或 waitpid 系统调用来完成子进程的状态收集工作,那么,这个虽然已经退出,但仍然占用着 pid,留存有进程状态信息的进程就变成了“不死不活”的状态,也就是僵死状态,或成为僵尸状态。

显然,这是一个很大的问题,首先,系统能够分配的 pid 数量是有限的,能够存储进程状态信息的资源同样是有限的,如果短时间产生大量僵尸进程,这会造成系统资源的浪费甚至导致系统无法创建新的进程。

从另一方面来说,当我们执行 ps 查看进程时,如果发现有大量 Z 状态的进程,对于我们监控系统运行状况、排查一些问题都会带来很大的影响。

4. 怎么避免僵尸进程

既然僵尸进程是我们不希望看到的,那么如何避免产生僵尸进程呢?

4.1 wait/waitpid

如上文所述,子进程死后,会发送 SIGCHLD 信号给父进程,只要父进程收到此信号后执行 wait/waitpid 函数为子进程收尸即可,子进程就会顺利从僵死状态变为彻底消失。

4.2 忽略 SIGCHLD 信号

父进程也可以显式忽略子进程的结束信号,系统会自动释放子进程资源而避免使子进程成为僵死进程。

4.3 杀死父进程

由于父进程死后,子进程以及僵死进程会成为孤儿进程,从而会被过继给 init 进程,init 进程就会负责清理僵死进程。

4.4 fork 两次

在建立子进程时,使用 2 次 fork,让所建立的子进程成为父进程的孙子进程,而实际中的子进程则随即推出,和第三条相同,由于孙子进程的父进程已经退出,所以在孙子进程会被自动过继给守护进程,由守护进程负责为该进程回收资源。

代码语言:javascript
复制
#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);   
}

5. docker 中如何避免僵尸进程

5.1 docker 中的僵尸进程

Docker 旨在提供一个经过封装和隔离的独立环境:

https://techlog.cn/article/list/10183736

通过 docker 的原理我们知道,实际上 docker 的所有进程都是我们指定的 ENTRYPOINT 这个 docker 进程的子进程,但我们不能保证这个 ENTRYPOINT 进程能够内置接管其孤儿子孙进程的能力,实际上几乎没人会真的这么做。这也就意味着,在我们的 docker 中,如果某一层的进程退出,那么他的所有子孙进程在结束后都会变成僵尸进程。

5.2 守护进程

如何解决这个问题呢?我们可以将各个 linux 发行版官方提供的镜像作为基础镜像,从而让我们的 docker 中可以模拟整个系统,或者在 docker 中安装 systemd 或者 sysvint 这类初始化系统的进程,但这无疑要消耗比较大的磁盘资源,所以一般我们并不会采用这样的方法。

5.3 Bash 进程作为守护进程

实际上,还有另一个选择,那就是 Bash 进程,Bash 进程内置了过继孤儿进程的能力,这样一来,只要我们让 docker 的 ENTRYPOINT 进程是通过 bash 启动的进程,然后所有其他进程都作为这个进程的子孙,孤儿进程就会自动被 Bash 进程过继。

但这么做的问题在于,Bash 不会将信号转发给子进程,也就是说,当我们要结束 docker 时,只有 bash 进程会被终止,而他的子孙进程的资源将无法得到有效回收。

另一方面,通过 bash 创建出来的进程,无论其执行结果如何,bash 都会以 0 作为返回状态退出,这样一来,如果实际执行的子进程是异常崩溃,我们就没有办法获取到这个进程的返回码了,而 docker 也会因为错误地判断了进程的执行状态而执行错误的重启策略,因为在 docker 看来,ENTRYPOINT 进程永远都是正常退出的,因为它返回了 0。

5.4 开源方案1 -- baseimage-docker

如今,已经有很多开源解决方案解决这个问题,比如 Phusion 写的 baseimage-docker 项目:

https://github.com/phusion/baseimage-docker

这个项目的目标是构建一个 ubuntu 系统的最小化基础镜像,因此他自然实现了 ubuntu 的 init 进程来自动过继孤儿进程。

5.5 开源方案2 -- tini

尽管 baseimage-docker 已经比原生的 ubuntu 镜像小了很多,但可能你仍然觉得它有些过度庞大,也许你仅仅是需要一个能够过继孤儿进程的守护进程而已,那么,tini 这个项目就会非常适合你:

https://github.com/krallin/tini

tini 是模拟 init 进程的简单系统,专门用来执行一个子程序,并等待子程序结束,即便子程序已经变成僵尸程序也能捕捉到,同时也能转送 Signal 给子程序。

下面是一个使用 tini 的 dockerfile:

代码语言:javascript
复制
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"]
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-10-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小脑斧科技博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 引言
  • 2. 进程状态
    • 2.1 进程状态码
      • 2.2 额外的进程状态码标识
      • 3. 什么是僵尸进程与孤儿进程
        • 3.1 孤儿进程
          • 3.2 僵尸进程
          • 4. 怎么避免僵尸进程
            • 4.1 wait/waitpid
              • 4.2 忽略 SIGCHLD 信号
                • 4.3 杀死父进程
                  • 4.4 fork 两次
                  • 5. docker 中如何避免僵尸进程
                    • 5.1 docker 中的僵尸进程
                      • 5.2 守护进程
                        • 5.3 Bash 进程作为守护进程
                          • 5.4 开源方案1 -- baseimage-docker
                            • 5.5 开源方案2 -- tini
                            相关产品与服务
                            容器镜像服务
                            容器镜像服务(Tencent Container Registry,TCR)为您提供安全独享、高性能的容器镜像托管分发服务。您可同时在全球多个地域创建独享实例,以实现容器镜像的就近拉取,降低拉取时间,节约带宽成本。TCR 提供细颗粒度的权限管理及访问控制,保障您的数据安全。
                            领券
                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档