一、什么是信号
1、信号的定义
信号是UNIX和Linux系统响应某些条件而产生的一个事件,接收到该信号的进程会相应地采取一些行动。信号是软中断,通常信号是由一个错误产生的。但它们还可以作为进程间通信或修改行为的一种方式,明确地由一个进程发送给另一个进程。一个信号的产生叫生成,接收到一个信号叫捕获。
一个完整的信号周期包括三个部分:信号的产生,信号在进程中的注册,信号在进程中的注销(由内部机制完成),执行信号处理函数。如下图所示:
重点是信号的产生与处理。
2、常用的信号
3、产生信号的条件
1. bash按下时,终端会发送信号给前台,Ctrl-C 产生 SIGINT 信号,Ctrl-\产生SIGQUIT信号,Ctrl-Z产生SIGTSTP 信号,上述信号使得进程停止;
2. 硬件异常信号,条件由硬件检测到并通知内核,然后内核向当前进程发送信号。例如执行了除以 0 的指令,再比如当前进程访问了非法内存地址,MMU 会产生异常,内核将这个异常解释为 SIGSEGV 信号发送给进程;
3. 一个进程调用 kill(2) 函数可以发送信号给另一个进程;
4. 可以用 **kill(1)**命令发送信号给某个进程,kill(1)命令也是调用kill(2)函数实现的,如果不明确指定信号则发送 SIGTERM 信号,该信号的默认处理动作是终止进程;
5. 当内核检测到某种软件条件发生时也可以通过信号通知进程,例如闹钟超时产生SIGALRM信号,向读端已关闭的管道写数据时产生SIGPIPE信号。
4、信号的处理
一个进程收到一个信号的时候,可以用如下方法进行处理:
1. 执行系统默认动作(SIG_DFL):对大多数信号来说,系统默认动作是用来终止该进程。
2. 忽略此信号忽略此信号(SIG_IGN):接收到此信号后没有任何动作。
3. 执行自定义信号处理函数:用户定义的信号处理函数处理该信号。
系统默认动作又分为以下几种:
Term:终止当前进程;
Core:终止当前进程并生成Core Dump(用于gdb调试);
Ign:忽略该信号;
Stop:停止当前进程;
Cont:继续执行先前停止的进程。
二、信号的分类
可以从两个不同的分类角度对信号进行分类:
1. 可靠性方面,分为可靠信号与不可靠信号;
2. 与时间的关系上,分为实时信号与非实时信号。
【不可靠信号】:
Linux信号机制基本上是从 UNIX 系统中继承过来的。早期 UNIX 系统中的信号机制比较简单和原始,后来在实践中暴露出一些问题,因此,把那些建立在早期机制上的信号叫做“不可靠信号”,信号小于 SIGRTMN 的信号都是不可靠信号。这就是“不可靠信号”的来源。
它的主要问题是:进程每次处理信号后,就将对信号的响应设为默认动作。在某些情况下,就导致对信号的错误处理;因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用 signal ( ),重新安装该信号。信号可能丢失,后面将对此详细阐述。因此,早期 UNIX 下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失。
Linux支持不可靠信号,但是对不可靠信号机制做了改进,在调用完信号处理函数后,不必重新调用该信号的安装函数(信号安装函数时在可靠机制上的实现)。因此,Linux 下的不可靠信号问题主要是指的是信号可能丢失。
【可靠信号】:
随着时间的发展,实践证明了有必要对信号的原始机制加以改进和扩充。所以,后来出现的各种 UNIX 版本分别在这方面进行了研究,力图实现“可靠信号”。由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。同时,信号的发送和安装也出现了新版本:信号发送函数 sigqueue() 及信号安装函数 sigaction() 。POSIX.4 对可靠信号机制做了标准化。但是,POSIX 只对可靠信号机制应具有的功能以及信号机制的对外接口做了标准化,对信号机制的实现没有作具体的规定。信号值位于 SIGRTMIN 和 SIGRTMAX 之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。Linux在支持新版本的信号安装函数 sigation()以及信号发送函数 sigqueue ( ) 的同时,仍然支持早期的 signal()信号安装函数,支持信号发送函数 kill()。
不要有这样的误解:由 sigqueue() 发送、sigaction 安装的信号就是可靠的。事实上,可靠信号是指后来添加的新信号(信号值位于SIGRTMIN及SIGRTMAX之间);不可靠信号是信号值小于SIGRTMIN的信号。信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。目前linux中的 signal() 是通过 sigation() 函数实现的,因此,即使通过 signal()安装的信号,在信号处理函数的结尾也不必再调用一次信号安装函数。同时,由 signal() 安装的实时信号支持排队,同样不会丢失。
对于目前 linux 的两个信号安装函数: signal() 及 sigaction() 来说,它们都不能把 SIGRTMIN 以前的信号变成可靠信号(都不支持排队,仍有可能丢失,仍然是不可靠信号),而且对 SIGRTMIN 以后的信号都支持排队。这两个函数的最大区别在于,经过 sigaction 安装的信号都能传递信息给信号处理函数(对所有信号这一点都成立),而经过signal 安装的信号却不能向信号处理函数传递信息。对于信号发送函数来说也是一样的。
三、可重入函数和不可重入函数
参与信号处理的函数必须是可重入函数。
试想一个问题,当进程接收到一个信号时,转到你关联的函数中执行,但是在执行的时候,进程又接收到同一个信号或另一个信号,又要执行相关联的函数时,程序会怎么执行?
也就是说,信号处理函数可以在其执行期间被中断并被再次调用。当返回到第一次调用时,它能否继续正确操作是很关键的。这不仅仅是递归的问题,而是可重入的(即可以完全地进入和再次执行)的问题。而反观Linux,其内核在同一时期负责处理多个设备的中断服务例程就需要可重入的,因为优先级更高的中断可能会在同一段代码的执行期间“插入”进来。
简言之,就是说,我们的信号处理函数要是可重入的,即离开后可再次安全地进入和再次执行,要使信号处理函数是可重入的,则在信息处理函数中不能调用不可重入的函数。下面给出可重入的函数在列表,不在此表中的函数都是不可重入的,可重入函数表如下:
【不可重入的函数】:
在实时系统的设计中,经常会出现多个任务调用同一个函数的情况。如果这个函数被设计成为不可重入的函数的话,那么不同任务调用这个函数时可能修改其他任务用到的数据,从而导致不可预料的后果。不可重入函数在实时系统设计中被视为不安全函数。满足下列条件的函数多数是不可重入的:
- 函数体内使用了静态的数据结构;
- 函数体内调用了malloc()或者free()函数;
- 函数体内调用了标准I/O函数。
四、SIGCHLD 语义
无论一个进程是正常终止还是异常终止,都会通过系统内核向其父进程发送 SIGCHLD (17) 信号。父进程完全可以在针对 SIGCHLD (17) 信号的信号处理函数中,异步地回收子进程的僵尸,简洁而又高效。
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/wait.h>
void sigchld (int signum) // 信号处理函数
{
while (1)
{
// 使用具有非阻塞特性的 waitpid 函数,
// 在一个循环过程中回收尽可能多的僵尸进程。
pid_t pid = waitpid (-1, NULL, WNOHANG);
if (pid == -1)
{
if (errno != ECHILD)
{
perror ("wait"), exit (1);
}
printf ("子进程都死光了\n");
break;
}
if (!pid)
break;
printf ("%d子进程终止\n", pid);
}
}
int main (void)
{
if (signal(SIGCHLD, sigchld) == SIG_ERR) // 发送信号失败
perror("signal"), exit(1);
sleep(5);
pid_t pid1 = fork();
if (pid1 == -1)
perror("fork"), exit(1);
else if (pid1 == 0)
{
printf("这是子进程 pid = %d\n", getpid());
printf("父进程的 ppid = %d\n", getppid());
}
else
{
sleep(10); //可以保证子进程先被调度
printf("这是父进程 ppid = %d\n", getpid());
}
return 0;
}