当前阶段一、 信号递达、未决与阻塞在深入代码之前必须先搞清楚三个容易混淆的概念术语英文含义递达Delivery实际执行信号的处理动作默认、忽略或自定义未决Pending信号从产生之后到递达之前所处的状态阻塞Block进程可以选择阻止某个信号递达。被阻塞的信号产生后将保持在未决状态直到解除阻塞⚠️重点区分阻塞 ≠ 忽略阻塞信号还没被处理只是因为“被屏蔽”而暂时无法递达。解除阻塞后信号仍然会递达。忽略信号已经递达了只是处理动作选择了“忽略”而已。注信号未决。信号在位图中还没来得及处理打个比方阻塞就像你把快递员拒之门外快递还在门口等着忽略就像你收了快递但直接扔掉了。一个是不让进来一个是进来后不处理。二、在内核中的表示信号在内核中表示示意图每个信号都有两个标记位分别表示 阻塞 (block) 和 未决(pending)还有一个函数指针表示处理动作。信号产生时 内核在进程控制块中设置该信号的未决标志 直到信号递达才清除该标志 。 上图 SIGHUP信号未阻塞也未产生过 当它递达时执行默认处理动作SIGINT信号产生过但正在被阻塞所以暂时不能递达。虽然它的处理动作是忽略 但在没有解除阻塞之前不能忽略这个信号因为进程仍然有机会改变动作之后再解除阻塞 。SIGQUIT信号未产生过一旦产生SIGQUIT信号将被阻塞它的处理动作是用户自定义含函数sighandler。每个进程的PCBtask_struct中与信号相关的核心数据结构有三部分blocked位图记录了哪些信号被阻塞1表示阻塞。pending位图记录了哪些信号已经产生但尚未递达1表示未决。handler数组记录了每个信号的处理函数指针SIG_DFL、SIG_IGN或用户函数地址。2.1 handler#include iostream #include unistd.h #include signal.h #include sys/types.h #include functional #include vector void handler(int sig) { std::cout hello sig : sig std::endl; signal(2,SIG_DFL); //2默认动作是终止 std::cout 恢复处理动作 std::endl; } int main() { //signal(2,handler); //自定义捕捉 //signal(2,SIG_IGN); //忽略信号 while(true) { sleep(1); std::cout . std::endl; } return 0; }2.2 相关知识解释Q1block 和 pending 有没有什么关系 ?没有对应的约束关系;如果你收到了消息当时阻塞了消息就显示不出来了三、信号集 —— sigset_t 类型验证信号保存的话题Linux提供信号操作一定是围绕着这三张表展开的3.1 附加话题 - 位图如何设置的sigset_t是信号集类型本质是一个位图我们不能直接用位操作/|/~修改它必须使用 Linux 提供的信号集操作函数这是操作信号阻塞的核心工具。3.2 信号集操作函数sigset_t 类型对于每种信号用一个 bit 表示 有效 或 无效状态 至于这个类型内部如何存储这些 bit 则依赖于系统实现 从使用者的角度是不必关心的使用者只能调用一下函数来操作sigset_t 变量 而不应该对它的内部数据做任何解释 比如用printf直接打印sigset_t 变量是没有意义的 。为了操作blocked和pending位图Linux提供了sigset_t类型通常是一个整数或结构体用于表示多个信号的集合。我们可以使用以下函数来操作信号集#include signal.h int sigemptyset(sigset_t *set); // 清空集合所有信号位0 int sigfillset(sigset_t *set); // 填满集合所有信号位1 int sigaddset(sigset_t *set, int signo); // 将signo加入集合 int sigdelset(sigset_t *set, int signo); // 将signo从集合中删除 int sigismember(const sigset_t *set, int signo); // 测试signo是否在集合中所有函数成功返回0出错返回-1。sigismember返回1表示是成员0表示不是-1表示出错。⚠️重要使用sigset_t变量前必须先用sigemptyset或sigfillset初始化否则结果不可预料。四、 更改信号屏蔽字 —— sigprocmasksigprocmask函数可以读取或更改进程的信号屏蔽字也就是blocked位图。#include signal.h int sigprocmask(int how, const sigset_t *set, sigset_t *oset);how含义SIG_BLOCK将set中的信号添加到当前屏蔽字中blocked | setSIG_UNBLOCK将set中的信号从当前屏蔽字中移除blocked ~setSIG_SETMASK直接将当前屏蔽字设置为set如果oset非空则之前的屏蔽字被备份到oset中。如果set为NULL则how被忽略仅用于获取当前屏蔽字。核心使用场景阻塞某个信号用SIG_BLOCK将信号添加到 block 位图。解除某个信号的阻塞用SIG_UNBLOCK将信号从 block 位图中移除。重置阻塞信号集用SIG_SETMASK直接替换整个 block 位图。获取当前阻塞信号集set传NULLoldset传信号集指针获取当前 block 位图。五、获取未决信号集 —— sigpendingsigpending函数用于获取当前进程的未决信号集pending位图。#include signal.h int sigpending(sigset_t *set);将当前的pending位图通过set参数传出。成功返回0出错返回-1。Q为什么只给我提供输出型参数 难道不给我们修改pending位图吗六、阻塞SIGINT并观察pending变化#include iostream #include unistd.h #include signal.h #include sys/types.h #include functional #include vector #include cstdio void PrintfPending(sigset_t pending) { printf(我是一个进程(%d),pending: ,getpid()); for (int signo 31; signo 1; signo--) { if (sigismember(pending, signo)) { std::cout 1; } else { std::cout 0; } } std::cout std::endl; } int main() { // 1.屏蔽2号信号 sigset_t block, oblock; // 先把位图全部清空 sigemptyset(block); sigemptyset(oblock); // 2号信号添加到集合里 sigaddset(block, SIGINT); int n sigprocmask(SIG_SETMASK, block, oblock); (void)n; // 4.重复获取/打印过程 while (true) { // 2.获取pending信号集合 sigset_t pending; int m sigpending(pending); // 3.打印 PrintfPending(pending); sleep(1); } return 0; }6.1 不可被捕捉信号要是把所有的信号都给屏蔽了所有信号都不可以递达 程序不久金刚不坏了6.2 0-1-0#include iostream #include unistd.h #include signal.h #include sys/types.h #include functional #include vector #include cstdio void handler(int sig) { std::cout 递达2号信号 std::endl; } void PrintfPending(sigset_t pending) { printf(我是一个进程(%d),pending: , getpid()); for (int signo 31; signo 1; signo--) { if (sigismember(pending, signo)) { std::cout 1; } else { std::cout 0; } } std::cout std::endl; } int main() { signal(SIGINT,handler); // 1.屏蔽2号信号 sigset_t block, oblock; // 先把位图全部清空 sigemptyset(block); sigemptyset(oblock); // 2号信号添加到集合里 sigaddset(block, SIGINT); //把所有信号都屏蔽了这ok // for (int i 1; i 32; i) // sigaddset(block, i); int n sigprocmask(SIG_SETMASK, block, oblock); (void)n; int cnt 0; // 4.重复获取/打印过程 while (true) { // 2.获取pending信号集合 sigset_t pending; int m sigpending(pending); // 3.打印 PrintfPending(pending); if(cnt 10) { sigprocmask(SIG_SETMASK,oblock,nullptr); std::cout 解除对2号的屏蔽 std::endl; } sleep(1); cnt; } return 0; }6.3 解除屏蔽 pending的变化递达之后才清0 递达之前清理结论当我们准备递达的时候要首先清空pending 信号集中对应的位图 1- 0七、为什么阻塞时信号不会递达在进程从内核态返回用户态时例如系统调用结束、中断处理完毕内核会执行以下逻辑检查 pending 位图 (~blocked 位图)如果存在非零位说明有信号没有被阻塞且处于未决状态内核就会处理该信号执行对应的handler或默认动作。而被阻塞的信号即使pending1也因为blocked对应位为1而被掩码过滤掉不会递达。当解除阻塞后内核再次检查时该信号就会满足条件从而递达。八、 常见误区与注意事项8.1 阻塞 ≠ 忽略操作效果信号是否会丢失阻塞block信号暂不递达解除阻塞后会递达不会常规信号多次产生只记一次忽略ignore信号递达后什么都不做信号已经递达只是动作是忽略8.2 常规信号的pending是位图不是队列如果进程阻塞了SIGINT然后在阻塞期间连续按下10次CtrlC最终解除阻塞后handler只会被调用一次。因为pending位图只能记录“有”或“没有”不能记录次数。这就是常规信号不可靠信号的特点。实时信号34~64则支持队列可以记录多次。8.3 SIGKILL和SIGSTOP无法被阻塞这两个信号是“终极”信号用于强制终止或停止进程不受阻塞和自定义捕捉的影响。这是系统设计的安全底线。8.4 sigprocmask的使用约束在多线程程序中sigprocmask的行为是未定义的应该使用pthread_sigmask。不能阻塞SIGKILL和SIGSTOP但函数不会报错会被内核忽略。