1、什么是僵尸进程?一般情况下,当一个程序调用exit(包括_exit和_Exit,这里不解释它们的区别)时,它的大部分内存和相关资源已经被内核释放了,但是在进程表中,这个进程项(entry)还保留着(进程ID、退出状态、占用的资源等),你可能会问,怎么这么麻烦,释放资源不就好了吗?这是因为有时它的父进程想知道它的退出状态。在子进程退出但未被其父进程“回收”之前,子进程是一个死进程,或者说僵尸进程。如果父进程先于子进程死亡,那么子进程就会被init进程收养。这时init就是子进程的父进程。所以一旦父进程运行了很长时间,却没有显示对wait或waitpid的调用,也没有处理SIGCHLD信号,此时init进程就没有办法为子进程收尸了。这时候子进程就真的变成了“僵尸”。向上。2.僵尸进程和孤儿进程有什么区别?这个问题的答案很简单,就是父亲(父进程)还是儿子(子进程)谁先死的问题!如果父亲去世而儿子还活着,儿子就成了孤儿。这时候儿子就会被init收养了。也就是说,init进程充当了儿子的父亲,所以当儿子死了,init进程去收尸。如果儿子死了,而父亲还活着,如果此时父亲不收儿子的尸体,儿子就会变成僵尸进程。3、死进程的危害?dead进程的PID仍然被占用,意味着大量子进程会占用进程表项,导致后续进程无法fork。死进程的内核栈不能释放(1K或2K大小),为什么保留它的内核栈,因为在栈的最下端,有一个thread_info结构体,里面有一个struct_task结构体,里面有一些exit信息。4、网上搜索了避免死进程的方法,有3种方法:①调用程序中显示的signal(SIGCHLD,SIG_IGN)忽略SIGCHLD信号,这样子进程结束后,内核会等待并释放资源②fork两次,fork完成后第一次fork的子进程直接退出,这样第二次fork得到的子进程没有父亲,会自动被祖先init收养,init会负责释放它的资源,这样就不会出现“僵尸”③等待子进程释放自己的资源,但是父进程一般没有时间守在那里,等待子进程退出,所以一般用signal来处理。当SIGCHLD信号到来时,调用signalhandler中的wait操作释放自己的资源。5、各种避免僵尸进程的方法分析总结首先,让我们看一个产生僵尸进程的程序zombie.c,如下所示:#include#include#includeintmain(intargc,constchar*argv[]){inti;pid_tpid;for(i=0;i<10;i++){if((pid=fork())==0)/*child*/_exit(0);}sleep(10);exit(EXIT_SUCCESS);}运行程序,用ps查看10s休眠期间的进程,会发现有10个僵尸进程被标记为“defunct”:接下来看第一种方法,程序avoid_zombie1.c如下:#include#include#include#include#includeintmain(intargc,constchar*argv[]){pid_tpid;if(SIG_ERR==signal(SIGCHLD,SIG_IGN)){perror("signalerror");_exit(EXIT_FAILURE);}while(1){if((pid=fork())==0)/*child*/_exit(0);}exit(EXIT_SUCCESS);}程序运行过程中,ps命令没有发现僵尸进程的存在。man文档中有一段话:请注意,即使SIGCHLD的默认处置是“忽略”,将处置显式设置为SIG_IGN也会导致对僵尸进程子进程的不同处理。意思是虽然系统默认对信号SIGCHLD的处理是“忽略”,但是这里显示为SIG_IGN的处理方式会显示不同的处理方式(即子进程结束后,资源会被系统自动回收),所以不会产生僵尸进程),这就是SIGCHLD信号和其他信号的区别。man文档中也有这样一段话:原始的POSIX标准没有指定将SIGCHLD设置为SIG_IGN的行为。这个方法好像不是每个平台都用的,尤其是一些老系统,兼容性不是很好,所以如果你是写可移植的程序,不推荐使用这个方法。第二种方法是通过两次fork来避免僵尸进程。让我们看一个avoid_zombie2.c的例子:#include#include#include#include#includeintmain(intargc,constchar*argv[]){pid_tpid;while(1){if((pid=fork())==0){/*child*/if((pid=fork())>0)_exit(0);sleep(1);printf("grandchild,parentid=%ld\n",(long)getppid());_exit(0);}if(waitpid(-1,NULL,0)!=pid){perror("waitpiderror");_exit(EXIT_FAILURE);}}exit(EXIT_SUCCESS);}这确实是一个有效的方法,但我认为这种方法不适用于并发网络服务器.应该是fork的效率不高。最后再看第三种方法,也是最通用的方法。先来看看我们的测试程序avoid_zombie3.c#include#include#include#include#include#include#include#include#includevoidavoid_zombies_handler(intsigno){pid_tpid;intexit_status;intsaved_errno=errno;while((pid=waitpid(-1,&exit_status,WNOHANG))>0){/*donothing*/}errno=saved_errno;}intmain(intargc,char*argv[]){pid_tpid;intstatus;structsigactionchild_act;memset(&child_act,0,sizeof(structsigaction));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("sigactionerror");_exit(EXIT_FAILURE);}while(1){if((pid=fork())==0){/*childprocess*/_exit(0);}elseif(pid>0){/*parentprocess*/}}_exit(EXIT_SUCCESS);}首先,你需要知道三件事:1.当一个信号的信号处理函数被调用时,该信号会被操作系统阻塞(默认sa_flags不设置SA_NODEFER标志)。2.当调用某个信号的信号处理函数时,当信号阻塞时,信号多次出现,那么操作系统不会将它们排队,只保留第一个,后面的丢弃。还有一点我们必须明确的是3.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)可以回收所有的僵尸进程了。我们可以在上面的信号处理函数中加入相应的打印信息:staticintnum1=0staticintnum2=0;voidavoid_zombies_handler(intsigno){pid_tpid;intexit_status;intsaved_errno=errno;printf("num1=%d\n",++num1);while((pid=waitpid(-1,&exit_status,WNOHANG))>0){printf("num2=%d\n",++num2);}errno=saved_errno;}你会发现当num1递增时1,也就是每次调用信号处理函数,num2一般都会自增很多,也就是while循环了很多次,所以虽然有些SIGCHLD信号被丢弃了,但是我们不用担心子进程未被恢复。当退出while循环时,证明此时系统中没有僵尸进程,所以退出信号处理函数后,唯一被阻塞的SIGCHLD信号会再次触发信号处理函数,所以我们不用担心关于它。我们无法阻止最坏的打算,也就是前面的信号全部丢弃,只捕获最后一个SIGCHLD信号,触发信号处理函数,所以不用担心,因为while循环会取返回所有Zombie进程的信息只是这次的循环次数多了很多。当然,这只是一种假设,一般系统中不会出现这样的情况(请参考本文最后一个程序示例)。为了证明等待系统函数与信号SIGCHLD无关,我们可以做一个简单的实验,代码如下:#include#include#include#include#includeintmain(intargc,char*argv[]){inti;pid_tpid;for(i=0;i<5;i++){if((pid=fork())==0)/*child*/_exit(0);}sleep(10);while(waitpid(-1,NULL,WNOHANG)>0){/*donothing*/}sleep(10);_exit(EXIT_SUCCESS);}下面是打印结果:可以看到第一次sleep的时候系统中积累了5个zombie进程,第二次sleep的时候5个zombie进程都被回收了。这也很明显地体现了使用信号处理函数的优势,即可以保证系统不会积累大量的僵尸进程,并且可以快速清理系统中的僵尸进程。