【linux】进程信号——信号的保存和处理
文章目录
- 一、阻塞信号
- 1.1 信号的相关概念
- 1.2 在内核中的构成
- 二、捕捉信号概念
- 2.1 内核态和用户态
- 2.2 信号捕捉流程图
- 三、信号操作
- 3.1 sigset_t信号集
- 3.2 信号集操作函数
- 3.2.1 更改block表sigprocmask
- 3.2.2 获取pending信号集sigpending
- 3.3 验证
- 四、捕捉信号操作
- 4.1 内核捕捉信号sigaction
- 4.1.1 act.sa_mask参数
- 五、可重入函数
- 六、volatile关键字
上一章主要讲述了信号的产生:【linux】进程信号——信号的产生
这篇文章主要讲后面两个过程。
一、阻塞信号
1.1 信号的相关概念
- 实际执行信号的处理动作称为信号递达(Delivery)。
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
因为信号不是被立即处理的,所以在信号产生之后,递达之前的这个时间窗口称作信号未决,也就是把信号暂时保存起来。
- 进程可以选择阻塞 (Block )某个信号。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
而且没有信号产生我们也可以选择阻塞某个信号。
1.2 在内核中的构成
我们知道发送信号的本质:修改PCB中的信号位图。 而阻塞和未决也是通过位图的方式来保存信号。它们的位图也存在于进程的PCB内。
位图的第几个比特位代表第几个信号。
对于block,比特位的内容代表是否阻塞信号。
对于pending,比特位的内容代表是否收到信号。
对于handler,他是一个函数指针数组,代表处理动作。
信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
而只要阻塞位图对应比特位为1,那么信号永远不能递达。
如果一个信号想要递达,那么pending位图对应的比特位为1,block位图对应的比特位为0。
总结:
1️⃣ 因为pending和block是两个位图,所以不会互相影响,一个信号没产生并不影响先被阻塞。
2️⃣ 进程能够识别信号是因为,内核当中有这三种结构,它们组合起来就能够识别信号。
3️⃣ 当有多个信号同时来的时候,因为位图只有一个比特位,所以只会处理一次,其他的信号都会被丢失。
二、捕捉信号概念
2.1 内核态和用户态
信号产生后不会立即进行处理,而是在合适的时候进行处理。那么什么时候是合适的时候呢?
从内核态返回用户态的时候进行处理。
如果用户态想要获得操作系统自身资源(getpid……)或者硬件资源(write……)的时候必须通过系统调用接口完成访问。
而我们无法以用户态的身份调用系统调用,必须让自己的状态变成内核态。
所以往往系统调用比较花费时间,我们应该避免频繁调用系统接口。
既然有内核态和用户态,那么我们怎么辨别我们当前是哪个身份呢?
CPU内存有寄存器,而寄存器又分为可见寄存器(EXP)和不可见寄存器(状态寄存器),而所有保存在寄存器跟当前进程强相关的数据叫做上下文数据。
CPU里面有一个叫做CR3的寄存器,它表征的就是当前进程的运行级别:
0表示内核态
3表示用户态
那么一个进程是如何进入操作系统中执行方法呢?
因为操作系统会加载到内存且只有一份,所以内核级页表也只需要一份。在CPU里有一块寄存器指向这个内核级页表,进程切换时这个寄存器不变。
所以进程可以在特定的区域内以内核级页表的方式访问操作系统的代码和数据。当进程想要访问OS的接口,直接在自己的进程地址空间跳转即可。
而我们知道操作系统有自己的保护机制,用户凭什么能执行访问操作系统数据的接口呢?
当想要跳转到内核区会进行权限认证,如果CR寄存器显示的是内核态就可以访问,反之阻止访问。但是我们怎么把用户态切换成内核态呢?当我们调用系统接口的时候,起始的位置会帮忙改变,先把CR3中的用户态改成内核态,然后再跳转到内核区。
2.2 信号捕捉流程图
当执行代码要调用系统调用接口时,本来应该调用完成后返回用户态继续执行代码。但是我们知道用户态和内核态的转换消耗时间很大,所以这里不会直接返回。
它会去找task_struct中的三张表先遍历block,当发现为1就跳过,如果不是1就看pending表如果为1就进入handler完成动作。而默认动作直接杀死进程,忽略动作直接把pending位图中的1置为0。
但是如果时自定义动作,我们自己写的handler方法在用户态,因为内核态不能直接访问用户态(从技术上可以,但是不能,为了安全),所以又要把自己的身份变成用户态再进入用户态执行handler方法。
当我们执行完handler后能不能直接返回代码区继续执行呢?
答案是不能,因为上下文信息都还在操作系统里。所以要先回到内核,经过特殊的系统调用回到代码区继续执行代码。
我们可以把线路简化一下方便观察:
分析:
这里的绿色部位交点代表身份的切换,而箭头的指向:
向下表示从用户态切换到内核态
向上表示从内核态切换到用户态
三、信号操作
经过上面的的学习我们知道了内核中有block和pending位图,为了方便我们操作,操作系统定义了一个类型sigset_t。
#include int sigemptyset(sigset_t *set);// 清0 int sigfillset(sigset_t *set); int sigaddset (sigset_t *set, int signo);// 比特位由0变为1 int sigdelset(sigset_t *set, int signo);// 比特位由1变为0 int sigismember(const sigset_t *set, int signo);
- 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
- 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
- 注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
- sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。
3.1 sigset_t信号集
我们能看到阻塞和未决都是用一个比特位进行标记(非0即1),所以在用户层采用相同的类型sigset_t进行描述。这个类型表示每个信号有效和无效的状态:在阻塞信号集就表示是否处于阻塞;在未决信号集就表示是否处于未决。
阻塞信号集有一个专业的名词叫做信号屏蔽字。
3.2 信号集操作函数
sigset_t对每个信号用一个比特位表示有效或者无效的状态。它的底层操作对于我们用户层来说不必要知道,我们只能调用下面的接口函数来操作sigset_ t变量。
3.2.1 更改block表sigprocmask
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
#include int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); RETURN VALUE sigprocmask() returns 0 on success and -1 on error. In the event of an error, errno is set to indicate the cause.
参数介绍:
how:怎么修改。
set:主要是用来跟how一起使用,用来重置信号。
oldset:输出型参数,把老的信号屏蔽字保存,方便恢复
3.2.2 获取pending信号集sigpending
#include int sigpending(sigset_t *set); RETURN VALUE sigpending() returns 0 on success and -1 on error. In the event of an error, errno is set to indicate the cause.
读取当前进程的未决信号集,通过set参数传出。 set是输出型参数。
3.3 验证
首先要知道默认情况所有信号都不会被阻塞。获取pending表对应的比特位变成1。
而如果被阻塞了,信号永远不会被递达,获取pending表对应的比特位永远为1。
static void show_pending(const sigset_t &Pending) { // 信号只有1 ~ 31 for(int signo = 31; signo >= 1; signo--) { if(sigismember(&Pending, signo)) { std::cout sigset_t Block, oBlock, Pending; // 初始化全0 sigemptyset(&Block); sigemptyset(&oBlock); sigemptyset(&Pending); // 在Block集添加阻塞信号 sigaddset(&Block, 2); // 修改block表 sigprocmask(SIG_SETMASK, &Block, &oBlock); // 打印 while(true) { // 获取pending sigpending(&Pending); show_pending(Pending); sleep(1); } return 0; } // 信号只有1 ~ 31 for(int signo = 31; signo = 1; signo--) { if(sigismember(&Pending, signo)) { std::cout sigset_t Block, oBlock, Pending; // 初始化全0 sigemptyset(&Block); sigemptyset(&oBlock); sigemptyset(&Pending); // 在Block集添加阻塞信号 sigaddset(&Block, 2); // 修改block表 sigprocmask(SIG_SETMASK, &Block, &oBlock); // 打印 int cnt = 8; while(true) { // 获取pending sigpending(&Pending); show_pending(Pending); sleep(1); if(--cnt == 0) { // 恢复 sigprocmask(SIG_SETMASK, &oBlock, &Block); std::cout void (*sa_handler)(int); //自己写的方法 void (*sa_sigaction)(int, siginfo_t *, void *);// null sigset_t sa_mask;// 信号集 int sa_flags;// 设置0 void (*sa_restorer)(void);// null }; std::cout struct sigaction act, oact; // 初始化 act.sa_handler = handler; act.sa_flags = 0; sigemptyset(&act.sa_mask); sigaction(SIGINT, &act, &oact); while(1) sleep(1); return 0; } printf(" %d signo is being caught\n",signo); printf("quit:%d\n",quit); quit = 1; printf("-%d\n",quit); } int main() { signal(2,handler); while(!quit); printf("i am quit\n"); return 0; }