signal与sigaction函数详解

概述

信号是事件发生时对进程的通知机制。也可称为软件中断,信号与硬件中断的相似之处在于,都打断了程序执行的正常流程。大多数情况下无法预测信号到达的时间。
进程可以向另一进程发送信号,也可以向自身发送信号,多数信号都是源于内核,引发内核向目的进程发送信号的各类事件如下:

  • 硬件发生异常。eg:0除,引用无法访问的内存区域
  • 用户键入能产生信号的终端字符。eg:中断字符Ctrl-C,暂停字符Ctrl-Z
  • 发生软件事件。eg:定时器超时,调整终端窗口大小,进程执行的CPU时间超限,或者进程的子进程退出。

信号到达后,进程视具体信号的执行函数进行如下默认操作:

  • 忽略信号
  • 终止进程
  • 产生核心转储文件,同时进程终止。(核心转储文件包含对进程虚拟内存的镜像,可将其加载到调试器中以检查进程终止时的状态)
  • 停止进程
  • 与之前暂停后再恢复进程的执行

除了执行默认的操作,也可以人为的改变信号到达时的响应行为。也称之为信号的处置设置。信号处置设置可以为如下之一:

  • 采取默认行为
  • 忽略信号
  • 执行信号处理器程序

本文主要对产生信号处理器程序的两个系统调用函数予以说明与解释。
UNIX系统提供了两种方法来改变信号处置:signal(),和sigaction()。signal系统调用是设置信号处置的原始API,所提供的接口比sigaction简单,简单的结果就是其可移植性差。故此,sigaction是建立信号处理器的首选API。

signal()函数

1
2
#include<signal.h>
void (*signal(int sig,void(*handler)(int)) ) (int);

有必要对该函数做一些解释(反正我第一看的时候有点恶心…)。从外往里看,整体是一个指向函数signal的指针,这个函数有一个int型的形参,无返回值,signal函数有一个int型的参数sig,标识希望修改处置的信号编号,第二个参数(又)是一个指向handler函数的指针,handler函数有一个int型的形参,无返回值,handler用于标识信号抵达时所调用函数的地址。信号处理器函数一般具有以下格式:

1
2
3
void handler(int sig){
/*code*/
}

handler位置可以用SIG_DFL或SIG_IGN替换,表示信号处置重置为默认值和忽略该信号。
signal的返回值是之前的信号处置,和handler一样,是一枚指向带有一个int型参数,无返回值的函数的指针。失败则返回SIG_ERR。

使用signal()将无法在不改变信号处置的同时,还能获取当前的信号处置,但是sigaction()可以做到。
一般对signal的原型做如下代换:

1
2
typedef void (*sighandler_t)(int);
sighandler_t signal(int sig,sighandler_t handler);

下面附一段最简单的signal示例程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<signal.h>
#include<stdlib.h>
#include<unistd.h>
void sigHandler(int sig){
printf("ouch\n");
}
int main(int argc,char *argv[]){
int j;
if(signal(SIGINT,sigHandler) == SIG_ERR){
exit(1); //signal调用失败
}
for(j = 0;;j++){
printf("%d\n",j);
sleep(1);
}
}

程序解释:为SIGINT信号建立的一个信号处理器函数,当用户输入Ctrl-C时触发该信号,默认操作是终止进程,此处将其改为打印一行输出,然后返回。在该程序中,如果设置一个指向带有一个int形参无返回值的函数的指针oldHandler,并将第一次调用signal时的返回值赋予它,那么在后期可以执行signal(SIGINT,oldHandler);来恢复对Ctrl-C信号的默认行为,即终止进程。

sigaction()函数

sigaction()的用法比signal更为复杂,但是作为回报,它更具有灵活性,移植性也更好。尤其是sigaction()允许在获取信号处置的同时无需将其改变,并且还可以设置各种属性对调用信号处理器程序时的行为施以更精确的控制。

1
2
3
#include<signal.h>
int sigaction(int sig,const struct sigaction *act,struct sigaction *oldact);
//成功返回0,失败返回-1

sig参数标识想要获取或者改变的信号编号,该参数可以是除去SIGKILL和SIGSTOP之外的任何信号(因为这两个信号的处理程序不能被修改,不能被阻塞)。act参数是指向描述信号新处理器的数据结构的指针,如果仅对信号的现有处置感谢趣,可以将该参数设为NULL。oldact参数是指向同一结构类型的指针,用以返回之前的信号处置的相关信息如果无意获取此类信息,可以将其设置为NULL。act和oldact所指的数据结构信息如下:

1
2
3
4
5
6
struct sigaction{
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};

sa_handler字段对应于signal的handler参数,指向信号处理器函数的地址,也可是是常量SIG_IGN,SIG_DFL之一,当sa_handler不是这两个常数时,才会对sa_mask和sa_flags进行处理。
sa_mask定义一组信号,在调用处理器函数时,系统将阻塞这组信号。在调用处理器函数前,系统将该进程信号掩码中没有这组信号中的信号添加到进程信号掩码中,调用结束时删除这些信号,即不允许这组信号中断处理器程序的执行,此外,引发对处理器程序的调用的信号也会被添加到进程信号掩码中。
sa_flags字段是一个位掩码,指定用于控制信号处理过程中的各种选项,使用时可以查阅手册。sa_restorer不适用于应用程序,在此不加赘述。
同样,附一段简单的sigaction()使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<signal.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
void handler(int sig){
printf("ouch\n"); //不建议使用
}
int main(int argc,char *argv[]){
struct sigaction sa;
int j;
sigemptyset(&sa.sa_mask); //信号集的一种初始化方式,此处为清空
sa.sa_flags = 0;
sa.sa_handler = handler;
if(sigaction(SIGINT,&sa,NULL) == -1) //不关心原来的处理器程序
exit(1);
for(j;;j++){
printf("%d\n",j);
sleep(1);
}

}


参考资料:
《UNIX/linux系统编程手册》(tlpi)
《UNIX环境高级编程 3th》(APUE)