信号保存
信号递达
实际执行信号的处理动作
信号未决
信号从产生到递达之间的状态
阻塞信号
不会立刻处理,会被缓存起来
上面这些概念不是凭空产生的,在linux里面有真实对应的
pending:未决信号集(已经到来、被阻塞卡住的信号)
位图每一位对应一个信号编号:
- 位 =
0:该信号没有待处理 - 位 =
1:该信号存在未决实例,等待递达
block:阻塞掩码(哪些信号要暂时挡住)
block 与 pending 的联动逻辑
- 内核发送信号给进程;
- 先查
block位图:- 对应位 = 0(不阻塞):直接信号递达,执行 handler,不会进入 pending;
- 对应位 = 1(阻塞):将
pending对应位标记为 1,信号暂存等待;
- 调用
sigprocmask()修改 block,把某信号位从 1 改为 0(解除阻塞); - 内核立刻扫描 pending,若该信号 pending 位 = 1,执行信号递达,清空 pending 标记。
handler:每个信号编号对应一条处理规则,决定信号递达时程序该做什么。
数组handler[32]长度 32,对应 Linux 1~31 号标准信号; 下标规则:数组下标 = 信号编号 - 1例:SIGINT (2 号信号) → 存储在handler[1];SIGHUP (1 号) →handler[0]。
函数指针数组数组里每一格存的是地址:可以是系统常量地址,也可以是用户自定义函数的地址。
三个问题
问题一
signal(2,myhandler)底层在做什么
将myhandler的函数指针传入handler列表
问题二
什么叫做忽略,默认
二者是内核用宏定义的特殊函数指针常量,强制转换成sighandler_t信号处理函数指针类型,存入handler[]函数指针数组。
数值标识:0代表默认处理,1代表忽略处理,和普通用户自定义函数地址区分开。
问题三
在信号还没有产生的时候,进程就能识别和处理信号???
在内核中就内置了信号的管理和处理方法
sigset_t叫做信号集,本质是一个位图(比特集合),每一个 bit 对应一个信号:
- bit = 1:该信号标记为「有效」
- bit = 0:该信号标记为「无效」 所有信号只用 1 个 bit 记录状态,不计数,同一个信号多次触发,bit 只会置 1,不会叠加次数。
前文提到,未决标志、阻塞标志都采用位图存储:每个信号仅占用 1 个比特位,状态只有 0/1 两种,不会记录信号触发的次数。 正因二者存储逻辑完全一致,Linux 统一使用sigset_t(信号集)类型承载这两类标记。
sigset_t本质是比特集合,用每一位标识单个信号的 “有效 / 无效” 状态,在不同集合中含义不同:
- 阻塞信号集(进程信号屏蔽字 Signal Mask)比特位为 1:代表该信号被阻塞; 比特位为 0:代表该信号允许递达。 ⚠️ 注意:此处的 “屏蔽” 指暂时阻塞,和直接忽略信号(SIG_IGN)是完全不同的逻辑。
- 未决信号集(pending)比特位为 1:代表该信号已产生、因阻塞暂未处理; 比特位为 0:代表无待处理的对应信号。
阻塞信号集也被称作进程的信号屏蔽字,属于进程独有属性。后续章节会详细讲解信号集的各类操作函数,用于增删、清空、判断集合内的信号。
系统当然也提供了对信号集修改的一些系统调用
| 函数原型 | 功能 | 参数说明 | 返回值 |
|---|---|---|---|
int sigemptyset(sigset_t *set); | 清空信号集,将集合内所有比特置 0(不含任何信号) | set:待操作的信号集指针 | 成功返回 0,失败返回 - 1 |
int sigfillset(sigset_t *set); | 填满信号集,将所有信号对应比特置 1(包含全部信号) | set:待操作的信号集指针 | 成功返回 0,失败返回 - 1 |
int sigaddset(sigset_t *set, int signum); | 向信号集中添加单个信号,对应 bit 置 1 | set:信号集;signum:信号编号(如 SIGINT=2) | 成功返回 0,失败返回 - 1 |
int sigdelset(sigset_t *set, int signum); | 从信号集中删除单个信号,对应 bit 置 0 | set:信号集;signum:信号编号 | 成功返回 0,失败返回 - 1 |
int sigismember(const sigset_t *set, int signum); | 判断指定信号是否存在于集合中 | set:只读信号集;signum:信号编号 | 存在返回 1,不存在返回 0,出错返回 - 1 |
sigprcmask
参数详解
int how:操作模式,3 种可选宏
表格
宏常量 作用 SIG_BLOCK 屏蔽叠加:将 set中的信号追加到原有屏蔽字(阻塞新增信号)SIG_UNBLOCK 解除屏蔽:从原有屏蔽字中移除 set里的信号SIG_SETMASK 覆盖替换:直接用 set完整替换当前进程屏蔽字const sigset_t *set:待操作的信号集
- 传
NULL:仅读取当前屏蔽字,不做任何修改; - 非空:根据
how执行增 / 删 / 替换操作。
- sigset_t *oldset:保存旧屏蔽字
- 传
NULL:不保存原有屏蔽字; - 非空:函数执行完毕后,将修改前的原始信号屏蔽字存入
oldset,方便后续恢复。
返回值
- 成功:返回
0 - 失败:返回
-1,并设置errno错误码
sigpending
读取进程未决信号集(pending 信号集),将当前所有已产生、但因被屏蔽暂时无法递达的信号存入参数set中。 未决信号:信号已经发送给进程,但该信号位于进程屏蔽字中被阻塞,系统暂存该信号,等待解除屏蔽后再处理。
参数详解
sigset_t *set:输出型参数,传入一个已初始化的信号集变量地址,函数执行完成后,set内部比特位会标记所有当前未决的信号。- 不可传入
NULL,否则无法获取未决信号信息。
返回值
- 成功:返回
0 - 失败:返回
-1,同步设置全局错误码errno
那么这么修改pending-->我们之前就学过了-->kill接口
使用
我们自己设置要阻塞的信号
void PrintPending(const sigset_t &pending) { for(int sig=31;sig>0;sig--) { if(sigismember(&pending,sig)) { cout<<"1"; } else cout<<"0"; } sleep(1); cout<<endl; } int main() { cout<<"我的pid:"<<getpid()<<endl; sigset_t block_set, old_set; sigemptyset(&block_set); sigemptyset(&old_set); sigaddset(&block_set, 2); // 只是设置自己的block // 还要再进行系统调用 int n = sigprocmask(SIG_SETMASK, &block_set, &old_set); if (n < 0) perror("sigprocmask"); while (1) { // 获取pending表 sigset_t pending; sigemptyset(&pending); int n = sigpending(&pending); if (n < 0) perror("sigpending"); // 打印pending表 PrintPending(pending); sleep(1); } return 0; }我们将每一位都设置成阻塞,依然是9等信号无法被阻塞
我们让进程进行15次循环之后将2号终止信号恢复
发现进程立马就停止了
一旦我们解除对某个信号的阻塞,他就会被立即递达
但是我们不想要这个样子就要用到signal函数
当阻塞结束的时候pending表相应的比特位会改变,那么是再handler之前变成0,还是再handler之后变成0的呢????
我们可以再handler方法内部继续将pending打印出来就可以知道了
根据观察我们不难得出----其实再调用handler函数之前,pending表对应的信号就已经被改成0了