这里指出,Linux对标准信号的编号为1~31。然而,Linux 于 signal(7)手册页中列出的信号名称却超出了 31 个。名称超出的原因有多种。有些名称只是其他名称的同义词,之所以定义是为了与其他 UNIX 实现保持源码兼容。其他名称虽然有定义,但却并未使用。以下列表介绍了各种信号
Linux的可用信号都定义在bits\signum.h
中,其中包括标准信号和POSIX实时信号
信号
硬件产生的信号
硬件异常可以产生 SIGBUS、SIGFPE、SIGILL,和 SIGSEGV 信号,调用 kill()函数来发送此类信号是另一种途径,但较为少见。SUSv3 规定,在硬件异常的情况下,如果进程从此类信号的处理器函数中返回,亦或是进程忽略或者阻塞了此类信号,那么进程的行为未定义。原因如下:
- 从信号处理器中返回:假设机器语言指令产生了上述信号之一,并因此而调用了信号处理器函数。当从处理器函数正常返回后,程序会尝试从其中断处恢复执行。可当初引发信号产生的恰恰正是这条指令,所以信号会再次“光临”。故事的结局通常是,程序进入无限循环,重复调用信号处理器函数
- 忽略信号:忽略因硬件而产生的信号于情理不合,试想算术异常之后,程序应当如何
继续执行呢?无法明确。当由于硬件异常而产生上述信号之一时,Linux 会强制传递信号,即使程序已经请求忽略此类信号。 - 阻塞信号。与上一种情况一样,阻塞因硬件而产生的信号也不合情理:不清楚程序随
后应当如何继续执行
正确处理硬件产生信号的方法有二:要么接受信号的默认行为(进程终止);要么为其编
写不会正常返回的处理器函数。除了正常返回之外,终结处理器执行的手段还包括调用_exit()以终止进程,或者调用 siglongjmp(),确保将控制传递回程序中(产生信号的指令
位置之外)的某一位置
下面程序展示了忽略或者阻塞 SIGFPE 信号的后果,或者可正常返回的处理器将其捕获的结果。
#include <memory.h>
#include <stdlib.h>
#include <stdio.h>
#include <zconf.h>
#define _GNU_SOURCE /* Get strsignal() declaration from <string.h> */
#include <string.h>
#include <signal.h>
static void
sigfpeCatcher(int sig)
{
printf("Caught signal %d (%s)\n", sig, strsignal(sig));
/* UNSAFE (see Section 21.1.2) */
sleep(1); /* Slow down execution of handler */
}
int
main(int argc, char *argv[])
{
/* If no command-line arguments specified, catch SIGFPE, else ignore it */
if (argc > 1 && strchr(argv[1], 'i') != NULL) {
printf("Ignoring SIGFPE\n");
if (signal(SIGFPE, SIG_IGN) == SIG_ERR) {
printf("signal");
exit(EXIT_FAILURE);
}
} else {
printf("Catching SIGFPE\n");
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sa.sa_handler = sigfpeCatcher;
if (sigaction(SIGFPE, &sa, NULL) == -1) {
printf("sigaction");
exit(EXIT_FAILURE);
}
}
bool blocking = argc > 1 && strchr(argv[1], 'b') != NULL;
sigset_t prevMask;
if (blocking) {
printf("Blocking SIGFPE\n");
sigset_t blockSet;
sigemptyset(&blockSet);
sigaddset(&blockSet, SIGFPE);
if (sigprocmask(SIG_BLOCK, &blockSet, &prevMask) == -1){
printf("sigprocmask");
exit(EXIT_FAILURE);
}
}
printf("About to generate SIGFPE\n");
int x, y;
y = 0;
x = 1 / y;
y = x; /* Avoid complaints from "gcc -Wunused-but-set-variable" */
if (blocking) {
printf("Sleeping before unblocking\n");
sleep(2);
printf("Unblocking SIGFPE\n");
if (sigprocmask(SIG_SETMASK, &prevMask, NULL) == -1){
printf("sigprocmask");
exit(EXIT_FAILURE);
}
}
printf("Shouldn't get here!\n");
exit(EXIT_FAILURE);
}
SIGBUS
- 产生该信号(总线错误,bus error)即表示发生了某种内存访问错误。
- 当使用由 mmap()所创建的内存映射时,如果试图访问的地址超出了底层内存映射文件的结尾,那么将产生该错误
SIGFPE
- 该信号因特定类型的算术错误而产生,比如除以0。
- 后缀 FPE 是浮点异常的缩写,不过整型算术错误也能产生该信号。
- 该信号于何时产生的精确细节取决于硬件架构和对 CPU 控制寄存器的设置
SIGILL
- 如果进程试图执行非法(即格式不正确)的机器语言指令,系统将向进程发送该信号。
SIGABRT
- 当进程调用abort()函数时,系统向进程发送该信号。默认情况下,该信号会终止进程,并产生核心转储文件。
- 产生核心转储文件用于调试
SIGALRM
- 当alarm函数设置的定时器超时时,产生此信号
- 当
setitimer
函数设置的间隔时间已经超时时,产生此信号
#include <signal.h>
#include<stdio.h>
#include <unistd.h>
void handler();
int main()
{
int i;
signal(SIGALRM,handler);
alarm(5);
for(i=1;i<8;i++){
printf("sleep is -----%d\n",i);
sleep(1);
}
return 0;
}
void handler()
{
printf("hello\n");
}
分析:alarm当时间(5s)超时时,会发送 SIGALRM 信号,从而调用 signal注册的函数handler,handler的动作是打印一个hello
SIGCHLD
- 当父进程的某一子进程终止(或者因为调用了 exit(),或者因为被信号杀死)时,(内核)将向父进程发送该信号。系统默认,忽略此信号。
- 如果父进程希望被告知其子进程的这种状态改变,应该捕捉这个信号
- 信号捕捉函数中通常序表调用
wait
函数以取得子进程ID和其终止状态 - 当父进程的某一子进程因收到信号而停止或恢复时,也可能会向父进程发送该信号。
SIGCLD
与 SIGCHLD 信号同义。
SIGEMT
- Unix系统通常用该信号来标识一个依赖于实现的硬件错误。
- Linux 系统仅在 Sun SPARC实现中使用了该信号。后缀 EMT 源自仿真器陷阱(emulator trap),Digital PDP-11 的汇编程序助记符之一
SIGHUP
- 当终端断开(挂机)时,将发送该信号给终端控制进程。
- SIGHUP 信号还可用于守护进程(比如,init、httpd 和 inetd)。许多守护进程会在收到 SIGHUP 信号时重新进行初始化并重读配置文件
- 借助于显式执行 kill命令或者运行同等功效的程序或脚本,系统管理员可向守护进程手工发送 SIGHUP 信号来触发这些行为
SIGINFO
在 Linux 中,该信号名与 SIGPWR 信号名同义。在 BSD 系统中,键入 Control-T 可产生SIGINFO 信号,用于获取前台进程组的状态信息
SIGINT
- 当用户键入终端中断字符(通常为 Control-C)时,终端驱动程序将发送该信号给前台进程组。该信号的默认行为是终止进程
SIGIO
- 利用 fcntl()系统调用,即可于特定类型(诸如终端和套接字)的打开文件描述符发生 I/O事件时产生该信号
SIGIOT
SIGKILL 和 SIGSTOP
- SIGKILL 信号的默认行为是终止一个进程,SIGSTOP 信号的默认行为是停止一个进程,二者的默认行为均无法改变。
- 当试图用 signal()和 sigaction()来改变对这些信号的处置时,将总是返回错误。同样,也不能将这两个信号阻塞
- 这是一个深思熟虑的设计决定。不允许修改这些信号的默认行为,这也意味着总是可以利用这些信号来杀死或者停止一个失控进程。
SIGCONT
- 将该信号发送给已停止的进程,进程将会恢复运行(即在之后某个时间点重新获得调度)。
- 当接收信号的进程当前不处于停止状态时,默认情况下将忽略该信号。
- 进程可以捕获该信号,以便在恢复运行时可以执行某些操作。
可使用 SIGCONT 信号来使某些(因接收 SIGSTOP、SIGTSTP、SIGTTIN 和SIGTTOU 信号而)处于停止状态的进程得以继续运行。由于这些停止信号具有独特目的,所以在某些情况下内核对它们的处理方式将有别于其他信号:
- 如果一个进程处于停止状态,那么一个 SIGCONT 信号的到来总是会促使其恢复运行,即使该进程正在阻塞或者忽略 SIGCONT 信号。该特性之所以必要,是因为如果要恢复这些处于停止状态的进程,舍此之外别无他法。(如果处于停止状态的进程正在阻塞 SIGCONT 信号,并且已经为 SIGCONT 信号建立了处理器函数,那么在进程恢复运行后,只有当取消了对 SIGCONT的阻塞时,进程才会去调用相应的处理器函数。
- 每当进程收到 SIGCONT 信号时,会将处于等待状态的停止信号丢弃(即进程根本不知道这些信号)。相反,如果任何停止信号传递给了进程,那么进程将自动丢弃任何处于等待状态的 SIGCONT 信号。之所以采取这些步骤,意在防止之前发送的一个停止信号会在随后撤销SIGCONT 信号的行为,反之亦然
如果有任一其他信号发送给了一个已经停止的进程,那么在进程收到 SIGCONT 信号而恢复运行之前,信号实际上并未传递。SIGKILL 信号则属于例外,因为该信号总是会杀死进程,即使进程目前处于停止状态。
可中断和不可中断的进程睡眠状态
SIGKILL和SIGSTOP信号对进程的作用是立竿见影的。对于这一论断,需要加入一个前提。内核经常需要令进程进入休眠。而休眠状态分为两种
- TASK_INTERRUPTIBLE:
- 进程正在等待某一事件。比如:
- 正在等待终端输入
- 等待数据写入当前的空管道
- 等待 System V 信号量值的增加
- 进程在该状态下所耗费的时间可长可短。
- 如果为这种状态下的进程产生一个信号,那么操作将中断,而传递来的信号将唤醒进程。
- ps(1)命令在显示处于 TASK_INTERRUPTIBLE 状态的进程时,会将其 STAT(进程状态)字段标记为字母 S
- 进程正在等待某一事件。比如:
- TASK_UNINTERRUPTIBLE:
- 进程正在等待某些特定类型的事件,比如:
- 磁盘 I/O 的完成。
- 如果为这种状态下的进程产生一个信号,那么在进程摆脱这种状态之前,系统将不会把信号传递给进程。
- ps(1)命令在显示处于 TASK_UNINTERRUPTIBLE 状态的进程时,会将其 STAT 字段标记为字母 D
- 进程正在等待某些特定类型的事件,比如:
因为进程处于 TASK_UNINTERRUPTIBLE 状态的时间通常转瞬即逝,所以系统在进程脱离该状态时传递信号的现象也不易于被发现。然而,在极少数情况下,进程可能会因硬件故障、NFS 问题或者内核缺陷而在该状态下保持挂起。这时,SIGKILL 将不会终止挂起进程。如果问题诱因无法得到解决,那么就只能通过重启系统来消灭该进程。
大多数UNIX系统实现都支持TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE状态。从内核 2.6.25 开始,Linux 加入第三种状态来解决上述挂起进程的问题。
- TASK_KILLABLE:
- 该状态类似于 TASK_UNINTERRUPTIBLE,但是会在进程收到一个致命信号(即一个杀死进程的信号)时将其唤醒。
- 在对内核代码的相关部分进行改造后,就可使用该状态来避免各种因进程挂起而重启系统的情况。这时,向进程发送一个致命信号就能杀死进程。
- 为使用 TASK_KILLABLE 而进行代码改造的首个内核模块是 NFS
利用信号进行进程间通信
从某种角度,可将信号视为进程间通信(IPC)的方式之一。然而,信号作为一种 IPC 机制却也饱受限制:
- 信号的异步本质就意味着需要面对各种问题,包括可重入性需求、竞态条件及在信号处理器中正确处理全局变量。(如果用 sigwaitinfo()或者 signalfd()来同步获取信号,这些问题中的大部分都不会遇到。)
- 没有对标准信号进行排队处理。即使是对于实时信号,也存在对信号排队数量的限制。这意味着,为了避免丢失信息,接收信号的进程必须想方设法通知发送者,自己为接受另一个信号做好了准备。要做到这一点,最显而易见的方法是由接收者向发送者发送信号
还有一个更深层次的问题,信号所携带的信息量有限:信号编号以及实时信号情况下一字之长的附加数据(一个整数或者一枚指针值)。与诸如管道之类的其他 IPC 方法相比,过低的带宽使得信号传输极为缓慢。
由于上述种种限制,很少将信号用于 IPC。
父子进程使用SIGUSR1和SIGUSR2进行通信
SIGUSR1 用户自定义信号 默认处理:进程终止
SIGUSR2 用户自定义信号 默认处理:进程终止
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
void handler(int signo)
{
switch(signo) {
case SIGUSR1: //处理信号 SIGUSR1
printf("Parent : catch SIGUSR1\n");
break;
case SIGUSR2: //处理信号 SIGUSR2
printf("Child : catch SIGUSR2\n");
break;
default: //本例不支持
printf("Should not be here\n");
break;
}
}
int main(void)
{
pid_t ppid, cpid;
//为两个信号设置信号处理函数
if(signal(SIGUSR1, handler) == SIG_ERR)
{ //设置出错
perror("Can't set handler for SIGUSR1\n");
exit(1);
}
if(signal(SIGUSR2, handler) == SIG_ERR)
{ //设置出错
perror("Can't set handler for SIGUSR2\n");
exit(1);
}
ppid = getpid();//得到父进程ID
if((cpid = fork()) < 0)
{
perror("fail to fork\n");
exit(1);
}
else if(cpid == 0)
{
// 子进程内向父进程发送信号SIGUSER1
if(kill(ppid, SIGUSR1) == -1)
{
perror("fail to send signal\n");
exit(1);
}
while(1);//死循环,等待父进程的信号
}
else
{
sleep(1);//休眠,保证子进程先运行,并且发送SIGUSR1信号
// 父进程向子进程发送SIGUSER2信号
if(kill(cpid, SIGUSR2) == -1)
{
perror("fail to send signal\n");
exit(1);
}
// 必须sleep一下,否则子进程捕获不到SIGUSER2信号
sleep(1);
printf("will kill child\n");//输出提示
if(kill(cpid, SIGKILL) == -1)
{ //发送SIGKILL信号,杀死子进程
perror("fail to send signal\n");
exit(1);
}
if(wait(NULL) ==-1)
{ //回收子进程状态,避免僵尸进程
perror("fail to wait\n");
exit(1);
}
printf("child has been killed.\n");
}
return 0;
}
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
static void sig_usr(int);
int main(void)
{
if(signal(SIGUSR1, sig_usr) == SIG_ERR)
printf("can not catch SIGUSR1\n");
if(signal(SIGUSR2, sig_usr) == SIG_ERR)
printf("can not catch SIGUSR2\n");
for(;;)
pause();
}
static void sig_usr(int signo)
{
if(signo == SIGUSR1)
printf("received SIGUSR1\n");
else if(signo == SIGUSR2)
printf("received SIGUSR2\n");
else
printf("received signal %d\n", signo);
}
[chinsung@thinkpad apue]$ ./a.out &
[1] 2581
[chinsung@thinkpad apue]$ kill -USR1 2581
received SIGUSR1
[chinsung@thinkpad apue]$ kill -USR2 2581
received SIGUSR2
[chinsung@thinkpad apue]$ kill 2581
[1]+ Terminated ./a.out