信号是什么
信号是一种软件中断,它提供了一种处理异步事件的方法,并且是进程间唯一的异步通信方式
当我们按下 crtl+c 时可以终止一个前台进程,我们可能会想为什么我们只是按下了一个按键就可以终止一个进程呢?好神奇啊!!!,这里如果我们计算机有一定的认识后,我们会知道按下 crtl+c 的同时,背后发生了很多事情。其实终端驱动程序在这里起到了很大的作用。
- 用户输入 crtl+c
- 驱动程序收到字符
- 匹配 VINTR 和 ISIG 的字符被开启
- 驱动程序调用系统信号
- 信号系统发送 SIGINT 到进程
- 进程收到 SIGINT
- 进程终止
信号来自哪里?
信号来自内核, 生成信号的的请求一共来自三个地方
- 用户, 用户可以通过 ctrl+c, ctrl+\,或者是终端驱动程序分配给信号控制字符的任意按键来请求内核产生信号。
- 内核,当进程执行出错时,内核给进程发送一个信号,例如,非法段存取,浮点数溢出,或者是一个非法的指令。同时也利用信号通知进程特定的事件发生。
- 进程,一个进程可以通过 kill 给另外一个进程发送信号,两个进程间可以通过信号通信。
由进程的某个操作产生的信号被称为同步信号(synchronous signals),像除零操作等。而由像用户按键这样进程外的事件请求发生的信号被称为异步信号(asynchronous signals)。
早期信号处理机制
如何处理信号
进程收到 SIGINT 信号时,并不一定要被终止,进程可以通过系统调用 signal 来告诉内核它要如何处理相应的信号。
进程一共有三个选择:
- 接受默认处理,一般是被终止。进程不做任何处理的话,收到信号时就是执行信号的默认处理动作,进程可以通过调用
signal(SIGINT, SIG_DFL)
来恢复默认处理动作。 - 忽略信号,进程可以通过
signal(SIGINT, SIG_IGN)
来告诉内核它要忽略 SIGINT 信号。这里需要注意忽略信号并不是没有处理信号,而是处理的结果是忽略此信号。 - 调用一个处理函数,通过调用
signal(SIGINT, func_name)
进程会告诉内核,当 SIGINT 信号到来时应该选用哪个处理函数。
signal 的返回值是指向前一个处理函数的函数指针。
信号的弱点
如果只有一个信号要被处理,则早期的信号处理模型足够使用。但是当有多个信号来临时会发生什么?如果进程采用的终止或忽略的方式那么结果不会出现意外,但如果选择调用函数来处理,情况就比较复杂了。
当一个信号捕获后,信号处理函数就失效了,因此在每个信号被捕获后都需要重新设置,但是就算信号重新设置的速度非常快,但还是有可能在本次捕捉和重新设置之间的时间间隙中让此刻发送过来的信号逃脱。就是因为存在这一间隙让原有的信号机制变得不可靠,所以早期的信号又被称为不可靠的信号。
那么进程自同时收到多个信号时该如何处理呢?
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
void WaitOneSec(int);
void WaitTwoSec(int);
int main() {
signal(SIGINT, WaitOneSec);
signal(SIGQUIT, WaitTwoSec);
char input[INPUT_LEN];
int nchars;
do {
nchars = read(0, input, (INPUT_LEN - 1));
if (nchars == -1) {
perror("read error");
}
else {
input[nchars] = '\0';
printf("%s\n", input);
}
} while (strncmp(input, "quit", 4) != 0);
return 0;
}
void WaitOneSec(int signo) {
printf("received signal : %d wait one second\n", signo);
sleep(1);
}
void WaitTwoSec(int signo) {
printf("received signal : %d wait two second\n", signo);
sleep(2);
}
我们可以通过不同的组合按键来测试我们的系统怎样处理同时到来的多个信号,我们分别输入^C^C^C^C, ^\^\^, aa^C^Cbb, ^\^\aa^C。
- 如果多个SIGINT 杀死了进程,则说明我们的系统使用的不可靠的信号,如果多个 SIGINT 没有杀死进程,则说明信号处理函数在被调用后还可以 生效。sigaction 允许我们选择这两种方式。
- 信号 a 打断信号 b 的处理过程,按下 ^C, ^\ 时,程序会先跳到 WaitOneSec 然后跳到 WaitTwoSec ,此函数执行完毕后回到 WaitOneSec ,然后回到主程序。
- 信号 a 打断信号 a ,这时有三种处理方式分别是(1)递归调用同一个处理函数。(2)忽略再次到来的信号。(3)阻塞再次到来的额信号,知道当前信号被处理完毕。
- 被中断的系统调用,在上面的程序中,read 会阻塞等待用户输入。当输入中断 ^C,程序会跳到信号处理函数,当处理完毕后,程序回到原位置。会有什么问题吗?如果输入 aa^Cbb,程序接受到的是 aabb 还是 bb?
早期的信号还有两个弱点
- 早期信号机制只告诉处理函数它是由于哪种类型的信号激发的,但是并不知道产生信号的原因。
- 处理函数不能安全的阻塞其它信号。
新的信号处理机制
处理多个信号
在以前的信号中,对信号的处理只有三种选择, SIG_DFL, SIG_IGN, 函数处理,这些选项在新的机制中被放在了 struct sigaction 中。
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int , siginfo_t , void *);
sigset_t sa_mask;
int sa_flags;
}
如果选择 sa_sigactin 为处理函数,那么对应的处理函数不仅可以得到信号编号,还可以得到被触发的原因。只需要设置 sa_flags 为 SIGINFO 就选择了新的处理方式。sa_mask 用来决定在处理一个信号时,是否要阻塞其他信号。
总结
一个进程常常会被多个信号打断,信号可能在任意时刻达到, signal 提供了一种简单但是不安全的信号处理机制。sigaction 可以让我们用明确的方法来应对可能同时到来的各种信号。