UNIX网络编程笔记(5):处理SIGCHLD信号
在上一讲中,我们使用fork函数得到了一个简单的并发服务器。然而,这样的程序有一个问题,就是当子进程终止时,会向父进程发送一个SIGCHLD信号,父进程默认忽略,导致子进程变成一个僵尸进程。僵尸进程一定要处理,因为僵尸进程占用内核中的空间,耗费进程资源。这里通过signal函数处理信号。
1、信号是啥?
信号(signal)就是告知某个进程发生了某个事件的通知,有时也叫软件中断(software interrupt)。信号通常是异步发生的,也就是说进程预先不知道信号的准确发生时间。
信号可以看做进程间的一种通信(我的理解),不但可以由一个进程发送给另一个进程,还可以由自己发送给自己,甚至内核发送给进程。上一讲中的SIGCHLD就是内核在任何一个进程终止时发送给父进程的一个信号。
对于信号,都有一个信号发生时响应的动作,也叫行为(action)。可以通过调用sigcation函数来设置对信号的处理方法。通常有三种方式处理信号:
(1)调用信号处理函数处理。提供一个函数,只要有信号发生就调用,这个函数称为信号处理函数(signal handler),这种行为叫做捕获(catching)信号。不过SIGKILL和SIGSTOP不能捕获。信号处理函数的原型如下:
void handler(int signo);没有返回值,只有一个信号值这个参数。
(2)忽略它。可以把某个信号的处理设置为SIG_IGN来忽略。不过SIG_KILL和SIG_STOP不能忽略;
(3)默认处理。还可以把某个信号的处理设置成SIG_DFL来启用默认处理;
可以通过调用sigaction函数建立信号处置的POSIX方法。不过sigaction优点复杂,因为该函数的参数之一是需要自己分配并填写的结构。可以通过调用signal函数来简化处理。下面是signal函数的定义:
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; #endif }else{ #ifdef SA_RESTART //act.sa_flags|=SA_RESTART; #endif } if(sigaction(signo,&act,&oact)<0) return(SIG_ERR); return(oact.sa_handler); }函数有两个参数,第一个是信号名,第二个是函数指针,或SIG_IGN、SIG_DFL。首先,我们通过下面的定义简化signal原型定义:
typedef void sigfunc(int);这个复杂的定义是这样的:
void (*signal (int signo,void (*func)(int)))(int);然后构造sigaction结构:将sa_handler成员设置成传进来的参数func;
7到16行设置SA_RESTART标志。这个标志会在后面介绍,这里首先把设置SA_START标志注释掉。
最后调用sigaction函数,设置对信号的处理方法。
2、处理SIGCHLD信号
既然通过调用sigaction函数可以设置对信号的处理,那么对于这里的具体问题怎么做呢?这里要处理僵尸子进程,也就是SIGCHLD信号,那么signal的第一个参数有了,就是SIGCHLD,缺少的是第二个参数,即信号处理函数。
下面是我们的信号处理函数:
void sig_chld(int signo) { pid_t pid; int stat; pid=wait(&stat); printf("child %d terminated. ",pid); return; }函数简单的调用wait函数来处理终止的子进程。下面是wait函数的定义,它包含在<sys/wait.h>头文件中:
pid_t wait(int *statloc);函数返回两个值:已终止的进程的进程ID,以及通过statloc指针返回的子进程终止状态(一个整数)。如果调用wait的进程没有已终止的子进程,不过有一个或多个子进程在运行,那么函数将阻塞到现有子进程中第一个终止为止。
现在有了信号处理函数,接下来就是如何调用,在服务器程序中的listen调用之后调用signal函数:
signal(SIGCHLD,sig_chld);下面就是完整的服务器程序:
#include <sys/wait.h> #include <sys/socket.h> #include <errno.h> #include <string.h> #include <strings.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdio.h> #include <time.h> #include <unistd.h> #define MAXLINE 1024 typedef void sigfunc(int); void str_echo(int sockfd); void sig_chld(int signo); sigfunc *signal(int signo,sigfunc *func); int main(int argc,char *argv[]) { int listenfd; struct sockaddr_in servaddr; char buff[MAXLINE]; time_t ticks; if((listenfd=socket(AF_INET,SOCK_STREAM,0))<0) { printf("socket error "); return 0; } bzero(&servaddr,sizeof(servaddr)); servaddr.sin_family=AF_INET; servaddr.sin_port=htons(5000); servaddr.sin_addr.s_addr=htonl(INADDR_ANY); if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0) { printf("bind error "); return 0; } if(listen(listenfd,5)<0) { printf("listen error "); return 0; } signal(SIGCHLD,sig_chld); int connfd; socklen_t len; struct sockaddr_in cliaddr; pid_t pid; for(;;) { len=sizeof(cliaddr); if((connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&len))<0) { if(errno==EINTR) { printf("Interrupted system call but continue "); continue; } else { printf("accept error "); return 0; } } if((pid=fork())==0) { if(close(listenfd)<0) { printf("close listenfd error "); return 0; } printf("[PID]%ld Receive a connection from:%s.%d ",(long)getpid(),inet_ntop(AF_INET,&cliaddr.sin_addr,buff,sizeof(buff)),ntohs(cliaddr.sin_port)); str_echo(connfd); if(close(connfd)<0) { printf("close child connfd error "); return 0; } return 0; } if(close(connfd)<0) { printf("close parent connfd error "); return 0; } } } void str_echo(int sockfd) { ssize_t n; char buf[MAXLINE]; again: while((n=read(sockfd,buf,MAXLINE))>0) write(sockfd,buf,n); if(n<0&&errno==EINTR) goto again; else if(n<0) { printf("read error "); return; } } void sig_chld(int signo) { pid_t pid; int stat; pid=wait(&stat); printf("child %d terminated. ",pid); return; } 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; #endif }else{ #ifdef SA_RESTART act.sa_flags|=SA_RESTART; #endif } if(sigaction(signo,&act,&oact)<0) return(SIG_ERR); return(oact.sa_handler); }下面是执行过程:
(1)启动服务器:
(2)客户发起连接并输入:
(3)然后客户输入Ctrl+C终止;
(4)服务器:
可以看到,服务器端正确的终止了子进程。通过ps命令查看是否有僵尸进程:
没有,说明对SIGCHLD信号的处理正确。
不过,这里还有个问题,上面服务器父进程调用signal函数正确处理了子进程之后貌似有什么输出:
Interrupted system call but continue
查看源程序可以知道,问题出这里:
for(;;) { len=sizeof(cliaddr); if((connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&len))<0) { if(errno==EINTR) { printf("Interrupted system call but continue "); continue; } else { printf("accept error "); return 0; } }是父进程accept函数中出现了EINTR,这是啥?
2、处理被中断的系统调用
来看看客户键入Ctrl+C来终止输入时发生了什么:
(1)客户TCP发送一个FIN给服务器,服务器响应以一个ACK;
(2)收到客户的FIN导致服务器TCP递送一个EOF给子进程阻塞中的readline,从而子进程终止;
(3)子进程终止后内核向父进程发送一个SIGCHLD信号,父进程阻塞于accept调用。sig_chld函数执行,wait调用取到子进程的进程ID和终止状态,然后printf,返回;
(4)accept函数是一个慢系统调用(slow system call),父进程阻塞在accept时捕获SIGCHLD信号,内核就会使accept返回一个EINTR错误(被中断的系统调用)。
这样,函数知道了出现EINTR,通过continue来重新进入for循环,相当于重新启动。
不过,在signal函数中可以设置SA_RESTART标志可以使被中断的系统调用自动重启而不用使用continue。
这里有如下结论:
(1)慢系统调用可能永远阻塞;
(2)当阻塞于某个慢系统调用的一个进程捕获某个信号且响应信号处理函数返回时,该系统调用可能返回EINTR错误;
(3)当出现EINTR错误时,有的系统内核自动重启被中断的系统调用,有的不重启。应该对这个错误有所准备;
3、还有什么?
当然还有一个问题。上面介绍wait函数时,说到如果一个进程中有多个子进程在执行,那么wait函数将阻塞到第一个子进程终止时为止。如果有多个子进程终止而成为僵尸进程,那么wait只能处理一个。也就是说,信号处理函数没能完全处理僵尸进程。
真的是这样么?
我们改变一下客户程序,让这个客户程序与服务器建立5个连接:
int main(int argc,char *argv[]) { int sockfd[5]; char recvline[MAXLINE]; if(argc!=2||strcmp(argv[1],"--help")==0) { printf("Usage:%s <IPaddress> ",argv[0]); return 0; } int i; for(i=0;i<5;i++) { if((sockfd[i]=socket(AF_INET,SOCK_STREAM,0))<0) { printf("socket error "); return 0; } struct sockaddr_in servaddr; bzero(&servaddr,sizeof(servaddr)); servaddr.sin_family=AF_INET; servaddr.sin_port=htons(5000); if(inet_pton(AF_INET,argv[1],&servaddr.sin_addr)<=0) { printf("inet_pton error for %s ",argv[1]); return 0; } if(connect(sockfd[i],(struct sockaddr*)&servaddr,sizeof(servaddr))<0) { printf("connect error "); return 0; } } str_cli(stdin,sockfd[0]); return 0; }这样,可以建立5个连接:
当客户终止时,所有打开的描述符由内核自动关闭,并且5个连接基本上同时终止,这引发了5个FIN,反过来时服务器的5个子进程基本上同时终止,导致差不多同时有5个SIGCHLD信号递交给父进程:
看看运行的怎么样:
(1)打开服务器并使客户连接,一共有5个,其中一个输入字符后终止:
(2)服务器终止了4个子进程:
(3)使用ps命令查看僵尸进程:
有一个进程ID是3625的子进程没有被处理。
严重的是,父进程处理子进程不确定:
这一次,5个子进程全部被终止,没有僵尸进程:
这是由于客户FIN到达主机的时间不同。
解决的办法是调用waitpid函数,函数定义如下:
pid_t waitpid(pid_t,int *statloc,int options);pid参数指定等待的进程ID,如果是-1表示第一个终止的子进程。options可以添加附加选项,这里使用WNOHANG,告诉内核在没有已终止子进程时不要阻塞。
下面使用waitpid函数改进sig_chld函数:
void sig_chld(int signo) { pid_t pid; int stat; while((pid=waitpid(-1,&stat,WNOHANG))>0) printf("child %d terminated. ",pid); return; }结果如下:
(1)打开服务器,客户建立连接并输入:
(2)客户终止后,服务器成功终止5个子进程:
(3)使用ps并没有发现僵尸进程:
4、总结一下?
(1)当fork子进程时,必须捕获SIGCHLD信号;
(2)当捕获信号时,必须处理被中断的系统调用;
(3)SIGCHLD的信号处理函数必须正确编写,应使用waitpid函数来避免留下僵尸进程。