简介
信号是操作系统提供了一种处理异步事件的方法,每个用户进程都会接受到信号,并且可以注册信号处理函数,当信号到来时会执行注册的处理函数。
当信号到来是可以告诉内核使用如下三种方式处理对应的信号:
- 忽略该信号。大多数信号都可以做此操作,除了SIGKILL和SIGSTOP这两个信号,代码中信号处理函数注册为SIG_IGN可以忽略信号。
- 捕获信号。注册信号处理函数为自己想要执行的用户函数,同样的,SIGKILL和SIGSTOP信号不能捕获。
- 执行默认动作。不同的信号具有不同默认动作,不过大部分都是结束该进程,代码中信号处理函数注册为SIG_DFL可以恢复默认动作。
本文只介绍一些关键的常用信号,其他信号请参考其他文档:
SIGABRT: 异常终止信号,默认动作:终止+core dump
SIGALRM: 定时器超时信号,默认动作:终止
SIGCHLD: 子进程终止或者状态改变,默认动作:忽略
SIGHUP: 终端连接断开,只会发送给拥有控制终端的控制进程,一般是会话主进程,默认动作:终止
SIGINT: 终端中断符(ctrl-c),发送给整个前台进程组,默认动作:终止
SIGQUIT: 终端退出符(ctrl-\),发送给整个前台进程组,默认动作:终止+core dump
SIGTSTOP: 终端停止(挂起)信号(ctrl-z),发送给整个前台进程组,默认动作:停止进程
SIGIO: 异步I/O信号,默认动作:系统差异,可能终止也可能是忽略
SIGKILL: 杀死信号,默认动作:终止
SIGPIPE: 写入无读进程的管道,默认动作:终止
SIGSEGV: 无效内存访问(段错误),默认动作:终止+core dump
SIGSTOP: 停止信号,默认动作:停止进程
SIGTERM: 终止信号,默认动作:终止
SIGURG: 进程紧急情况信号,用于socket接收带外数据,默认动作:忽略
SIGUSR1: 用户定义信号,默认动作:终止
SIGUSR2: 用户定义信号,默认动作:终止
关于信号的继承特性:
- 当调用fork创建一个新进程时,子进程会继承父进程的信号处理方式。
- 当子进程使用exec替换一个新程序时,原先设置为要捕获的信号都更改为默认动作,其他信号保持不变。
signal函数
#include <signal.h>
void ( *signal(int signum, void (*handler)(int)) ) (int);
这个函数声明很难以理解,实际上它等于如下的定义方式:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
这是最简单的处理信号机制的接口,根据Linux、Unix的不同存在很多个实现版本,在一些版本上是按照不可靠信号语义实现,因此它可能是不可靠的并且存在移植性的问题,所以应该尽量避免使用它,而改用sigaction函数。
这个接口在一些平台上可能是不可靠的,所谓不可靠,那就是说明有一些信号可能会被丢失,原因是该信号处理函数每次设置只会生效一次,一般的做法是在信号处理函数中重新再设置一遍,但是两个设置之间并不是原子操作,所以会产生丢失现象。
它接收两个参数,第一个参数是信号number,第二个是信号处理回调函数。返回一个函数指针,该指针指向在此之前的上一个信号处理函数。如果出错返回SIG_ERR。
sigaction函数
#include <signal.h>
int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact);
返回值:成功返回0,错误返回-1
和signal功能一样,设置信号处理函数,但是它属于可靠信号的实现,建议使用它来设置信号处理函数。
sigprocmask函数
#include <signal.h>
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
返回值:成功返回0,错误返回-1
该函数可以设置、清除、查看信号的屏蔽字。how可选三个值:
SIG_BLOCK: 进程屏蔽字是set和当前屏蔽字的并集。
SIG_UNBLOCK: 解除对set中信号的屏蔽。
SIG_SETMASK: 更新进程信号屏蔽字为set。
set为要设置的屏蔽字信号集合,oset为设置前的进程屏蔽字集合。
sigpending函数
#include <signal.h>
int sigpending(sigset_t *set);
返回值:成功返回0,错误返回-1
该函数返回信号阻塞期间产生的信号集,也叫挂起状态的信号集。比如一个信号被设置为阻塞,在阻塞期间,内核依然产生了该信号,那么它就属于被挂起的信号,当解除阻塞后该挂起信号是会重新送达给进程的。
sigsuspend函数
#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
返回值:成功返回0,错误返回-1
该函数阻塞当前进程等待信号的到来,和pause不同,它会执行如下三步:
- 修改当前信号屏蔽字,按照sigmask阻塞信号,只等待其他信号
- 阻塞等待其他信号的到来
- 恢复当前信号屏蔽字为旧值
之所以增加此API,是因为这三步可以认为是原子操作,不用担心中间存在不安全的时间窗口,到时信号丢失或者其他异常。
发送信号函数
#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
#include <stdlib.h>
void abort(void);
kill可以向指定进程发送一个信号,raise只能向本进程发送一个信号。
alarm函数会设定定时器,当时间到期后向本进程发送一个SIGALRM信号。
abort异常终止函数,向本进程发送SIGABRT异常终止信号。
pause函数
#include <unistd.h>
int pause(void);
阻塞进程等待一个信号的到来,只有执行了相应的信号处理程序后才会从pause返回。返回值是-1, errno设置为EINTR。
被信号中断的系统调用
一个低速系统调用可能会阻塞,如果再阻塞期间,进程捕获到了一个信号,则该系统调用会被中断不再继续执行,系统调用返回错误,errno被设置为EINTR。针对这种中断,需要软件进行处理,以重启系统调用恢复执行。
比如一个典型的read操作:
ssize_t
readn(int fd, void *vptr, size_t n)
{
size_t nleft;
ssize_t nread;
char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ( (nread = read(fd, ptr, nleft)) < 0) {
if (errno == EINTR)
nread = 0;/* and call read() again */
else
return(-1);
} else if (nread == 0)
break; /* EOF */
nleft -= nread;
ptr += nread;
}
return(n - nleft); /* return >= 0 */
}
针对这种特定场景,有些系统支持自动重启系统调用,并且可以针对每个信号配置是否允许重启系统调用:
int set_signal(int signo, SIG_FUNC 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 { //Only SIGALRM can interrupt the system call, otherwise we restart system call
#ifdef SA_RESTART
act.sa_flags |= SA_RESTART; /* SVR4, 44BSD */
#endif
}
if (sigaction(signo, &act, &oact) < 0)
return(-1);
return 0;
}
为了软件的可移植性,需要使用#ifdef来判断平台是否支持对应的属性。
示例
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>
#include <string.h>
int set_signal(int signo, void func(int signo))
{
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) {
printf("sigaction error,%s\n",strerror(errno));
return 1;
}
return 0;
}
void signal_handler(int signo)
{
switch (signo) {
case SIGALRM:
printf("SIGALRM triggered\n");
break;
case SIGINT:
printf("SIGINT triggered\n");
break;
case SIGQUIT:
printf("SIGQUIT triggered\n");
break;
case SIGTERM:
printf("process terminated!!!\n");
break;
default:
printf("Do nothing for signal:%d\n", signo);
break;
}
}
int sigprocmask_test(void)
{
sigset_t set, oset, pendset;
if (set_signal(SIGINT, signal_handler) < 0) {
printf("set signal error, exit\n");
exit(1);
}
if (set_signal(SIGQUIT, signal_handler) < 0) {
printf("set signal error, exit\n");
exit(1);
}
sigemptyset(&set);
sigaddset(&set, SIGINT);
if (sigprocmask(SIG_BLOCK, &set, &oset) < 0) {
printf("sigprocmask error, exit\n");
exit(1);
}
printf("blocked signal SIGINT, please try ctrl-c!\n");
printf("use ctrl-\\ to wake up process using SIGQUIT\n");
printf("wait for a signal by pause...\n");
pause();
if (sigpending(&pendset) < 0) {
printf("sigpending error\n");
exit(1);
}
if (sigismember(&pendset, SIGINT)) {
printf("SIGINT is pending\n");
}
if (sigprocmask(SIG_SETMASK, &oset, NULL) < 0) {
printf("sigprocmask error, exit\n");
exit(1);
}
printf("reset signal mask to default, please try ctrl-c again!\n");
/*
* during reset and pause, it has a time window here which is unsafe
* this time window may make the pause wait for another signal to wake up
* but not the pending one
*/
pause();
}
int sigsuspend_test(void)
{
sigset_t set, oset, pendset, waitset;
if (set_signal(SIGINT, signal_handler) < 0) {
printf("set signal error, exit\n");
exit(1);
}
if (set_signal(SIGQUIT, signal_handler) < 0) {
printf("set signal error, exit\n");
exit(1);
}
sigemptyset(&set);
sigaddset(&set, SIGINT);
if (sigprocmask(SIG_BLOCK, &set, &oset) < 0) {
printf("sigprocmask error, exit\n");
exit(1);
}
printf("blocked signal SIGINT, please try ctrl-c!\n");
printf("use ctrl-\\ to wake up process using SIGQUIT\n");
printf("wait for a signal by pause...\n");
pause();
if (sigpending(&pendset) < 0) {
printf("sigpending error\n");
exit(1);
}
if (sigismember(&pendset, SIGINT)) {
printf("SIGINT is pending\n");
}
printf("sigsuspend wait for a signal without SIGQUIT\n");
sigemptyset(&waitset);
sigaddset(&waitset, SIGQUIT);
//suspend return -1 as success return
if (sigsuspend(&waitset) != -1) {
printf("sigsuspend error, exit\n");
exit(1);
}
printf("sigsuspend get an signal and returned\n");
printf("sigsuspend should reset signal mask to the status before sigsuspend, please try ctrl-c again!\n");
pause();
}
/* alarm test */
int alarm_test(void)
{
char buf[20];
int n;
if (set_signal(SIGALRM, signal_handler) < 0) {
printf("set signan error, exit\n");
exit(1);
}
printf("alarm timer will expire 10 seconds later\n");
alarm(10);
printf("read blockly from stdin for 20 characters, timeout 10 seconds\n");
n = read(STDIN_FILENO, buf, 20);
if (n < 0) {
if (errno == EINTR)
printf("read interrupted by signal, exit\n");
exit(1);
}
printf("read returned successfully, cancel the alarm timer\n");
alarm(0);
write(STDOUT_FILENO, buf, n);
exit(0);
}
void usage(int argc, char *argv[])
{
printf("Usage:\n");
printf("\t %s [-a] \"alarm test\"\n", argv[0]);
printf("\t %s [-m] \"sigprocmask test\"\n", argv[0]);
printf("\t %s [-s] \"sigsuspend test\"\n", argv[0]);
}
int main(int argc, char *argv[])
{
int opt, ret, i;
pid_t pid;
if (argc == 1) {
usage(argc, argv);
exit(1);
}
while((opt = getopt(argc, argv, "sam")) != -1) {
switch(opt) {
case 'a':
alarm_test();
break;
case 'm':
sigprocmask_test();
break;
case 's':
sigsuspend_test();
break;
default:
printf("unknown option:%c\n", optopt);
usage(argc, argv);
break;
}
}
return 0;
}