多进程并发如何防止僵尸进程——服务器开发

在并发服务器设计中,很常用的一种办法是用fork为每个连接创建子进程来单独处理客户端请求。

流程图如下:


可见,在父进程中直接执行accept等待下一个连接而并没有用wait或者waitpid来等待子进程返回。这会造成怎样的后果呢?当子进程exit退出的时候,它并没有真正销毁,而是还着保留一个数据结构用来记录它的退出状态等信息(因为父进程有可能会获取该信息)。若父进程不用wait或者waitpid等待,则此信息会一直保留直到父进程退出。不幸的是,并发服务器恰恰在正常情况会一直运行下去,并且不断为每个连接创造子进程,久而久之造成严重的资源泄漏。所以并发服务器必须采用某些手段来防止僵尸进程。

在子进程退出时会给父进程发送一个SIGCHLD信号,我们可以在父进程循环accept之前用signal来为SIGCHLD注册一个回调函数来等待子进程返回。

signal函数的声明:

void (*signal(int signo,void (*func)(int)))(int);
这是一个含函数指针的函数声明,它的第二个参数和返回值都是函数指针,也可以用以下声明方式。
typedef void(*sig_t) ( int );
sig_t signal(int signo,sig_t func);

注:以上两个函数声明是同等的。

简单来解释:signal第一参数填的是要处理的信号(这里是SIGCHLD),第二个参数是收到这个信号要调用的回调函数的指针。

具体做法如下:

void sid_child(int signo)      //处理SIGCHLD信号的回调函数
{
    pid_t pid;
    int stat;
    while((pid=waitpid(-1,&stat,WNOHANG))>0);
    return;
}

//此处省略之前socket、bind
listen(servfd,10);
signal(SIGCHLD,sid_child);   //为SIGCHLD的注册回调函数sid_child
while(1)
{
      if((cliefd=accept(servfd,(sockaddr*)0,0))<0)
      {
          if(errno==EINTR) continue;
          else err_sys("accept call error");
      }
      //这里省略fork创建子进程处理请求
}

解释:

1、

signal(SIGCHLD,sid_child);  
调用后,当父进程收到有子进程传来的SIGCHLD信号,就会中断调用回调函数sid_child。

2、

while((pid=waitpid(-1,&stat,WNOHANG))>0);
在sid_child函数里,循环调用waitpid等待子进程。

为什么要循环调用waitpid? waitpid的用法是如何?

waitpid的声明

pid_t waitpid(pid_t pid,int * status,int options);

这里挑重点的讲:waitpid功能是等待某个子进程返回,waitpid第一个参数是要等待返回的子进程ID号,若填-1则代表等待任意子进程返回。第二个参数用以接受子进程终止状态。第三个是附加选项,,WNOHANG选项的意思使函数不阻塞,无论有没子进程退出都返回。

WNOHANG选项下waitpid的调用特性:

2.1、当有子进程退出且正常返回时,返回子进程ID号;

2.2、当没有子进程退出,返回0;

2.3、当调用出错返回-1;

又因为无论有多少子进程只要有没处理的子进程退出,就会父进程收到且只有一个SIGCHLD,所以while((pid=waitpid(-1,&stat,WNOHANG))>0); 的意思是:只有收到SIGCHLD信号,就循环等待退出的子进程,直到waitpid返回非大于零的数(代表没有子进程退出了)。之后又返回正常的循环中,accept等待连接。当下次有子进程退出,父进程会再一次收到SIGCHLD。

3、

 if((cliefd=accept(servfd,(sockaddr*)0,0))<0)
 {
       if(errno==EINTR) continue;
          else err_sys("accept call error");
 }
accept、write、read等都是慢系统调用。当服务器阻塞在accept中时,我们知道父进程收到SIGCHLD就会发生中断并调用我们注册的回调函数sid_child。当回调函数返回时,慢系统调用的函数可能会返回一个EINTR错误,表明该函数在阻塞等待期间被中断了。当然这个错误是我们设计预料之内的,所以我们得调用continue来继续循环工作。


这样就防止僵尸进程的出现。


你可能感兴趣的:(Linux程序设计,网络开发)