0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

linux下开发避免僵尸进程的方法

科技绿洲 来源:Linux开发架构之路 作者:Linux开发架构之路 2023-11-11 16:38 次阅读

一、什么是僵死进程?

一般情况下,程序调用exit(包括_exit和_Exit,它们的区别这里不做解释),它的绝大多数内存和相关的资源已经被内核释放掉,但是在进程表中这个进程项(entry)还保留着(进程ID,退出状态,占用的资源等等),你可能会问,为什么这么麻烦,直接释放完资源不就行了吗?这是因为有时它的父进程想了解它的退出状态。在子进程退出但还未被其父进程“收尸”之前,该子进程就是僵死进程,或者僵尸进程。如果父进程先于子进程去世,那么子进程将被init进程收养,这个时候init就是这个子进程的父进程。

所以一旦出现父进程长期运行,而又没有显示调用wait或者waitpid,同时也没有处理SIGCHLD信号,这个时候init进程就没有办法来替子进程收尸,这个时候,子进程就真的成了“僵尸”了。

二、僵死进程与孤儿进程的区别?

回答这个问题很简单,就是爸爸(父进程)和儿子(子进程)谁先死的问题!

如果当儿子还在世的时候,爸爸去世了,那么儿子就成孤儿了,这个时候儿子就会被init收养,换句话说,init进程充当了儿子的爸爸,所以等到儿子去世的时候,就由init进程来为其收尸。

如果当爸爸还活着的时候,儿子死了,这个时候如果爸爸不给儿子收尸,那么儿子就会变成僵尸进程。

三、僵死进程的危害?

  1. 僵死进程的PID还占据着,意味着海量的子进程会占据满进程表项,会使后来的进程无法fork.
  2. 僵死进程的内核栈无法被释放掉(1K 或者 2K大小),为啥会留着它的内核栈,因为在栈的最低端,有着thread_info结构,它包含着 struct_task 结构,这里面包含着一些退出信息

四、避免僵死进程的方法

网上搜了下,总结有三种方方法:

① 程序中显示的调用signal(SIGCHLD, SIG_IGN)来忽略SIGCHLD信号,这样子进程结束后,由内核来wai和释放资源

② fork两次,第一次fork的子进程在fork完成后直接退出,这样第二次fork得到的子进程就没有爸爸了,它会自动被老祖宗init收养,init会负责释放它的资源,这样就不会有“僵尸”产生了

③ 对子进程进行wait,释放它们的资源,但是父进程一般没工夫在那里守着,等着子进程的退出,所以,一般使用信号的方式来处理,在收到SIGCHLD信号的时候,在信号处理函数中调用wait操作来释放他们的资源。

五、对每个避免僵死进程方法的解析与总结

首先我们让我们来看一个生成僵尸进程的程序zombie.c如下:

#include < stdio.h >  
#include < stdlib.h >  
#include < unistd.h >  
  
int main(int argc, const char *argv[])  
{  
    int i;  
    pid_t pid;  
  
    for (i = 0; i < 10; i++) {  
        if ((pid = fork()) == 0)    /* child */  
            _exit(0);  
    }  
    sleep(10);  
  
    exit(EXIT_SUCCESS);  
}

运行程序,在10s睡眠期间使用ps查看进程,你会发现有10个标记为“defunct”的僵尸进程:

图片

接下来看第一种方法,程序avoid_zombie1.c如下:

#include < stdio.h >  
#include < stdlib.h >  
#include < signal.h >  
#include < unistd.h >  
#include < errno.h >  
  
int main(int argc, const char *argv[])  
{  
    pid_t pid;  
  
    if (SIG_ERR == signal(SIGCHLD, SIG_IGN)) {  
        perror("signal error");  
        _exit(EXIT_FAILURE);  
    }  
  
    while (1) {  
        if ((pid = fork()) == 0)    /* child */  
            _exit(0);  
    }  
  
    exit(EXIT_SUCCESS);  
}

程序运行期间通过ps命令的确没有发现僵尸进程的存在。

在man文档中有这段话:

Note that even though the default disposition of SIGCHLD is "ignore", explicitly setting the disposition to SIG_IGN results in different treatment of zombie process children.

意思是说尽管系统对信号SIGCHLD的默认处理就是“ignore”,但是显示的设置成SIG_IGN的处理方式在在这里会表现不同的处理方式(即子进程结束后,资源由系统自动收回,所以不会产生僵尸进程),这是信号SIGCHLD与其他信号的不同之处。

在man文档中同样有这样一段话:

The original POSIX standard left the behavior of setting SIGCHLD to SIG_IGN unspecified. 看来这个方法不是每个平台都使用,尤其在一些老的系统中,兼容性不是很好,所以如果你在写一个可移植的程序的话,不推荐使用这个方法。

第二种方法,即通过两次fork来避免僵尸进程,我们来看一个例子avoid_zombie2.c:

#include < stdio.h >  
#include < stdlib.h >  
#include < signal.h >  
#include < unistd.h >  
#include < errno.h >  
  
int main(int argc, const char *argv[])  
{  
    pid_t pid;  
  
    while (1) {  
        if ((pid = fork()) == 0) {  /* child */  
            if ((pid = fork()) > 0)  
                _exit(0);  
            sleep(1);  
            printf("grandchild, parent id = %ldn",  
                            (long)getppid());  
            _exit(0);  
        }  
        if (waitpid(-1, NULL, 0) != pid) {  
            perror("waitpid error");  
            _exit(EXIT_FAILURE);  
        }  
    }  
  
    exit(EXIT_SUCCESS);  
}

这的确是个有效的办法,但是我想这个方法不适宜网络并发服务器中,应为fork的效率是不高的。

最后来看第三种方法, 也是最通用的方法

先看我们的测试程序avoid_zombie3.c

#include < stdio.h >  
#include < stdlib.h >  
#include < errno.h >  
#include < string.h >  
#include < libgen.h >  
#include < signal.h >  
#include < unistd.h >  
#include < sys/wait.h >  
#include < sys/types.h >  
  
  
void avoid_zombies_handler(int signo)  
{  
    pid_t pid;  
    int exit_status;  
    int saved_errno = errno;  
  
    while ((pid = waitpid(-1, &exit_status, WNOHANG)) > 0) {  
        /* do nothing */  
    }  
  
    errno = saved_errno;  
}  
  
int main(int argc, char *argv[])  
{  
    pid_t pid;  
    int status;  
    struct sigaction child_act;   
  
    memset(&child_act, 0, sizeof(struct sigaction));  
    child_act.sa_handler = avoid_zombies_handler;  
    child_act.sa_flags = SA_RESTART | SA_NOCLDSTOP;   
    sigemptyset(&child_act.sa_mask);  
    if (sigaction(SIGCHLD, &child_act, NULL) == -1) {  
        perror("sigaction error");  
        _exit(EXIT_FAILURE);  
    }  
  
    while (1) {  
        if ((pid = fork()) == 0) {  /* child process */  
            _exit(0);  
        } else if (pid > 0) {        /* parent process */  
        }  
    }  
      
    _exit(EXIT_SUCCESS);  
}

首先需要知道三点:

  1. 当某个信号的信号处理函数被调用时,该信号会被操作系统阻塞(默认sa_flags不设置SA_NODEFER标志)。

2.当某个信号的信号处理函数被调用时,该信号阻塞时,该信号又多次发生,那么操作系统并不将它们排队,而是只保留第一次的,后续的被抛弃。

还有一点我们必须清楚的是

  1. wait系列函数与信号SIGCHLD是没有任何关系的,即wait系列函数并不是信号SIGCHLD驱动的。

这个时候,肯定有人有疑问了,既然会丢弃信号,那怎么保证可以收回所有的僵尸进程呢?

关于这个问题,我们可以这样来理解,当子进程结束时,不管有没有产生SIGCHLD信号,或者子进程产生了SIGCHLD信号,而不管父进程有没有收到SIGCHLD信号,这都与子进程已经终止这个事实无关,就是说,子进程终止与信号其实没有任何关系,只是操作系统在子进程终止时会发送信号SIGCHLD给父进程,告之其子进程终止的消息,这样的话,父进程就可以做相应的操作了。而wait系列函数的目的就是收回子进程终止时残留在进程列表中的信息,所以任何时候调用while ((pid = waitpid(-1, &exit_status, WNOHANG)) > 0)都可以收回所有的僵尸进程信息(可以参考下面的程序)。但是这里为什么放在信号处理函数中处理了,这样做的原因是:子进程什么时候结束是个异步事件,而信号机制就是用来处理异步事件的,所以当子进程结束时,可以迅速的收回其残余信息,这样系统中就不会积累大量的僵尸进程了。

也可以这样来理解:系统把所有的僵尸进程串在一起形成一个僵尸进程链表,而while ((pid = waitpid(-1, &exit_status, WNOHANG)) > 0)就是来清空这个链表的,直到waitpid()返回0,表明已经没有僵尸进程了,或者返回-1,表明出错(当错误码errno为ECHILD的时候同样表明已经不存在僵尸进程了)。

了解了以上知识点,就能理解为什么while ((pid = waitpid(-1, &exit_status, WNOHANG)) > 0)能够回收所有的僵尸进程了。

我们可以在上面的信号处理函数中加入相应的打印信息:

static int num1 = 0  
static int num2 = 0;  
void avoid_zombies_handler(int signo)  
{  
    pid_t pid;  
    int exit_status;  
    int saved_errno = errno;  
  
    printf("num1 = %dn", ++num1);  
    while ((pid = waitpid(-1, &exit_status, WNOHANG)) > 0) {  
        printf("num2 = %dn", ++num2);  
    }  
  
    errno = saved_errno;  
}

打印的结果你会发现,当num1递增1的时候,即每调用一次信号处理函数,num2一般会递增很多,即while循环了很多次,所以尽管有的SIGCHLD信号被丢弃了,但是我们不用担心子进程的残余信息会收不回来。退出while循环时,证明此时系统中已经没有僵尸进程了,所以退出信号处理函数后,阻塞的唯一SIGCHLD信号会再次触发该信号处理函数,这样我们就不用担心了。我们不防做个最坏的打算,即之前的信号全部被丢弃了,只有最后一次的SIGCHLD信号被捕获,从而触发了信号处理函数,这样我们也不用担心,因为while循环会一次性收回全部的僵尸进程信息,只是这次循环的次数要多得多罢了,当然这只是假设,一般系统不会出现这样的情况(可以参考本文最后一个程序事例)。

为了证明wait系统函数与信号SIGCHLD没有任何关系,我们可以做个简单的实验,代码如下:

#include < stdio.h >  
#include < stdlib.h >  
#include < unistd.h >  
#include < sys/wait.h >  
#include < sys/types.h >  
  
int main(int argc, char *argv[])  
{  
    int i;  
    pid_t pid;  
  
    for (i = 0; i < 5; i++) {  
        if ((pid = fork()) == 0)    /* child */  
            _exit(0);  
    }  
    sleep(10);  
    while (waitpid(-1, NULL, WNOHANG) > 0) {  
        /* do nothing */  
    }  
    sleep(10);  
  
    _exit(EXIT_SUCCESS);  
}

以下是打印结果:

图片

可以看到第一次sleep时系统中积累了5个僵尸进程,第二次sleep时,那5个僵尸进程都被收回了。这个也明显的看到了使用信号处理函数的优势,即可以保证系统不会积累大量的僵尸进程,它可以迅速的清理掉系统中的僵尸进程。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • Linux
    +关注

    关注

    87

    文章

    11219

    浏览量

    208879
  • 内存
    +关注

    关注

    8

    文章

    2996

    浏览量

    73870
  • 程序
    +关注

    关注

    116

    文章

    3773

    浏览量

    80831
  • WAIT
    +关注

    关注

    0

    文章

    4

    浏览量

    2505
收藏 人收藏

    评论

    相关推荐

    Linux开发_Linux进程编程

    介绍Linux进程概念、进程信号捕获、进程管理相关的命令的使用等知识点。
    的头像 发表于 09-17 15:38 1324次阅读
    <b class='flag-5'>Linux</b><b class='flag-5'>开发</b>_<b class='flag-5'>Linux</b><b class='flag-5'>下</b><b class='flag-5'>进程</b>编程

    Linux系统进程的几种状态介绍

    文章对 Linux 系统进程的几种状态进行介绍,并对系统出现大量僵尸进程和不可中断进程的场景进
    发表于 11-24 16:15 1.2w次阅读
    <b class='flag-5'>Linux</b>系统<b class='flag-5'>下</b><b class='flag-5'>进程</b>的几种状态介绍

    Linux进程间如何实现共享内存通信

    这次我们来讲一Linux进程通信中重要的通信方式:共享内存作为Linux软件开发攻城狮,进程
    发表于 04-26 17:14 674次阅读

    linux查询进程占用的内存方法有哪些?

    linux查询进程占用的内存方法
    发表于 04-08 06:03

    孤儿进程僵尸进程

    前段时间,由于研究经典面试题,把孤儿进程僵尸进程也总结了一。我们有这样一个问题:孤儿进程僵尸
    发表于 11-29 14:08

    Linux进程结构

    (TASK_KILLABLE):Linux内核 2.6.25 引入了一种新的进程状态,名为 TASK_KILLABLE。该状态的运行机制类似于 TASK_UNINTERRUPTIBLE,只不过处在该状态
    发表于 05-27 09:24

    为什么会出现LINUX僵尸进程

    僵尸进程出现在父进程没有回收子进程的PCB的时候,这个时候子进程已经结束,但是父进程没有回收他,
    发表于 08-07 06:48

    进程有几种状态?

    ?线程间同步方法有哪些?什么是内核线程和用户线程?内核线程和用户线程的区别?内核线程和用户线程有什么优缺点?什么是僵尸进程,孤儿进程,守护进程
    发表于 12-24 07:16

    僵尸进程的产生介绍和危害以及解决方法

    如果你经常使用 Linux,你应该遇到这个术语“僵尸进程Zombie Processes”。 那么什么是僵尸进程? 它们是怎么产生的? 它们
    的头像 发表于 12-18 15:56 5744次阅读
    <b class='flag-5'>僵尸</b><b class='flag-5'>进程</b>的产生介绍和危害以及解决<b class='flag-5'>方法</b>

    什么是僵尸进程_Linux僵尸进程可以被“杀死”吗?

    首先要明确一点,僵尸进程的含义是:子进程已经死了,但是父进程还没有wait它的一个中间状态,这个时候子进程是一个
    的头像 发表于 07-28 10:09 4651次阅读
    什么是<b class='flag-5'>僵尸</b><b class='flag-5'>进程</b>_<b class='flag-5'>Linux</b><b class='flag-5'>僵尸</b><b class='flag-5'>进程</b>可以被“杀死”吗?

    Linux 系统中僵尸进程

    不合理,父进程从不调用 wait 等系统调用来收集僵尸进程,那么这些进程会一直存在内存中。在 Linux
    发表于 04-02 14:40 435次阅读

    Linux数据中心服务器上的僵尸进程怎样正确的处理

    。虽然僵尸进程不像运行中的流氓应用程序那样占用宝贵资源,但可能会构成威胁。
    发表于 09-30 17:29 858次阅读
    <b class='flag-5'>Linux</b>数据中心服务器上的<b class='flag-5'>僵尸</b><b class='flag-5'>进程</b>怎样正确的处理

    Linux僵尸进程会被杀死吗?

    那么,根据POSIX标准关于信号(signal)的定义,当我们执行kill -9 4730(4730是4730和4731的TGID,也是整个进程用户态视角的PID)的时候,是要杀死整个4730进程的,所以这个时候4731被我们杀死,整个
    发表于 08-07 16:48 357次阅读
    <b class='flag-5'>Linux</b><b class='flag-5'>僵尸</b><b class='flag-5'>进程</b>会被杀死吗?

    如何在Linux终止僵尸进程

    在了解Zombie进程之前,让我回忆一什么是进程。简而言之,进程是程序实例。它可以是前台的交互式进程或后台的非交互式或自动
    的头像 发表于 12-12 17:40 2050次阅读

    如何查看系统是否有僵尸进程

    进程中的指令已经执行完成,但是进程PCB结构还没有回收。   即子进程先于父进程退出后,子进程的PCB需要其父
    的头像 发表于 11-29 15:52 6894次阅读
    如何查看系统是否有<b class='flag-5'>僵尸</b><b class='flag-5'>进程</b>