本文共 1853 字,大约阅读时间需要 6 分钟。
在之前的篇章中,我们已经实现了点对点通信,服务器对多个客户端信息回射,我们一直都是以进程的方式来处理这些通信,一般都是server端在应对多个client端的请求时,fork出多个子进程来处理对应的client,这种方式带来了一种新的问题,叫僵尸进程。
维基百科上的介绍如下:
在类UNIX系统中,僵尸进程是指完成执行(通过exit系统调用,或运行时发生致命错误或收到终止信号所致)但在操作系统的进程表中仍然有一个表项(进程控制块PCB),处于”终止状态”的进程。这发生于子进程需要保留表项以允许其父进程读取子进程的exit status:一旦退出态通过wait系统调用读取,僵尸进程条目就从进程表中删除,称之为”回收(reaped)”。正常情况下,进程直接被其父进程wait并由系统回收。进程长时间保持僵尸状态一般是错误的并导致资源泄漏。 英文术语zombie process源自en:zombie — 不死之人,隐喻子进程已死但仍然没有被收割。与正常进程不同,kill命令对僵尸进程无效。孤儿进程不同于僵尸进程,其父进程已经死掉,但孤儿进程仍能正常执行,但并不会变为僵尸进程,因为被init(进程ID号为1)收养并wait其退出。简要地说,就是一个进程退出的时候并没有完全消失,而是留下一个称为僵尸进程(Zombie)的数据结构,里面存储着进程退出时的一些信息(供其他进程收集),之后一直在系统中存在,除非被主动回收或父进程退出,但是没任何作用
开头就提到了,我们这里是server端对多个client端,并且是fork出子进程来应答。当我们的client通信完成了,在自己这一端退出了client,那么服务器那端对应的应答子进程也会收到一个结束符,宣告这个进程退出,但是由于是fork出来的子进程,父进程没有退出,所以子进程的退出势必会留下一个僵尸进程。
我们可以模拟看看僵尸进程的产生:
在client.c中修改了一下代码,设定了5个client端连接server,用其中一个通信,之后,我再ctrl+c终止了client的运行,这样一来5个client端全部退出。 之后我们再输入ps -ef|grep server,查看系统中和server有关的进程: 很明显,有5个server僵尸进程。后缀是defunct 这时候的server父进程还没有关闭,很明显就是它fork出来的5个子进程了。这样就产生了僵尸进程,试想一下,正常的服务器每天要接受的请求和应答何其多,如果每个用户都留下这么一个僵尸进程,长期以往,服务器肯定会撑爆,所以这种情况必须主动处理。
子进程死后,系统会发送SIGCHLD 信号给父进程,父进程对其默认处理是忽略。如果想响应这个消息,父进程通常在SIGCHLD 信号事件处理程序中,使用wait系统调用来响应子进程的终止。
知道了这一点,我们就可直接在程序中添加代码了:
//直接在main函数中添加如下代码#includesignal(SIGCHLD,SIG_IGN);//这个是交给系统init去回收僵尸进程,SIG_IGN表示忽略信号
因为是系统通知,这段代码可以写在父进程(main函数)的任意位置,都会捕捉到子进程返回的信号,并优先做出回应,优先执行这段代码。
添加完之后,在执行之前同样的操作,就不会出现僵尸进程了。也可以如下这么做:
void handle_sigchld(int sig){ while(waitpid(-1,NULL,WNOHANG) > 0) ; //pid = -1 等待任何子进程,相当于 wait() //WNOHANG 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若结束,则返回该子进程的ID}int main(void){ signal(SIGCHLD,handle_sigchld) //使用waitpid函数实现,主动处理fork出来的会变成僵尸进程的方法 ......}
waitpid停止目前进程的执行,直到有信号到来或子进程结束。
waitpid执行成功,返回子进程识别码(PID),错误返回-1,存入全局变量errno中。更多wait和waitpid函数的详解参考,这里不作解释了。