//--------------------------------------------------1.引出信号----------------------------------------------------
在服务器子进程终止时,内核给父进程发送一个SIGCHILD信号,但是若没有在代码中捕获该信号,而该信号的默认行为是被忽略,既然父进程未加处理,子进程于是进入僵死状态,可以通过ps命令验证。
//--------------------------------------------------2.信号定义----------------------------------------------------
信号(signal)就是告知某个进程发生了某个事件的通知,有时也称为软件中断(software interrupt),信号通常是异步发生的,也就是说进程预先不知道信号的准确发生时刻。
信号可以由一个进程发给另一个进程(或自身)或者由内核发给某个进程,比如上面提到的SIGCHILD信号就是由内核在任何一个进程终止时发给它的父进程的一个信号。
//--------------------------------------------------3.信号捕获-----------------------------------------------------
贴上一段作者封装的通篇使用的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; /* 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);
}
其第一个参数是信号名,第二个参数是或为指向函数的指针,或为常值SIG_IGN或SIG_DEF.
1.设置处理函数
sigaction 结构的sa_handler成员被置为func参数
2.设置处理函数的信号掩码
POSIX允许我们指定这样一组信号,它们在信号处理函数被调用时阻塞。任何阻塞的信号都不能递交给进程,我们把sa_mask 成员设置为空集,意味着在该信号处理函数运行期间不阻塞额外的信号,但是POSIX保证被捕获的信号在其信号处理函数运行期间总是阻塞的。
3.设置SA_RESTART标志
此标志是可选的,若设置,由相应信号中端的系统调用将由内核自动重启。如果被捕获的信号不是SIGALARM且SA_RESTART有定义,我们就设置该标志。(对SIGALARM进行特殊处理的原因在于:产生该信号的目的通常是为IO操作设置超时,这种情况下我们希望受阻塞的系统调用被该信号中断掉)
4.调用sigaction函数,并将相应信号的旧行为作为signal函数的返回值。
// -------------------------------------------------4.POSIX信号语义-----------------------------------------------------------
1.将sa_mask置为空集,意味着在该信号处理函数运行期间,除了被捕获的信号外,没有额外的信号被阻塞。
2.如果一个信号在被阻塞期间产生了一次或多次,那么该信号被阻塞之后通常只递交一次,也就是说Unix信号默认是不排队的。
3.利用sigprocmask函数选择性地阻塞或解阻塞一组信号是可能的,这使得我们可以做到在一段临界区代码执行期间,防止捕获某些信号,以此保护这段代码。
//------------------------------------------------5.为什么要设计出僵死进程--------------------------------------------------
设置僵死(zombie)状态的目的是维护子进程的信息,以便父进程子以后某个时候获取,包括子进程的进程id,终止状态,以及资源利用信息(CPU时间,内存使用量等等),如果一个进程终止,而该进程有子进程处于僵死状态,那么它的所有僵死子进程的父进程ID将被重置为1(inti进程),继承这些子进程的init进程将会清理它们,也就是说init进程将wait它们,从而去除它们的僵死状态。
//--------------------------------------------------------6.处理僵死进程----------------------------------------------------------
先看一段看似完美的简单并发服务器例子
#include "unp.h"
int
main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
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(SERV_PORT);
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
for ( ; ; ) {
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
if ( (childpid = Fork()) == 0) { /* child process */
Close(listenfd); /* close listening socket */
str_echo(connfd); /* process the request */
exit(0);
}
Close(connfd); /* parent closes connected socket */
}
}
但是却忽略了僵死进程的处理,而我们不愿意留存僵死进程,他们占用内核中的空间,最终可能导致我们耗尽进程资源。无论何时,我们fork子进程都得wait他们,以防止他们变成僵死进程,为此我们建立一个俘获SIGCHID信号的信号处理函数,在函数体中我们调用wait。所以之前的简单模型,是不完整的,应该在listen调用之后(因为我们必须在fork第一个子进程之前完成,且只能调用一次),增加如下调用
signal(SIGCHID, sig_chld);
我们就建立了该信号处理函数。
sig_chld的定义如下:
#include "unp.h"
void sig_chld(int signo)
{
pid_t pid;
int stat;
pid = wait(&stat);
printf("child %d terminate\n", pid); // 信号处理函数里调用标准io库函数并不合适
return;
}
//----------------------------------------------7.引出被中断的系统调用------------------------------------------------------------
有了上述的处理僵死进程,看似就完美了吧,其实还没有,原因步骤如下:
当客户键入EOF来终止客户,客户TCP发送一个FIN给服务器,服务器TCP响应以一个ACK,收到客户的FIN导致服务器TCP递送一个EOF字符给子进程阻塞中的readline,从而子进程终止。
当SIGCHLD信号递交时,父进程阻塞于accept调用,sig_chld函数(信号处理函数)执行,其wait调用获取到子进程的PID和终止状态,printf后返回。
既然该信号是在父进程阻塞于慢系统调用accept时由父进程捕获的,内核就会使accept返回一个EINTR错误(被中端断的系统调用)而父进程不处理该错误,于是中止。
所以说在编写和捕获信号有关的网络程序时,我们的工作可不简单了,必须认清楚被中断的系统调用且要处理它们。
但是不同系统内核可能自动重启被中断的系统调用,或者不自动重启,所以作者自己定义了自己的signal函数(看第三条),贯穿全书使用,以应对不同os之间的这个潜在问题。
//-------------------------------------------------8.处理被中断的系统调用------------------------------------------------------
当阻塞与某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误。
即便有些系统会自动重启某些被中端的系统调用,不过为了便于移植程序,当我们编写捕获信号的程序时(多数并发服务U启捕获SIGCHLD),我们必须对慢系统调用返回EINTR有所准备。即便某个实现支持SA_RESTART标志,也并非所有被中断的系统调用都可以自动重启,距离来说,大多数源自Berkley的实现从不自动重启select,其中有些实现从不重启accept和recvfrom。
所以上面的服务器程序还是得改,为了处理被中断的accpet
for ( ; ; )
{
clilen = sizeof(cliaddr);
if ( (connfd = Accept(listenfd, (SA *) &cliaddr, &clilen)) < 0 )
{
if(errno == EINTR)
continue; // 自己来重启
else
err_sys("accept error");
}
if ( (childpid = Fork()) == 0) { /* child process */
Close(listenfd); /* close listening socket */
str_echo(connfd); /* process the request */
exit(0);
}
Close(connfd); /* parent closes connected socket */
}
这段代码所做的事情就是自己重启被中断的系统调用,对于accpet以及诸如read,write,select,open之类的函数来说是合适的,不过connect函数不能重启,如果它返回EINTR,那么我们就不能再次重启它,否则立即返回一个错误。
// -------------------------------------------------9.引出wait和waitpid的学习 ---------------------------------------------------
现在似乎比较完善了,但是还是会有问题,因为我们用的是wait(),建立一个信号处理函数并在其中调用wait并不足以防止出现僵死进程。假如多个客户同时断开连接,那么服务器的5个子进程几乎同一时刻终止,又导致差不多在同一时刻有多个SIGCHLD信号递交给父进程(造成同一信号多个实例的递交问题),还是会残留僵死进程得不到释放。
作者的例子里所有5个信号都在信号处理函数执行前产生,而信号处理函数只执行一次,因为unix信号一般不排队,留下多少僵死进程取决于FIN到达服务器主机的时机。
正确的解决办法是调用waitpid取代wait
#include “unp.h”
void sig_chld(int signo)
{
pid_t pid;
int stat;
while( (pid = waitid(-1, &stat, WNOHANG)) > 0 )
{
printf("child %d terminate \n", pid);
}
return;
}
可以看到,在一个循环内调用waitpid,以获取所有已终止子进程的状态,必须制定WNHANG选项,它告知waitpid在有尚未终止的子进程在运行时不要阻塞。为什么不用wait的原因就在这,不能再循环内调用wait,因为没有办法防止wait在正在运行的子进程尚有未终止时阻塞。
所以服务器代码的最终正确版本是如下(信号处理采用上面的sig_chld):
#include "unp.h"
int
main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
void sig_chld(int);
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(SERV_PORT);
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
Signal(SIGCHLD, sig_chld); /* must call waitpid() */
for ( ; ; ) {
clilen = sizeof(cliaddr);
if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
if (errno == EINTR)
continue; /* back to for() */
else
err_sys("accept error");
}
if ( (childpid = Fork()) == 0) { /* child process */
Close(listenfd); /* close listening socket */
str_echo(connfd); /* process the request */
exit(0);
}
Close(connfd); /* parent closes connected socket */
}
}