信号是软件中断,提供了一种处理异步事件的方法。每个信号的名字都以SIG字符开头,为正整型常量,定义在<signal.h>头文件中(实际上,实现将信号定义在内核头文件中,<signal.h>又包含该内核头文件,如:Linux 3.2.0将信号定义在<bits/signum.h>中,FreeBSD 8.0将信号定义在<sys/signal.h>中)。
信号有3种处理方式:
(1) 忽略此信号。大多数信号都使用这种方式处理。但是有两种信号不能被忽略:SIGKILL和SIGSTOP。原因是这两种信号向内核和超级用户提供了使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号(如非法内存引用、除0错误等),那进程的运行行为是未定义的。
(2) 捕捉信号。通知内核在某种信号发生时,调用一个用户函数来处理该信号。同样地,不能捕捉SIGKILL和SIGSTOP信号。
(3) 执行系统默认动作。对大多数信号而言,系统默认动作就是终止该信号。
Linux系统信号列表如下(“终止 + core”表示在进程当前工作目录的core文件中复制了该进程的内存映像,大多数Unix系统调试程序使用core文件检查进程终止时的状态):
信号 |
说明 |
默认动作 |
SIGABRT |
异常终止(abort) |
终止 + core |
SIGALRM |
定时器超时(alarm) |
终止 |
SIGBUS |
硬件故障(内存故障) |
终止 + core |
SIGCHLD |
子进程状态改变 |
忽略 |
SIGCONT |
使暂停进程继续运行 |
继续/忽略 |
SIGEMT |
硬件故障 |
终止 + core |
SIGFPE |
算术运算异常(浮点溢出) |
终止 + core |
SIGHUP |
连接断开 |
终止 |
SIGILL |
非法硬件指令 |
终止 + core |
SIGINT |
终端中断符(Ctrl + C) |
终止 |
SIGIO |
异步I/O |
终止/忽略 |
SIGIOT |
硬件故障 |
终止 + core |
SIGKILL |
终止 |
终止 |
SIGPIPE |
写至无读进程的管道 |
终止 |
SIGPOLL |
轮询事件 |
终止 |
SIGPROF |
梗概时间超时(setitimer) |
终止 |
SIGPWR |
电源失效/重启动 |
终止/忽略 |
SIGQUIT |
终端退出 |
终止 + core |
SIGSEGV |
无效内存引用(如访问了一个未经初始化的指针) |
终止 + core |
SIGSTKFLT |
协处理器栈故障 |
终止 |
SIGSTOP |
停止进程 |
停止 |
SIGSYS |
无效系统调用 |
终止 + core |
SIGTREM |
kill发送的系统默认终止信号 |
终止 |
SIGTRAP |
硬件故障 |
终止 + core |
SIGTSTP |
终端停止(Ctrl + Z) |
停止 |
SIGTTIN |
后台读取控制终端 |
停止 |
SIGTTOU |
后台写入控制终端 |
停止 |
SIGURG |
紧急情况(套接字、带外数据) |
忽略 |
SIGUSR1 |
用户定义信号,可用于应用程序 |
终止 |
SIGUSR2 |
用户定义信号,可用于应用程序 |
终止 |
SIGVTALRM |
虚拟时间闹钟(setitimer) |
终止 |
SIGWINCH |
终端窗口大小改变 |
忽略 |
SIGXCPU |
超过CPU限制(setrlimit) |
终止 + core |
SIGXFSZ |
超过文件长度限制(setrlimit) |
终止 + core |
注: 在下列条件下不产生core文件: (1) 进程是设置用户ID的,而且当前用户并非程序文件的所有者; (2) 进程是设置组ID的,而且当前用户并非程序文件的组所有者; (3) 用户没有写当前工作目录的权限; (4) 文件已存在,而且用户对该文件没有写权限; (5) 文件太大(超过RLIMIT_CORE限制); |
函数signal
#include <signal.h> void (*signal(int signo, void (*func)(int))) (int); 返回值:成功,返回以前的信号处理函数;失败,返回SIG_ERR 说明: signo为信号名。 func为常量值SIG_IGN(忽略此信号)/SIG_DFL(执行系统默认动作)/(接收此信号要调用的函数地址)。 signal函数的返回值是一个函数地址,指向在此之前的信号处理函数,而func指向新的信号处理函数。
由于函数原型太过复杂,也可使用下面的定义方式: typedef void Sigfunc(int); Sigfunc* signal(int, Sigfunc*); |
[root@benxintuzi signal]# cat signal.c #include <signal.h> #include <stdio.h> static void sig_usr(int); int main(void) { if (signal(SIGUSR1, sig_usr) == SIG_ERR) printf("can't catch SIGUSR1\n"); if (signal(SIGUSR2, sig_usr) == SIG_ERR) printf("can't catch SIGUSR2\n"); for (; ;) pause(); return 0; } static void sig_usr(int signo) { if (signo == SIGUSR1) printf("received SIGUSR1\n"); else if (signo == SIGUSR2) printf("received SIGUSR2\n"); else printf("can't process signal %d\n", signo); } [root@benxintuzi signal]# gcc signal.c -o signal [root@benxintuzi signal]# ./signal & [1] 2389 [root@benxintuzi signal]# kill -USR1 2389 [root@benxintuzi signal]# received SIGUSR1 kill -USR2 2389 [root@benxintuzi signal]# received SIGUSR2
可再入函数
进程捕捉到信号并对其进行处理时,正常执行的指令序列就会被中断,首先需要执行信号处理程序,之后则应该接着执行之前未完成的指令序列。但是在信号处理程序中,并不能判断捕捉到信号时进程执行到什么地方,如果进程正在执行malloc,而此时由于捕捉到信号而插入信号处理函数也要调用malloc,此时,由于malloc通常会为它所分配的存储区维护一个链表,而插入信号处理函数时,该进程正在修改链表,那么结果是进程环境遭到破坏,丢失重要信息。
Single Unix Specification说明了在信号处理程序中保证调用安全的函数,这些函数是可再入的,称为异步信号安全(async-signal safe)函数。除了可再入外,在信号处理期间,它会阻塞任何引起不一致的信号发送。
一般不可再入函数有如下特点:
(1) 使用静态数据结构;
(2) 调用malloc或free;
(3) 属于标准I/O函数,因为很多标准I/O库都以不可再入的方式使用了全局数据结构。
可靠信号术语
不可靠的信号是指信号在处理之前可能丢失。在发送一个信号给进程时,我们说向进程递送(delivery)了一个信号。在信号产生(generation)和递送之间的时间间隔内,称该信号是未决(pending)的。
如果进程采用“阻塞信号递送”(每个信号都有一个信号屏蔽字(signal mask),它规定了当前要阻塞递送到该进程的信号集。进程可以调用sigprocmask来检测和更改当前信号屏蔽字。进程调用sigpending函数来判断哪些信号是设置为阻塞并处于pending状态),而且对该信号的处理是采用系统默认动作或者捕捉该信号,那么该信号将一直保持未决状态,直到进程对信号解除阻塞,或者对该信号的处理改为忽略。
函数kill和raise
kill并非杀死进程,而是将信号发送到进程或进程组,而raise用于进程向自身发送信号。
#include <signal.h> int kill(pid_t pid, int signo); int raise(int signo); 返回值:成功,返回0;失败,返回-1 说明: 调用raise(signo)等价于调用kill(getpid(), signo)。 关于参数pid: pid > 0: 将信号发送给ID为pid的进程。 pid == 0: 将该信号发送给与发送进程属于同一组的所有进程。 pid < 0: 将该信号发送给进程组ID为pid绝对值的所有进程。 pid == -1: 将该信号发送给所有进程(不包括内核进程和init进程)。
编号为0的信号定义为空信号,如果kill的signo参数为0,则kill仍然执行正常的错误检查,但是不发送信号,这就可以用来确定一个特定的进程是否仍然存在。如果向一个并不存在的进程发送空信号,则kill返回-1,并且errno被设置为ESRCH。 |
函数alarm和pause
使用alarm函数可以设置一个定时器,如果在某个时刻定时器超时,则产生SIGALRM信号,其默认系统处理方式为终止调用该alarm函数的进程。
#include <unistd.h> unsigned int alarm(unsigned int seconds); 返回值:之前设置的闹钟的余留时间或者0 说明: 每个进程只能有一个闹钟时间,如果在调用alarm时,之前为该进程设置的闹钟时间还没有超时,那么用其余留的秒数作为本次alarm函数的返回值,以前设置的闹钟时间被新值代替;如果在前一个闹钟未超时的情况下设置新的闹钟,而新闹钟的seconds为0,则返回前一个闹钟的余留时间后取消前一个闹钟。
int pause(void); pause函数挂起调用进程直至捕捉到一个信号。就是说只有执行了一个信号处理程序并从其返回时,pause才返回,此时,pause返回-1,errno设置为EINTR。 |
信号集
信号集sigset_t是一种表示多个信号集合的数据类型,信号集处理函数如下所示:
#include <signal.h> int sigemptyset(sigset_t* set); int sigfillset(sigset_t* set); int sigaddset(sigset_t* set, int signo); int sigdelset(sigset_t* set, int signo); 返回值:成功,返回0;失败,返回-1 int sigismember(const sigset_t* set, int signo); 返回值:条件为真,返回1;为假,返回0 说明: sigfillset使set包含所有的信号,sigemptyset清空set。一般在应用程序使用信号集set之前,至少调用sigfillset或者sigemptyset一次。 如果信号集中的信号数目少于一个整型量所包含的位数,则可用一位表示一个信号的方法实现信号集,sigemptyset将整型中的位全部设置为0,而sigfillset全部设置为1,这两个函数在<signal.h>中实现为宏: #define sigemptyset(ptr) (*(ptr) = 0) #define sigfillset(ptr) (*(ptr) = ~(sigset_t)0, 0) sigfillset函数除了将各位设为1外,还必须返回0,因此使用逗号算符将逗号后的值作为表达式的值返回。由于没有编号为0的信号,因此从信号编号中减去1以获得要处理的位编号。 |
如下为sigaddset、sigdelset、sigismember函数的实现:
#include <signal.h> #include <errno.h> /* <signal.h> usually defines NSIG to include signal number 0 */ #define SIGBAD(signo) ((signo) <= 0 || (signo) >= NSIG) int sigaddset(sigset_t* set, int signo) { if (SIGBAD(signo)) { errno = EINVAL; return (-1); } *set |= 1 << (signo - 1); /* turn bit on */ return (0); } int sigdelset(sigset_t* set, int signo) { if (SIGBAD(signo)) { errno = EINVAL; return (-1); } *set &= ~(1 << (signo - 1)); /* turn bit off */ return (0); } int sigismember(const sigset_t* set, int signo) { if (SIGBAD(signo)) { errno = EINVAL; return (-1); } return ((*set & (1 << (signo - 1))) != 0); }
函数sigprocmask
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是个空指针,则不改变当前进程的信号屏蔽字,how的值也无意义。 |
如下程序打印调用进程信号屏蔽字中的信号名:
[root@benxintuzi signal]# cat mask.c #include <errno.h> #include <signal.h> #include <stdio.h> void pr_mask(const char* str) { sigset_t sigset; int errno_save; errno_save = errno; /* save errno */ if (sigprocmask(0, NULL, &sigset) < 0) printf("sigprocemask error\n"); else { printf("%s: ", str); if (sigismember(&sigset, SIGINT)) printf("SIGINT "); if (sigismember(&sigset, SIGQUIT)) printf("SIGQUIT "); if (sigismember(&sigset, SIGUSR1)) printf("SIGUSR1 "); if (sigismember(&sigset, SIGALRM)) printf("SIGALRM "); /* remaining signals can go here */ printf("\n"); } errno = errno_save; /* restore errno */ } int main(void) { pr_mask("print signal mask...\n"); return (0); } [root@benxintuzi signal]# ./mask print signal mask... :
函数sigpending
函数sigpending返回信号集:
#include <signal.h> int sigpending(sigset_t* set); 返回值:成功,0;失败,-1 |
[root@benxintuzi signal]# cat mask02.c #include <signal.h> #include <stdio.h> static void sig_quit(int); int main(void) { sigset_t newmask, oldmask, pendmask; if (signal(SIGQUIT, sig_quit) == SIG_ERR) printf("can't catch SIGQUIT\n"); /* * * Block SIGQUIT and save current signal mask. * */ sigemptyset(&newmask); sigaddset(&newmask, SIGQUIT); if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) printf("SIG_BLOCK error\n"); sleep(5); /* SIGQUIT here will remain pending */ if (sigsuspend(&pendmask) < 0) printf("sigsuspend error\n"); if (sigismember(&pendmask, SIGQUIT)) printf("\nSIGQUIT pending\n"); /* * Restore signal mask which unblocks SIGQUTI. * */ if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) printf("SIG_SETMASK error\n"); printf("SIGQUIT unblocked\n"); sleep(5); /* SIGQUIT here will terminate with core file */ exit(0); } static void sig_quit(int signo) { printf("caught SIGQUIT\n"); if (signal(SIGQUIT, SIG_DFL) == SIG_ERR) printf("can't reset SIGQUIT\n"); }
函数sigaction
sigaction的功能是检查或更改指定信号相关联的处理函数,此函数取代了Unix早期版本中的signal函数。
#include <signal.h> int sigaction(int signo, const struct sigaction* restrict act, struct sigaction* restrict oact); 返回值:成功,0;失败,-1 说明: 信号的前一个处理函数用oact保存,若act非空,则指向信号的新处理函数。 struct sigaction { void (*sa_handler)(int); /* addr of signal handler, or SIG_IGN, or SIG_DFL */ sigset_t sa_mask; /* additional signals to block */ int sa_flags; /* signal options */ void (*sa_sigaction)(int, siginfo_t*, void*); /* alternate handler */ };
sa_flags选项如下: SA_INTERRUPT: 由此信号中断的系统调用不自动重启。 SA_NOCLDSTOP: 若signo是SIGCHLD,当子进程停止时,不产生此信号;当子进程终止时,仍旧产生此信号。 SA_NOCLDWAIT: 若signo是SIGCHLD,当调用进程的子进程终止时,不创建僵死进程;若调用进程随后调用wait,则阻塞直到其所有的子进程都终止,此时返回-1,errno设置为ECHILD。
SA_NODEFER: 在执行信号捕捉函数时,系统不自动阻塞此信号(除非sa_mask包含了此信号)。这种类型的信号对应于早期的不可靠信号。 SA_ONSTACK: 若用sigaltstack已声明了一个替换栈,则此信号递送给替换栈上的进程。 SA_RESETHAND: 在此信号捕捉函数的入口处,将此信号的处理方式重置为SIG_DFL,并清除SA_SIGINFO标志。这种类型的信号对应于早期的不可靠信号。设置此标志的行为如同设置了SA_NODEFER标志。 SA_RESTART: 由此信号中断的系统调用自动重启动。 SA_SIGINFO: 为信号处理程序提供了附加信息:一个指向siginfo结构的指针以及一个指向进程上下文标识符的指针。 |
通常,按下列方式调用信号处理程序:
void handler(int signo);
但是,如果设置了SA_SIGINFO标志,那么按下列方式调用信号处理程序:
void handler(int signo, siginfo_t* info, void* context);
siginfo结构体包含信号产生原因的有关信息:
struct siginfo
{
int si_signo; /* signal number */
int si_errno; /* errno value from <errno.h> */
int si_code; /* additional info */
pid_t si_pid; /* sending process ID */
uid_t si_uid; /* sending process real user ID */
void* si_addr; /* address that caused the fault */
int si_status; /* exit value or signal number */
union sigval si_value; /* application-specific value */
};
union sigval
{
int sival_int;
void* sival_ptr;
}
应用程序在递送信号时,在si_value.sival_int中传递一个整型数或者在si_value.sival_ptr中传递一个指针值。
大多数的平台都使用sigaction函数实现signal函数。当然有些平台仍然在使用不可靠的signal,主要是为了向下兼容。
使用sigaction实现signal的程序如下:
/* Reliable version of signal(), using POSIX sigaction() */ 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; #endif } else act.sa_flags |= SA_RESTART; if (sigaction(signo, &act, &oact) < 0) return (SIG_ERR); return (oact.sa_handler); }
对于除了SIGALRM外的所有信号,我们都尝试设置SA_RESTART标志,因此被这些信号中断的系统调用都能自动重新启动。不希望重启动由于SIGALRM信号中断的系统调用的原因是:我们希望对I/O操作可以设置时间限制。当然也可以设置为阻止所有被中断的系统调用的重新启动,只需去掉if判断,其他保持不变即可。
函数abort
#include <stdlib.h> void abort(void); 说明: 该函数的功能是使程序异常终止。 ISO C规定,调用abort将向主机环境递送一个未成功终止的通知,其方法是调用raise(SIGABRT)。 让进程捕捉SIGABRT的用意是:在进程终止之前执行所需的清理工作。首先查看是否将执行默认动作,如果是则冲刷所有的标准I/O流。不进行冲刷的唯一条件是如果进程捕捉到了此信号,然后调用了_exit或_Exit,在这种情况下,任何未冲刷的内存中的标准I/O缓存都将被丢弃。 |
#include <signal.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> void abort(void) /* POSIX-style abort() function */ { sigset_t mask; struct sigaction action; /* Caller can't ignore SIGABRT, if so reset to default */ sigaction(SIGABRT, NULL, &action); if (action.sa_handler == SIG_IGN) { action.sa_handler = SIG_DFL; sigaction(SIGABRT, &action, NULL); } if (action.sa_handler == SIG_DFL) fflush(NULL); /* flush all open stdio streams */ /* Caller can't block SIGABRT; make sure it's unblocked */ sigfillset(&mask); sigdelset(&mask, SIGABRT); /* mask has only SIGABRT turned off */ sigprocmask(SIG_SETMASK, &mask, NULL); kill(getpid(), SIGABRT); /* send the signal */ /* If we're here, process caught SIGABRT and returned */ fflush(NULL); /* flush all open stdio streams */ action.sa_handler = SIG_DFL; sigaction(SIGABRT, &action, NULL); /* reset to default */ sigprocmask(SIG_SETMASK, &mask, NULL); /* just in case ... */ kill(getpid(), SIGABRT); /* and one more time */ exit(1); /* this should never be executed ... */ }
函数sleep、nanosleep、clock_nanosleep
#include <unistd.h> unsigned int sleep(unsigned int seconds); 返回值:0或剩下的秒数 说明: 此函数使调用进程被挂起直到满足下面两个条件之一: (1) 超过seconds指定的秒数。 (2) 调用进程捕捉到一个信号并从信号处理程序返回。 Linux 3.2.0用nanosleep函数实现sleep,使得sleep的具体实现与信号和闹钟alarm相互独立。
#include <time.h> int nanosleep(const struct timespec* reqtp, struct timespec* remtp); 返回值:若睡眠到指定的时间,返回0;若出错,返回-1 说明: nanosleep与sleep类似,但提供了纳秒级的精度。 reqtp指定了睡眠的秒和纳秒,remtp结构中包含了至指定睡眠时间还剩下的秒数(如果对改时间不感兴趣,可以将其设为NULL)。
#include <time.h> int clock_nanosleep(clockid_t clock_id, int flags, const struct timespec* reqtp, struct timespec* remtp); 返回值:若睡眠到指定时间,返回0;若出错,返回错误码 说明: 该函数计算的是基于特定时钟的相对时间。 clock_id指定了所基于的特定时钟: CLOCK_REALTIME: 实时系统时间; CLOCK_MONOTONIC: 不带负跳数的实时系统时间; CLOCK_PROCESS_CPUTIME_ID: 调用进程的CPU时间; CLOCK_THREAD_CPUTIME_ID: 调用线程的CPU时间; flags用于控制延迟是相对的(0)还是绝对的(TIMER_ABSTIME)。 |
作业控制信号
SIGCHLD: 子进程已停止或终止。
SIGCONT: 如果进程已停止,则使其继续运行。
SIGSTOP: 停止信号(不能被捕捉或忽略)。
SIGTSTP: 交互式停止信号。
SIGTTIN: 后台进程读取控制终端。
SIGTTOU: 后台进程写入控制终端。
除了SIGCHLD以外,大多数应用程序并不处理这些信号。交互式shell通常对这些信号感兴趣,当键入挂起字符(Ctrl + Z)时,SIGTSTP被送至前台进程组的所有进程。当我们通知shell在前台或后台恢复运行一个作业时,shell向该作业中的所有进程发送SIGCONT信号。如果向一个进程递送了SIGTTIN或SIGTTOU信号,则在系统默认情况下将停止此进程。
一个程序处理作业控制时,通常使用如下代码序列,函数的功能为将标准输入复制到标准输出中:
[root@benxintuzi signal]# cat jobctr.c #include <signal.h> #include <unistd.h> #include <stdio.h> #define BUFFSIZE 1024 static void sig_tstp(int signo) /* signal handler for SIGTSTP */ { sigset_t mask; /* ... move cursor to lower left corner, reset tty mode ... */ /* * * Unblock SIGTSTP, since it's blocked while we're handling it. * */ sigemptyset(&mask); sigaddset(&mask, SIGTSTP); sigprocmask(SIG_UNBLOCK, &mask, NULL); signal(SIGTSTP, SIG_DFL); /* reset disposition to default */ kill(getpid(), SIGTSTP); /* and send the signal to ourself */ /* we won't return from the kill until we're continued */ signal(SIGTSTP, sig_tstp); /* reestablish signal handler */ /* ... reset tty mode, redraw screen ... */ } int main(void) { int n; char buf[BUFFSIZE]; /* * * Only catch SIGTSTP if we're running with a job-control shell. * */ if (signal(SIGTSTP, SIG_IGN) == SIG_DFL) signal(SIGTSTP, sig_tstp); while ((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0) if (write(STDOUT_FILENO, buf, n) != n) printf("write error\n"); if (n < 0) printf("read error\n"); return (0); }
信号名和信号编号
在信号名和信号编号之间,很多Unix系统是用数组来提供这种映射关系的,数组下标作为编号,而对应的数组元素是指向信号名的指针:
extern char* sys_siglist[];
#include <signal.h> void psignal(int signo, const char* msg); 说明: psignal函数打印与信号编号对应的信号名。 msg部分输出到标准错误文件,文件中内容格式如下: msg: “该信号的说明”\n 如果msg为NULL,则只输出信号的说明部分到文件中。
#include <signal.h> void psiginfo(const siginfo_t* info, const char* msg); psiginfo用于打印与siginfo结构体相关的信息。
#include <string.h> char* strsignal(int signo); 说明: 如果只需得到信号名,不需要将其写到标准错误文件中(可以写到日志文件中),可以使用strsignal函数。 |