僵死进程存在的目的 和 需要处理的原因
设置僵死进程的目的是维护子进程的信息, 以便父进程在以后的某个时刻获取. 但是留存的僵死进程会占用内核的空间, 最终可能导致耗尽进程资源. 所以还是需要处理这些僵死进程.
处理僵死进程的方法
在服务器子进程终止时, 给父进程发送了一个SIGCHLD信号,该信号是在子进程终止时,由内核发送给父进程的一个信号. 每个信号都有一个与之关联的处置, 也称为行为. 我们可以通过调用sigaction函数来设置处置. 设定信号的处置有三种方法: (1)提供信号处理函数,一旦信号被捕获, 就调用该函数.(或者说特定信号一旦发生就立刻调用该信号处理函数) (2)处置设定为SIG_IGN, 一旦信号被捕获就忽略; (3)处置设定为SIG_DFL来启用它的默认处置, 比如默认处置是终止进程.
这里详细介绍第一种方法: 提供信号处理函数 void sig_chld(int signo);
#include "unp.h"
void
sig_chld(int signo)
{
pid_t pid;
int stat;
pid = wait(&stat);
printf("child %d terminated\n", pid);
return;
}
调用wait函数来处理已经终止的子进程
那么通过signal函数可以设置上述的sig_chld函数, 之所以不用sigaction函数来设置, 是因为那样操作有点复杂, 因为该函数的参数之一是我们必须分配并填写的结构. 所以不如直接调用sig_chld, 参数简单就两个, 第一个参数是信号名(例如SIGCHLD), 第二个参数或为指向函数的指针(例如指向sig_chld函数的指针), 或为SIG_IGN , 或为SIG_DFL.
如下是unix网络编程一书中定义的signal函数
/* include signal */
#include "unp.h"
Sigfunc *
signal(int signo, Sigfunc *func)
{
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if (signo == SIGALRM) {
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT; /* SunOS 4.x */
#endif
} else {
#ifdef SA_RESTART
act.sa_flags |= SA_RESTART; /* SVR4, 44BSD */
#endif
}
if (sigaction(signo, &act, &oact) < 0)
return(SIG_ERR);
return(oact.sa_handler);
}
/* end signal */
Sigfunc *
Signal(int signo, Sigfunc *func) /* for our signal() function */
{
Sigfunc *sigfunc;
if ( (sigfunc = signal(signo, func)) == SIG_ERR)
err_sys("signal error");
return(sigfunc);
}
所以综上所属, 函数之间的调用关系是
signal 调用 sig_chld函数和sigaction函数
sig_chld函数调用wait函数
处理被中断的系统调用
SIGCHLD信号在父进程阻塞于慢系统调用(accept)时由父进程捕获且相应的信号处理函数返回时, 内核就会使accept返回一个EINTR错误(被中断的系统调用). 而父进程如果不处理该错误, 就会终止. 那么如何处理被中断的系统调用呢? 有如下两种方法.
1. 如果在signal()函数里设置了SA_RESTART标志, 那么信号中断的系统调用将由内核自动重启.
2. 如果没有没有设置上述标志: 可以用如下程序重启
if((connfd=accept(listenfd, (SA*) &cliaddr, &clilen))<0){
if(errno==EINTR)
continue;// back to for()
else
err_sys("accept error");
父进程会同时收到多个SIGCHLD信号 怎么办?
在一个信号处理函数运行期间, 正被递交的信号是被阻塞的. 如果一个信号在被阻塞期间产生了一次或多次, 那么该信号被解阻塞之后通常只递交一次, 也就是说UNIX信号默认是不排队的. 如果存在多个子进程同时结束, 那么父进程会同时收到多个SIGCHLD信号 , 那么调用wait最终可能只会处理1个信号(5个信号都在信号处理函数执行之前产生), 或者不确定个数的信号(如果多个该信号产生时间有差距).
wait 和 waitpid 都可以处理已终止的子进程, 均返回两个值: 已终止子进程的进程ID号, 以及通过statloc指针返回的子进程终止状态(一个整数).
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
如果调用wait的进程没有已终止的子进程, 不过有一个或多个子进程仍在执行, 那么wait将阻塞到现有子进程第一个终止为止.
waitpid函数就等待哪个进程和是否阻塞给了我们更多的控制, 指定选项WNOHANG就可以告知waitpid在有尚未终止的子进程运行时不要阻塞. 用如下程序可以比较好地解决同时收到多个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 ;
}
TCP服务器程序最终版本
1. 实现了客户发来什么信息返回什么信息的功能;
2. 处理了僵死进程问题---Signal(SIGCHLD, sig_chld);
3. 处理了系统调用中断的问题---SA_RESTART
4. 处理了多个SIGCHLD信号同时到来不能完全处理所有僵死进程的问题---waitpid.
#include "unp.h"
#include <time.h>
void srv_echo(int sockfd)
{
ssize_t n;
char buf[1000];
again:
while((n=read(sockfd,buf,1000))>0)
Writen(sockfd,buf,n);
if(n<0 && errno==EINTR)
goto again;
else if(n<0)
err_sys("srv_echo: read error");
}
void
sig_chld(int signo)
{
pid_t pid;
int stat;
while( (pid=waitpid(-1, &stat, WNOHANG))>0)
printf("child %d terminated\n", pid);
return ;
}
int
main(int argc,char ** argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
char buff[1000];
listenfd=Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
servaddr.sin_port=htons(1234);
Bind(listenfd, (SA*) &servaddr, sizeof(servaddr));
Listen(listenfd, 100);
Signal(SIGCHLD,sig_chld);
for(;;){
clilen=sizeof(cliaddr);
connfd=Accept(listenfd, (SA*) &cliaddr, &clilen);
/*在Signal()函数里设置了SA_RESTART标志, 那么信号中断的系统调用将由内核自动重启
if((connfd=accept(listenfd, (SA*) &cliaddr, &clilen))<0){
if(errno==EINTR)
continue;// back to for()
else
err_sys("accept error");
}
*/
printf("connection from %s , port %d\n",
inet_ntop(AF_INET,&cliaddr.sin_addr, buff, sizeof(buff)),
ntohs(cliaddr.sin_port));
if((childpid=Fork())==0){
Close(listenfd);
srv_echo(connfd);
exit(0);
}
Close(connfd);
}
}