目录
- 3. 保存信号-内核通过 “未决信号集” 为每个进程存储已产生但未处理的信号
- 3.1 信号处理流程中的不同状态
- 3.2 信号在内核中的表示
- 3.3 sigset_t信号集类型
- 3.4 信号集操作函数
- 3.4.1 sigprocmask读取或更改进程的信号屏蔽字
- 3.4.2 sigpending读取当前进程的未决信号集
- 3.4.3 综合测试用例
3. 保存信号-内核通过 “未决信号集” 为每个进程存储已产生但未处理的信号
3.1 信号处理流程中的不同状态
- 实际执行信号的处理动作称为信号递达(
Delivery)【自定义、默认、忽略】 - 信号从产生到递达之间的状态,称为信号未决(
Pending)。【信号不是立即处理的,信号产生到处理之间的状态成为信号未决】(内核通过 “未决信号集(Pending Set)” 为每个进程存储已产生但未处理的信号) - 进程可以选择阻塞(
Block)某个信号。(即不让信号未决状态转到递达状态(保持在未决状态),即不处理信号)(阻塞也称屏蔽)- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而信号忽略是递达动作的一种。
3.2 信号在内核中的表示
信号在内核中的表示示意图
进程与信号相关的有三张表:[Linux提供信号的操作,就是围绕这三张表展开的]
pending:unsigned int pending,保存收到的信号的位图,即未决表。比特位的位置:表示的是第几个信号;比特位的内容:是否收到。block:unsigned int block,保存信号的阻塞状态的位图,即阻塞表。比特位的位置:表示第几个信号;比特位的内容:是否阻塞。- 可被递达的信号:pending & (~block)
handler:sighandler_t handler[31],函数指针,数组下标表示信号编号,信号重定义(为指定的信号绑定处理动作)signal()函数的本质就是修改handler表。SIG_DEF:定义即为把0强转成sighandler_t 格式。功能:将信号处理恢复默认动作。SIG_IGN:定义即为把1强转成sighandler_t格式。功能:忽略信号。// 用法:signal(信号,SIG_DEF)signal(信号,SIG_IGN)NAME signal-ANSI C signal handling SYNOPSIS#include<signal.h>typedefvoid(*sighandler_t)(int);sighandler_tsignal(intsignum,sighandler_t handler);
将下表横着看,每一个信号一行。
每个信号都有两个标志位分别表示
阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。
如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理? POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的: 常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。
// 内核结构 2.6.18structtask_struct{.../* signal handlers */structsighand_struct*sighand;sigset_t blockedstructsigpendingpending;...}structsighand_struct{atomic_t count;structk_sigactionaction[_NSIG];// #define _NSIG 64spinlock_t siglock;};struct__new_sigaction{__sighandler_t sa_handler;unsignedlongsa_flags;void(*sa_restorer)(void);/* Not used by Linux/SPARC */__new_sigset_t sa_mask;};structk_sigaction{struct__new_sigactionsa;void__user*ka_restorer;};/* Type of a signal handler. */typedefvoid(*__sighandler_t)(int);structsigpending{structlist_headlist;sigset_t signal;};3.3 sigset_t信号集类型
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
📌
信号屏蔽字(Signal Mask)类似权限的umask
3.4 信号集操作函数
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态, 至于这个类型内部如何存储这些bit则依赖于系统实现, 从使用者的角度是不必关心的, 使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释, 比如用printf直接打印sigset_t变量是没有意义的。
#include<signal.h>intsigemptyset(sigset_t*set);// 初始化信号集为空集: 将参数 set 指向的 sigset_t 变量清空。intsigfillset(sigset_t*set);// 初始化信号集为全集: 将参数 set 指向的 sigset_t 变量填充为包含所有可被捕获、阻塞的信号(逻辑上包含所有有效信号)。intsigaddset(sigset_t*set,intsigno);// 向已初始化的信号集中添加指定信号:将信号编号 signo 对应的信号,加入到 set 指向的信号集中。intsigdelset(sigset_t*set,intsigno);// 从已初始化的信号集中删除指定信号:将信号编号 signo 对应的信号,从 set 指向的信号集中移除。intsigismember(constsigset_t*set,intsigno);//判断指定信号是否为信号集的成员:检查信号编号 signo 对应的信号,是否存在于 set 指向的信号集中。函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。
注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的 状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含 某种 信号,若包含则返回1,不包含则返回0,出错返回-1。
3.4.1 sigprocmask读取或更改进程的信号屏蔽字
调用函数sigprocmask 可以读取或更改进程的信号屏蔽字(阻塞信号集)。
代码块#include<signal.h>intsigprocmask(inthow,constsigset_t*set,sigset_t*oset);返回值:若成功则为0,若出错则为-1如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信 号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后 根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
3.4.2 sigpending读取当前进程的未决信号集
#include<signal.h>intsigpending(sigset_t*set);读取当前进程的未决信号集,通过set参数传出。 调用成功则返回0,出错则返回-13.4.3 综合测试用例
综合测试用例1:屏蔽信号,再发送信号,观察未决表pending
#include<iostream>#include<cstdio>#include<vector>#include<functional>#include<unistd.h>#include<signal.h>#include<sys/types.h>voidPrintPending(sigset_t&pending){printf("我是一个进程(%d),pending: ",getpid());for(intsigno=31;signo>=1;signo--){if(sigismember(&pending,signo))std::cout<<"1";elsestd::cout<<"0";}std::cout<<std::endl;}intmain(){// 1.屏蔽2号信号sigset_t block,oblock;sigemptyset(&block);sigemptyset(&oblock);sigaddset(&block,SIGINT);// for(int i = 1; i < 31; i++) // 5.将所有信号屏蔽,并用"kill 信号 pid"测试// sigaddset(&block, i);intn=sigprocmask(SIG_SETMASK,&block,&oblock);(void)n;// 4.重复获取打印过程while(true){// 2.获取pending信号集合sigset_t pending;intm=sigpending(&pending);// 3.打印PrintPending(pending);sleep(1);}return0;}📌结论:9号信号不可被捕捉,不可被阻塞
综合测试用例2:测试功能在代码结尾注释
#include<iostream>#include<cstdio>#include<vector>#include<functional>#include<unistd.h>#include<signal.h>#include<sys/types.h>voidPrintPending(sigset_t&pending){printf("我是一个进程(%d),pending: ",getpid());for(intsigno=31;signo>=1;signo--){if(sigismember(&pending,signo))std::cout<<"1";elsestd::cout<<"0";}std::cout<<std::endl;}voidhandler(intsig){std::cout<<"######################"<<std::endl;std::cout<<"递达"<<sig<<"信号!"<<std::endl;sigset_t pending;intm=sigpending(&pending);PrintPending(pending);// 如果是 0000 0010 则结论为:处理完handler函数再将2号信号置0。// 验证结果:0000 0000 ,实际结论为:先给pending未决表的2号信号置0,再执行handler函数。std::cout<<"######################"<<std::endl;}intmain(){// 补0.对2号信号的功能重定义signal(SIGINT,handler);// 1.屏蔽2号信号sigset_t block,oblock;sigemptyset(&block);sigemptyset(&oblock);sigaddset(&block,SIGINT);// for(int i = 1; i < 31; i++) // 5.将所有信号屏蔽,并用"kill 信号 pid"测试// sigaddset(&block, i);intn=sigprocmask(SIG_SETMASK,&block,&oblock);(void)n;// 4.重复获取打印过程intcnt=0;while(true){// 2.获取pending信号集合sigset_t pending;intm=sigpending(&pending);// 3.打印PrintPending(pending);if(cnt==10){// 5.恢复对2号信号的block情况std::cout<<"解除对2号信号的屏蔽"<<std::endl;sigprocmask(SIG_SETMASK,&oblock,nullptr);}sleep(1);cnt++;}return0;}// 前提,执行过程中使用Print函数每秒打印pending未决表// 1.将2号信号重定义// 2.将2号信号屏蔽// 3.运行程序,给进程发送2号信号// 4.10秒之后对2号信号恢复,即接触屏蔽// 5.解除之后调用重定义后的2号信号函数// 5.1 验证先给未决表置0,还是先执行2号信号函数(handler函数中)📌结论:在递达之前,先清空pending未决表中对应的信号位图:1 → 0