信号(signal)就是告知某个进程发生了某个事件的通知,有时也称软件中断(software interrupt)。注意:信号是异步发生的,也就是说进程预先并不知道信号的准确发生时刻。
查询僵死子进程的shell命令
ps -A -ostat,ppid,pid | grep -e '^[Zz]'
信号可以
1. 由一个进程发送给另外一个进程(包括自身)
2. 由内核发送给某个进程
每个信号都有一个与之关联的处置。当有特定信号发生的时候,特定的函数被调用,那么这个特定函数就称为这个特定信号的信号处理函数,调用这个函数的过程称为信号捕获过程。
signal(int signo, void *func)
signo是信号名,意为我们捕捉到的信号或者我们将要的处理的信号;func是指向函数的指针,意为我们将要处理signo信号的方式。第二个参数可以为常值SIG_IGN(忽略信号)或SIG_DFL(该信号的默认处置)。
子进程向父进程递交一个SIGCHLD信号,若父进程未加处理(即没有调用wait函数),子进程将会进入僵死状态。如果一个进程终止,且该进程有子进程处于僵死状态,那么它的所有僵死子进程的父进程ID将重置为1(init进程),init进程将调用wait函数清理这些僵死子进程。这意味着无论什么时候我们fork子进程后都得wait它们,以防它们变成僵尸进程。
在服务器程序中我们先调用listen函数将未连接套接字sockfd变为监听套接字listenfd之后,然后增加Signal(SIGCHLD, sig_chld);这个函数将会递交SIGCHLD信号并使得父进程阻塞与accept调用,然后sig_child(int signo)函数将会捕获到子进程的PID和终止状态,最后返回。这里在Signal函数中我们设置了SA_RESTART标志,是为了让内核能够自动重启被中断的系统调用。
慢系统调用函数,以accept为例,在服务器程序中,bind和listen执行完毕后,未连接套接字将会变成监听套接字,此时accept函数等待一个来自客户端的连接请求。若客户端一直不运行或是一直不执行connect(tcp三次握手)函数,那么accept函数将会一直阻塞,accept调用有可能不会返回已连接的套接字。
EINTR:标志被中断的系统调用。适用于慢系统调用的基本规则是:当阻塞与某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误。当我们编写捕获信号的程序时,我们必须对慢系统调用返回EINTR有所准备,即是说,我们需要重启被中断的系统调用,一般在for循环中利用continue来实现。以下为处理被中断的accept函数的操作:
//因为accept调用需要等待客户端程序的connect连接,这个操作可以处理
//被中断的accept调用
if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
if (errno == EINTR)
continue; /* back to for() */
else
err_sys("accept error");
}
当我们判断到这个系统调用为慢系统调用即errno = EINTR,则利用continue继续重启accept函数调用。不过有一个函数我们不能重启:connect,重启就报错。
调用的函数wait来处理已终止的子进程。比如子进程向父进程发送了一个SIGCHLD信号,此时父进程调用函数wait可以清理子进程;若父进程不调用wait函数,则会使子进程变为僵死子进程。
#include
pid_t wait(int *statloc); //&stat,地址
pid_t waitpid(pid_t pid, int *statloc, int options);
示例
int
main()
{
int i, sockfd[5];
struct sockaddr_in servaddr;
char *argv = "192.168.31.54";
for (i = 0; i < 5; i++) { //创建5个客户端进程
sockfd[i] = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
Inet_pton(AF_INET, argv, &servaddr.sin_addr);
Connect(sockfd[i], (SA *) &servaddr, sizeof(servaddr));
}
str_cli(stdin, sockfd[0]); /* do it all */
exit(0);
}
在运行了tcpserv04.c和tcpcli04.c之后,在客户端输入EOF标志,即是ctrl+d,所有打开的套接字描述符由内核自动关闭,且所有5个连接基本在同一时间终止,这就意味着同一时刻有5个SIGCHLD信号从子进程发送给父进程。因为客户端的每一次connect调用,都会使得服务器fork一个新的子进程,这里fork了5个子进程,但是信号处理函数wait只调用了一次,那么剩余的四个进程都将成为僵死进程。因此我们要修改之前的sig_chld函数为:
void
sig_chld(int signo)
{
pid_t pid;
int stat;
while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0)
printf("child %d terminated\n", pid);
return;
}
注意这里waitpid函数的WNOHANG选项,直译为不挂起,这里选择WNOHANG选项可以使得waitpid函数不被尚未终止的子进程阻塞。而wait函数在没有已终止的子进程时将会被阻塞。