目录
1.信号在内核中的表示
信号处理的方法
2.信号的递达,未决,阻塞
3.信号集操作函数
(1).sigset_t(信号集类型)
(2).信号集函数
(3).sigprocmask函数—修改block表
(4).测试
4.信号处理流程
1.信号在内核中的表示
在进程的task_struct里面有三张表来存储信号相关的信息,分别是block表,pending表,handler表。block表和pending表是位图结构。
block表用于表示当前信号是否被阻塞,比特位的位置代表信号的编号,比特位对应的值代表信号是否被阻塞,0表示未被阻塞,1表示被阻塞。
pending表用于表示当前信号是否处于未决状态(就是是否有信号要处理),比特位的位置代表信号的编号,比特位对应的值代表信号是否收到该信号,0表示未收到,1表示收到。
handler表用于保存进行信号递达(信号处理)时的处理方法,是一个指针数组,存放的是处理函数的指针。
信号处理的方法
信号处理有三种方式:
1.默认:如果要终止进程,直接终止进程,释放进程资源。如果要暂停进程,把进程状态设置为stop,放到等待队列里面就行。
2.忽略:将pending里面的1置为0,然后直接返回就行。
3.自定义捕捉:执行用户提供的信号处理函数handler。比如我们要处理信号4,此时我们处于内核态,根据4号信号的handler表里面找到用户定义的handler函数地址,然后切换到用户态执行信号处理函数(这里调用handler对操作系统来说相当于是一次回调)。
2.信号的递达,未决,阻塞
- 实际执行信号的处理动作称为信号递达(Delivery)——自定义捕捉,默认,忽略(忽略就是把pending表/递达表从1置0)
- 信号从产生到递达之间的状态,称为信号未决(Pending)——本质是这个信号被暂存在task_struct信号位图中,未决
进程可以选择阻塞(Block )某个信号——本质是OS,允许进程暂时屏蔽指定的信号(之后再处理/递达)
被阻塞的信号:
1.该信号依旧是未决的
2.该信号不会被递达,直到解除阻塞!方可递达
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达时可选的一种处理动作。
3.信号集操作函数
(1).sigset_t(信号集类型)
未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
就像是一个unsigned int
虽然sigset_t 是一个位图结构,但是不同的OS实现是不一样的,不能让用户直接修改该变量,需要使用特定的函数
(2).信号集函数
#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);
int sigismember(const sigset_t *set, int signo); //判断当前信号是否在信号集中
- 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
- 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
- 注意,在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
(3).sigprocmask函数—修改block表
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集),也就是block位图
- 第一个参数how可以用以下宏
- 第二个参数set是一个输入型参数,表示一个信号集(需要我们在用户层面使用信号集函数指定好信号集)
- 第三个参数oset是一个输出型参数,把老的信号集返回出来
(4).测试
1.首先定义两个信号集,iset和oset,之后作为参数传到sigprocmask里面。
2.往iset里面加入2号信号
3.调用sigprocmask函数将2号信号设置为阻塞
4.结果:一直循环打印“zebra”,使用kill -2或者Ctrl+C没有反应。
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
int main()
{
sigset_t iset, oset;
sigemptyset(&iset);
sigemptyset(&oset);
sigaddset(&iset, 2); // 往iset里面加上2号信号
sigprocmask(SIG_SETMASK, &iset, &oset);
int count = 0;
sigset_t pending;
while (1)
{
printf("zebra\n");
}
return 0;
}
4.信号处理流程
首先我们来了解一下什么是内核态,什么是用户态。
- 内核态:执行OS的代码和数据时,计算机所处的状态就叫做内核态。OS的代码的执行全部都是在内核态
- 用户态:就是用户代码和数据被访问或者执行的时候,所处的状态。我们自己写的代码全部都是在用户态执行的
其主要区别在于权限不同,内核态的权限更高。
大致流程:
我们在用户态发起系统调用(或者进程时间片到了,要切换进程,必须进入内核态,让操作系统来执行进程切换)后会进入内核态执行系统调用,执行完以后根据task_struct里面的pendng表,block表,和handler表(两个位图一个函数指针数组),检测是否有信号要处理,如果有一个信号block表里面是0,pending表里面是1,表示信号需要递达。此时我们根据handler表找到用户定义的自定义信号处理方法handler,切换到用户态执行handler方法(如果是默认或者忽略,则不需要进入用户态),然后再通过sigreturn函数返回内核态,进行后续一些处理以后(操作系统内核让你跳转到用户态,你执行完信号处理函数自然要返回给操作系统一些信息,比如是否执行成功等),检测有没有新的信号要递达,如果没有就返回用户态,继续执行进程后面的代码。