信号的概念
信号就是告知某个进程发生了某件事的通知,也可以称为软件中断。信号一般是异步的,进程预先无法知道信号发生的时间。
信号的发送方:
可以是一个进程发给另一个进程;
也可以是内核发送给某个进程。
UNIX系统信号的类型见UNIX环境高级编程P251.
信号的处置
每个信号都有与之关联的处置,也被称为行为(action)。可以通过sigaction函数,来设定对一个特定信号的处置方式。信号的处置方式一般有三种:
(1)忽略信号
将某种信号的处置设定为SIG_IGN。大多数信号采用这种方式进行处理。但是有两种信号绝不能被忽略:SIGKILL和SIGSTOP。这两种信号不能被忽略的原因是:它们向内核和超级用户提供了使进程终止或停止的可靠方法。
(2)捕获信号
定义某个函数,只要特定信号发生,就调用该函数进行相应处理,该函数执行对这种事件的处理。这样的函数被称为信号处理函数(signal handler),这种行为就是捕获信号。SIGKILL和SIGSTOP信号不能被捕获。
(3)默认处理
信号处置方式为SIG_DFL。每一种信号都有对应的系统默认处理方式,具体方式见UNIX环境高级编程P251页。大多数信号的系统默认处理是终止进程,其中某些信号在终止时在当前工作目录产生一个进程的核心映像(core image,也成为内存映像)。个别信号的处置方式是默认。
signal函数
(1)signal函数接口
signal函数接口是UNIX信号处理机制中最简单的接口。
#include<signal.h>
void (*singal(int signo, void (*func)(int)))(int);
函数原型说明,signal函数有两个参数,第一个是指定的信号名;第二个是常量SIG_IGN、常量SIG_DFL或某个函数指针,该指针指向接到此信号后要调用的函数。指向SIG_IGN则表示忽略此信号(SIGKLL和SIGSTOP不能忽略);指向SIG_DFL则表示执行系统默认工作。
signal函数的返回较为复杂:
它返回一个指向某个函数的指针。
void (*f)(int)
该函数就是信号的处理函数,是一个无返回值且有一个参数为整数的函数。
我们可以:
typedef void Sigfunc(int);
那么signal函数可以改写为:
Sigfunc *signal(int signo, Sigfunc *func);
因此在使用该函数时,我们只需要传入两个参数,一个信号编号,一个是指向某个信号处置函数的指针(或者直接传入函数名)。
(2)signal函数返回值
在signal.h文件中,定义了几个字符用于表示一般情况下的signal函数返回:
#define SIG_ERR (void (*)()) -1
#define SIG_DFL (void (*)()) 0
#define SIG_IGN (void (*)()) 1
可以用这三个定义值来表示signal函数的返回和第二个参数。
(3)Signal包裹函数
#tepyedef void Sinfunc(int);
Sigfunc * Signal(int signo, Sigfunc *func) {
Sigfunc *sigfunc;
if ( (sigfunc = signal(signo, func)) == SIG_ERR){
printf("signal error");
exit(1);
}
return(sigfunc);
}
包裹函数的目的在与出路调用singal函数时可能返回的错误SIG_ERR。
如果不实现包裹函数,我们对信号的处置方法不感兴趣,也可以直接用代码段包裹signal函数的调用:
if(signal(signo , func )==SIG_ERR){
printf("signal error");
exit(1);
}
处理SIGCHLD信号
signal函数的意义就在于指定我们的程序对特定信号的处置方式。
(1)sig_chld函数
在服务器子进程终止时,会给父进程发送一个SIGCHLD信号。该信号通知父进程,子进程的状态发生改变。该信号的默认行为是忽略,因此子进程进入僵死状态。我们可以给该信号设置处置方式,来处理僵死的子进程。处理僵死子进程的可移植方法,就是调用wait或者waitpid函数来进行处理。
使用signal函数时,我们可以定义自己的处置方法,在方法中调用wait函数。
void sig_chld(int signo){
pid_t pid;
int stat;
pid=wait(&stat);
printf(“child %d terminated\n”,pid);
return;
}
随后在服务器的父进程中调用signal函数:
if(signal(SIGCHLD , sig_chld )==SIG_ERR){
printf("signal error");
exit(1);
}
调用signal函数必须在fork第一个子进程时完成,且只能完成一次。
(2)处理被中断的系统调用
当SIGCHLD信号递交时,父进程正阻塞于accept调用。此时sig_chld函数执行。父进程捕获到该信号,内核会就会使accept返回一个EINTR错误(被中断的系统调用)。
这种调用被称为慢系统调用:当阻塞于某个慢系统调用的一个进程捕获到某个信号且相应的信号处理函数返回时,该系统调用可能会返回一个EINTR错误。
不同系统对于中断的响应不同。某些系统被中断则直接停止运行程序,accpet返回errno=EINTR;有些程序会自动重启被中断的系统调用。为了可移植性,我们需要统一处理被中断的系统调用。
处理中断:
for(;;){
clilen=sizeof(cliaddr);
if((connfd=accept(listenfd, (struct sockaddr *)&cliaddr, &clilen))<0){
if(errno=EINTR)
continue;
else{
printf(“accept调用发生错误”);
exit(1);
}
}
}
(3)wait函数
函数原型
#include<sys/wait.h>
pid_t wait(int *statloc);
作用:
在某进程中调用wait函数,如果该进程中有已经终止的子进程,则返回该子进程的进程ID。如果此时该进程没有已经终止的子进程,不过仍然有一个或多个子进程仍然在执行,那么wait将阻塞到现有子进程第一个终止为止。如果该进程没有子进程或者子进程已经结束,则wait函数立即返回-1.
参数
int *statloc:用于返回子进程状态的指针,指向一个整数。对于子进程来说,有三种状态:正常终止、由某个信号杀死、由作业控制停止。
返回值:
成功:子进程的进程ID号。
失败:返回-1
实例:
pid_t pid;
int stat;
pid=wait(&stat);
(4)waitpid函数
函数原型
#include<sys/wait.h>
pid_t waitpid(pid_t pid,int *statloc, int options);
作用:
waitpid函数可以指定等待的子进程。还可以指定函数是否阻塞。
参数:
pid_t pid:指定我们想要等待的进程ID:
大于0:指定回收的子进程ID;
等于-1:回收任意子进程,相当于wait
int statlic:用于返回该进程的结束状态
int opions:用于指定附加想。常用选项:
WNOHANG:非阻塞回收,若由pid指定的子进程未发生状态改变(没有结束),则waitpid()不阻塞,立即返回0
返回值:
成功:成功结束运行的子进程id
失败:返回-1
如果设置int options为WNOHANG:指定子进程未停止,则返回0.
(5)使用waitpid应对多个子进程僵死
假设主机同时与服务器的五个子进程通信,此时结束通信,那么会有五个SIGCHLD信号发送到父进程。如果父进程使用wait函数实现的sig_chld函数来处理SIGCHLD信号,那么五个子进程只有一个被执行wait函数,另外四个仍然处于僵死状态。
这是因为Unix信号不会排队,五个SIGCHLD都发生在函数调用前,而信号处理函数只执行一次。
为了应对这种情况,应该使用waitpid函数来代替wait函数,实现对这种信号的处置。此时我们将waitpid的参数设为WNOHANG,使它在没有子进程终止时立刻返回0。
void sig_chld(int signo){
pid_t pid;
int stat;
while((pid=waitpid(-1, &stat, WNOHANG)) > 0)
printf(“child %d 终止!”, pid);
return;
}
不能在循环内调用wait函数,因为没有办法防止wait函数在仍然有尚未终止的子进程时进入阻塞。
处理SIGPIPE信号
客户端与服务端通信时,服务端突然断开连接,此时服务端再收到客户端的请求时,会回复一个RST。当一个进程向某个已收到RST的套接字执行写操作时,内核会向该进程发出一个SIGPIPE信号。该信号的默认行为是终止进程,因此进程必须捕获它,以免不情愿地被终止。
不论是捕获该进程并由信号处理函数返回,还是直接忽略该信号,写操作都会返回EPIPE错误。
如果直接忽略该信号,并相应地处理EPIPE错误,则只需要直接将处置方式设置为SIG_IGN即可。