Linux信号机制学习笔记
一、常见的信号有哪些
使用kill -l
命令可以查看所有的信号。
常用的命令及说明如下:
信号(信号值) | 信号产生 | 默认动作 |
---|---|---|
SIGINT(2) | 键盘中断Ctrl+c | 终止进程 |
SIGKILL(9) | kill - 9 pid | 杀死进程&不能被捕获&不能被忽略 |
SIGSEGV(11) | 段错误、无效的内存引用 | 终止进程 |
SIGTERM(15) | kill pid 或 killall 进程名 | 终止进程 |
SIGCHLD(17) | 子进程死的时候会给父进程发送这个信号 | 忽略此信号 |
当一个进程收到信号后会有三种处理方式
- 忽略,不对该信号做任何处理。
- 捕获,设置处理函数,将处理函数告诉内核,当接收到信号,内核调用该处理函数。
- 默认动作,采用系统默认的处理方式,对于不同的信号,默认的处理方式不同,有的会终止进程,有的忽略。
需要注意,虽然我们可以设置处理函数来捕获信号,但是有些信号是不可以被捕获,也不可以被忽略的,如 SIGKILL
,我们通常使用 kill - 9 pid 杀死进程,它是系统提供的一种强制手段,来保证一定能杀死目标进程。
code time~~
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
void sigHandler(int signum);
int main(){
struct sigaction sigConfig;
sigConfig.sa_handler=sigHandler;
//注册一个信号处理函数
//sigaction(2,&sigConfig,NULL);
printf("休眠30s~~~\n");
sleep(30);
printf("after sleep\n");
}
void sigHandler(int signum){
printf("收到了信号%d\n",signum);
}
上面是一个简单的代码,先把注册信号处理函数的代码注释了,执行这个程序,终端必定会处于等待状态,然后我们按ctrl+c。
对于上面的代码,因为是直接在命令行运行的程序,并且属于前台程序,命令行会等待程序的执行完成,这里程序休眠了30s,然后没到30s我就按下了ctrl+c,前面说过 ctrl+c 就相当于发送了SIGINT信号,默认动作是终止进程,所以后面的"after sleep"也没来得及输出。
取消sigaction(2,&sigConfig,NULL)
这行代码的注释再次编译执行结果如下:
可以看到,当程序休眠时,我按下了ctrl+c,休眠被中断了,然后去执行了信号的处理函数,因为我给信号注册了处理函数,就不会执行它的默认动作了,也就是终止进程。处理函数执行完,又接着sleep之后的代码执行。
补充:sleep是一个系统调用,当一个信号到来时,会中断系统调用。
下面详细讲解下sigaction函数
二、sigaction函数
通过sigaction函数可以注册信号的处理函数,其原型如下:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
- signum:要捕获的信号
- act: sigaction 结构体,用来对该信号做一些处理配置。
- oldact:用来获取之前对该信号做的配置。
sigaction 结构体常用参数说明
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
- sa_handler:信号的处理函数,可以传SIG_DFL(默认处理方式)、SIG_DFL(忽略信号)。或者我们自己定义的函数指针,通过sa_handler的函数原型可以看出它只能接收一个参数,也就是信号的信号值作为入参。
- sa_sigaction:也是信号的处理函数,但是它的参数比sa_handler更加丰富。使用sa_sigaction来替代sa_handler,需要我们将sa_flags 参数设置为SA_SIGINFO。可以发现sa_sigaction的函数原型中,参数有三个,第一个是信号值,第二个是信号的一些简单信息,第三个不常用。
- sa_mask:通过它可以设置在调用信号处理程序时阻塞某些信号。注意仅仅是在信号处理程序正在执行时才能阻塞某些信号,如果信号处理程序执行完了,那么依然能接收到这些信号。
- sa_flags:这个字段有很多取值,但是有两个是常用的,
SA_SIGINFO
表示用sa_sigaction处理函数,而不是sa_handler。SA_RESTART
前面说过信号会中断系统调用,如果设置了这个值,当信号处理函数完了,被中断的系统调用会被系统重新调用。需要注意,实际上只有部分系统调用在被中断后还能重启,eg:read、wait、scanf。
三、信号的阻塞
那么什么是信号的阻塞呢?
当一个信号传递给进程时,如果该信号处于阻塞状态,那么操作系统不会将信号传递出去,被阻塞的信号也不会影响进程的行为,信号只是暂时被阻止传递,一旦解除了该信号的阻塞,系统就会把信号传递出去。阻塞和忽略是不同的,忽略是信号可以被传递,但是进程不做任何处理,将信号丢弃。
既然说到了阻塞,那么为了显的我不那么水,就再补充两个概念:
- 信号递达:执行信号的处理动作称为信号递达。
- 信号未决:信号从产生到抵达之间称为信号未决。
coding~
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
void sigHandler(int signum);
int main(){
struct sigaction sigConfig;
sigConfig.sa_handler=sigHandler;
sigset_t blockset;
sigemptyset(&blockset);
//将信号15添加到信号集中
sigaddset(&blockset,15);
//设置信号集中的信号为阻塞状态
sigprocmask(SIG_BLOCK,&blockset,NULL);
//注册一个信号处理函数
sigaction(15,&sigConfig,NULL);
int i=0;
while(1){
printf("休眠1s\n");
sleep(1);
i++;
if(i==10){
printf("10s到了,,现在解除信号集合中信号的阻塞状态\n");
sigprocmask(SIG_UNBLOCK,&blockset,NULL);
}
}
}
void sigHandler(int signum){
printf("收到了信号%d\n",signum);
}
如上代码,通过sigprocmask函数第一个参数SIG_BLOCK与SIG_UNBLOCK可以设置信号集中信号的阻塞与解除阻塞状态。先通过./signalText 启动程序,然后在另一个终端,连续发送3次信号值为15的信号,执行结果如下:
可以看到,确实满足了i==10条件后,解除了信号的阻塞,之后我们发送的信号才能被收到,但是发现没用,其实我连续发送了3个信号,为什么只收到了一个?
这又引入了一个概念,可靠信号与不可靠信号。
信号分为可靠信号(信号值在1-32)和不可靠信号(信号值>32)
- 不可靠信号:相同的信号多次到来,不会做排队处理,会被合并为一个,造成信号的丢失。
- 可靠信号:会做排队处理,不存在信号丢失问题。
ok,那我修改下代码,把信号值15换成55试试。启动程序后,在另一个终端,连续发送3次信号值为55的信号 killall -55 signalText
没毛病~
四、常用的信号处理函数
函数 | 说明 |
---|---|
sigemptyset() | 初始化信号集 |
sigaddset() | 增加一个信号至信号集 |
sigdelset() | 从信号集里删除一个信号 |
sigfillset() | 将所有信号添加至信号集 |
sigfillset() | 某个信号是否已添加至信号集 |
sigprocmask() | 查询或设置信号遮罩 |
sigaction() | 查询或设置信号处理方式 |
signal() | 设置信号处理方式 |
kill() | 发送信号给指定进程 |
具体用法参看linux的文档,不细说了。前面介绍了通过sigaction函数给进程设置处理方式,其实还可以通过signal,不过signal实在太简单了,也没sigaction的设置丰富。
五、信号处理函数的中断
当一个信号达到后,会调用信号的处理函数,如果这时候有其他的信号到来(和当前不是同一个新信号),那么当前的处理函数执行会被中断,等新信号的处理函数执行完成了才继续执行之前的信号处理函数.如果多次到来的是同一个信号,那么相同的信号会排队等待(如果是不可靠信号,需要排队执行的多个相同信号会被合并成一个)。
code time ~~
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
void sigHandler(int signum);
int main(){
struct sigaction sigConfig;
sigConfig.sa_handler=sigHandler;
//注册55信号处理函数
sigaction(55,&sigConfig,NULL);
//注册56信号处理函数
sigaction(56,&sigConfig,NULL);
//注册57信号处理函数
sigaction(57,&sigConfig,NULL);
while(1){
sleep(10);
}
}
void sigHandler(int signum){
printf("信号%d到达\n",signum);
int i;
for(i=0;i<5;i++){
sleep(2);
printf("信号%d正在执行中...\n",signum);
}
}
编译程序后运行,然后再另一个终端向该进程发送55、56、57信号。
[root@VM-0-13-centos ~]# killall -55 signalText
[root@VM-0-13-centos ~]# killall -56 signalText
[root@VM-0-13-centos ~]# killall -57 signalText
执行结果如下:
这里我是用可靠信号演示的,如果是相同的信号是不会中断处理函数的执行,多个相同信号会挨个排队执行。
如果是不可靠信号,多次发送相同的不可靠信号也会排队但由于是不可靠信号,后面排队的多个相同信号在会被合并为1个。
发送信号如下:
代码只捕获信号值2和4,执行结果如下:
可以看到第一个信号2没有被第二个信号2中断,第二个以及第三个信号2会排队等待第一个信号2的处理函数执行完成,第一个信号2的处理函数没执行完又来了一个不同的信号4,信号2的处理函数的执行被中断,开始执行信号4的处理函数,接着后面又来了两个信号4,由于是相同信号,会排队,但由于是不可靠信号,后面相同的信号4会被合并成为1个,所以虽然发送了3个信号4但只打印了两个。最后又来了两个信号2,会和之前的信号2合并为一个。