也谈孤儿进程与僵尸进程

之前调bug时会看到进程的状态是‘Z’,由此知道僵尸进程,将查的资料略作总结,以备后用。


先上定义:

孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。

孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,

那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。


有一篇文章讲的很易懂,原文链接:http://blog.youkuaiyun.com/u014181676/article/details/22103203

以防删除,粘贴关键如下:

一、

在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息

(包括进程号the process ID,退出状态the termination status of the process,

运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放。

也就是父进程必须为子进程用wait来进行收尸,问题来了,如果父进程不替子进程收尸怎么办


如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。



孤儿进程是没有父进程的进程,呢么孤儿进程死后,收尸的工作就落到了init进程身上。

init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init,

而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,

init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。


任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。  

如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。


僵尸进程危害:

例如有个进程,它定期的产 生一个子进程,这个子进程需要做的事情很少,做完它该做的事情之后就退出了,因此这个子进程的生命周期很短,但是,父进程只管生成新的子进程,至于子进程 退出之后的事情,则一概不闻不问,这样,系统运行上一段时间之后,系统中就会存在很多的僵死进程,倘若用ps命令查看的话,就会看到很多状态为Z的进程。

严格地来说,僵死进程并不是问题的根源,罪魁祸首是产生出大量僵死进程的那个父进程。因此,当我们寻求如何消灭系统中大量的僵死进程时,答案就是把产生大 量僵死进程的那个元凶枪毙掉(也就是通过kill发送SIGTERM或者SIGKILL信号啦)。枪毙了元凶进程之后,它产生的僵死进程就变成了孤儿进 程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源,这样,这些已经僵死的孤儿进程 就能瞑目而去了。


二、简单例子

ret = fork();
while(1)
{
    if (0 == ret)//child
    {
        print (getpid(), getppid);
        exit(0); //子进程退出
    }
    else if ( 0 < ret)//父进程
    {
        print(getpid());
        sleep(10);//每隔十秒产生一个子进程并退出后变成僵尸进程
    }
}

可以用ps 查看进程,发现过一会就多一个僵尸进程。

这样,只要父进程不死,他又不替子进程收尸,就会有越来越多的僵尸进程。既然父进程不给子进程收尸,呢就不是合格的父进程,于是,你毫不犹豫地用kill把父进程干掉了,这样,呢些僵尸着的子进程的收尸工作就移交给init进程了,僵尸们终于可以瞑目了。


三、子进程父进程间信号。

子进程exit后,会向父进程发送SIGCHLD信号,作为一个合格的父进程,父进程代码中应该有子进程exit后接收处理SIGCHLD信号的部分。

other_code();
signal(SIGCHLD,child_dead_process); //只要子进程一exit,父进程就会接收到SIGCHLD信号,并调用
while(1)                            //child_dead_process 来进行处理善后工作。
{
    ret = fork();
    if (0 == ret)
    {
        handle_sth();
        exit(0);
    }
    else if (0 < ret)
    {
         deal_sth_or_listen_sth();
    }
}

但child_dead_process()函数里要调用 wait或者waitpid函数来处理,其详细用法见附录一。


void chile_dead_process()
{
    .....
    deadpid = wait(&returnstatus) //基本处理完善后工作。收到一个信号,处理掉一个僵尸,当然,代码很不
    if ()                         //严谨,只说明大致意思,细节暂不考虑。
    ......
    .......
}

插入该部分相关的又深一层的题外话,见附录二


四、并发服务器中的使用


一些架构是这样的,服务器父进程监听请求,有请求的时候fork个子进程来处理,处理完了子进程自动退出,

但是,问题来了,请求过多的时候,父进程已经负担够重了,还要处理子进程exit后的善后工作,

所以为了提高性能,自古忠孝不能两全,父进程为了提高服务器的系统性能,

不得不把子进程exit后的善后工作移交他人(init)。

方法:signal(SIGCHLD, SIG_IGN); //父进程忽略子进程退出时所发出的信号


借用一句话:

对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将 SIGCHLD信号的操作设为SIG_IGN。

signal(SIGCHLD,SIG_IGN);

这样,内核在子进程结束时不会产生僵尸进程。

例子可以自己思考怎么写。


附录一:

wait

系统中的僵尸进程都要由wait系统调用来回收。

函数原型:

#include<sys/types.h>

#include<sys/wait.h>

pid_t wait(int *status);

进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就像下面这样:

pid = wait(NULL);

如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。


waitpid



附录二:

摘抄一段感觉还不错的分析吧。

提问者:
看unix网络编程第一卷的时候,碰到书上这样一个例子:
一个并发服务器, 每一个客户端连接服务器就fork一个子进程.书上讲到当同时有n多个客户端断开连接时,
服务器端同时有n多个子进程终止, 这时候内核同时向父进程发送n多个sigchld信号.它的sigchld信号处理
函数如下:
void sig_chld(int signo)
{
       pid_t pid;
       int stat;
        
       while((pid = waitpid(-1, &stat, WNOHANG)) > 0){
               printf("child %d terminated\n", pid);
       }
        return;
}

我的问题是:既然sigchld是不可靠的信号,进程就不可能对sigchld进行排队, 直接丢弃了sigchld信号(当进程注册信号的时候,发现已有sigchld注册进未决信号, 因为内核同时发送多个sigchld).请问大家上面的代码是如何保证不产生僵尸进程的.谢谢!




牛人正解:

根本就不需要找回来!
好比有五个进程,
不妨分别称为 p1 p2 p3 p4 p5,
一开始 p1 结束了,发了一个 SIGCHLD(s1),
这时父进程可能空闲了,于是开始处理这个信号,假设处理的过程中 p2 又结束了,又发了一个 SIGCHLD(s2),
这时候已经有两个信号了(一个正在处理,一个待处理),这时如果 p3 又结束了,那么它发的那个 SIGCHLD(s3) 势必会丢失,
丢失了怎么办?
没关系,因为那个信号处理函数是个循环嘛,
所以 while(waitpid()) 的时候,会把 p1 p2 p3 都处理的。
即使是很不幸,因为十分凑巧的原因,p3 没有被回收,导致变成僵尸进程了,也没关系,
因为还有 p4 p5 嘛,等到 p4 或者 p5 结束的时候,
又会再一次调用 while(waitpid()),到时候虽说这个 while(waitpid()) 是由 p4/p5 引起的,但是它也会一并把 p3 也处理的,因为它是个循环嘛!

如果还搞不懂,你就再看看 waitpid 的 man。

记住一点:
waitpid 和 SIGCHLD 没关系,即使是某个子进程对应的 SIGCHLD 丢失了,只要父进程在任何一个时刻调用了 waitpid,那么这个进程还是可以被回收的。

哎呀呀,简直费劲死了,其实说白了,就是一个“生产者-消费者”问题。
子进程结束的时候,系统“生产”出一个僵尸进程,
同时用 SIGCHLD 通知父进程来“消费”这个僵尸进程,
即使是 SIGCHLD 丢失了,没有来得及消费,
但是只要有一次消费,就会把所有的僵尸进程都处理光光!
(我再说一遍:因为,while(waitpid()) 是个循环嘛!)






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值