hello~ 很高兴见到大家! 这次带来的是Linux系统中关于信号这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢?
个 人 主 页 : 默|笙

文章目录
- 一、信号保存
-
- [1.1 信号其他相关常见概念](#1.1 信号其他相关常见概念)
- [1.2 三张表](#1.2 三张表)
- [1.3 信号集操作函数](#1.3 信号集操作函数)
一、信号保存

1.1 信号其他相关常见概念
- 信号递达:实际执行信号处理的动作称作信号递达---信号处理。
- 信号未决:从信号产生到信号递达中间的这段时间叫做信号未决。也就是信号保存。
- 信号阻塞:进程可以选择阻塞(屏蔽)某个信号。被阻塞的信号产生后,会一直维持在未决状态,无法走到递达处理的步骤;直到进程解除对该信号的阻塞,信号才会被递达。
- 阻塞!= 忽略 :忽略是信号已经递达后的一种处理方式(进程选择不做任何操作);而阻塞是在递达前就拦截(它是不让信号递达的方式),让信号始终停留在未决状态,二者发生的阶段和本质完全不同。
- 信号需要被保存,是因为信号产生后通常不会立即被进程处理(递达),因此需要先将其暂存。
- 对于信号递达、信号未决、信号阻塞,进程的task_struct 里面有三张表分别对应:handler表、pending表和block表。
1.2 三张表

sigset_t类型(信号集)
- pending 表和 block 表都是 sigset_t 类型,而 sigset_t 本质是 unsigned long 数组实现的位图(bitmask);位图的第 1 位对应 1 号信号,第 2 位对应 2 号信号,以此类推,信号编号与位图的位编号一一对应。例如:第 2 位 对应 SIGINT(2 号信号)。
pending表(未决信号集)
- pending 表的本质是一个位图,每一个比特位与信号编号一一对应;信号产生后,OS 会将该表中对应信号编号的比特位置为 1 并保存,因为 OS 不会立即处理该信号。因此,表中比特位为 1 的信号,其状态就是信号未决状态(比特位:信号编号,比特位的内容:是否收到该信号)。
- 产生信号后,信号的状态会保存在 pending 表中;信号递达并处理完成后,pending 表中该信号对应的比特位会被置为 0。
block表(信号屏蔽字)
- block 表与 pending 表本质相同,均为位图结构,每一个比特位与信号编号一一对应;比特位为 1 代表该信号被阻塞,为 0 则代表该信号未被阻塞(比特位:信号编号,比特位的内容:该信号是否被阻塞)。
- 修改 block 表的阻塞状态不会影响信号在 pending 表中的保存;被阻塞的信号会一直滞留在 pending 表中,除非解除阻塞,否则 pending 表中对应的比特位会始终保持为 1。
- 如果信号在阻塞解除之前产生多次,常规信号在递达之前产生多次只计⼀次。
- 特殊信号 9 号和 19 号无法被屏蔽,这两个信号的阻塞位永远为1。内核会拒绝所有想要屏蔽它们的操作。
handler表
-
handler 表并非 sigset_t 类型,它是一个函数指针数组,数组中存储着各个信号的处理函数指针;注意:数组下标 + 1 = 信号编号。
-
以 signal(2, myhandler) 为例,底层会将 2 号信号 对应的数组下标 1 处的元素,替换为我们自定义的 myhandler 函数指针。这样下次在处理 2 号信号时,就会使用我们提供的自定义处理方法。
-
什么是信号的忽略和默认处理?忽略与默认是信号的两种基础处理方式,它们在 handler 表中存储的内容分别为 SIG_IGN 和 SIG_DFL;这两个宏的本质,就是将整数 1 和 0 强制转换成函数指针类型后存入数组。
1.3 信号集操作函数

- C 标准库(遵循 POSIX 标准)提供了一组专用的信号集操作函数,这些函数专门用于对 sigset_t 类型的信号集变量进行初始化、添加信号、删除信号、判断信号是否存在等操作;它们仅在用户态工作,不直接与内核交互,是我们操作信号集的基础工具函数。用户态和内核态这两个概念下一篇博客会讲到。
- sigemptyset:清空信号集,所有比特位置 0。
- sigfillset:填满信号集,所有比特位置 1。
- sigaddset:向信号集中添加一个指定信号。
- sigdelset:从信号集中删除一个指定信号。
- sigismember:判断指定信号是否在信号集中。
这 5 个信号集操作函数的底层本质都是位运算;理论上我们可以手动通过位运算实现相同功能,但绝不建议这样做。
这 5 个信号都是成功返回 0,失败返回 -1。
- 操作系统允许用户控制信号的核心本质,就是访问和操作 block、pending、handler 这三张内核维护的表;系统为我们提供了专用的系统调用接口,例如 sigprocmask(操作 block 表)、sigpending(查询 pending 表),配合相关接口可完成对三张表的修改与管理。


- sigprocmask 函数用于修改进程的 block 表(信号屏蔽字):第一个参数 how:指定修改 block 表的方式,包含三种取值:SIG_BLOCK:将 set 中的信号追加屏蔽到 block 表;SIG_UNBLOCK:对 set 中的信号解除屏蔽;SIG_SETMASK:直接用 set 替换当前的 block 表。第二个参数 set:传入我们提前准备好的 sigset_t 信号集(待操作的目标集合);传入 NULL 表示不修改 block 表。第三个参数 oldset:输出型参数,用于保存修改前的旧 block 表;传入 NULL 表示不保存旧值。

- sigpending 函数用于获取当前进程的未决信号集(pending 表);它的参数是一个输出型参数,内核会将进程的 pending 表拷贝到该参数指向的变量中,该函数仅用于读取 pending 表,无法修改。
- 修改 pending 表我们之间就已经讲了5种方法,也就是信号产生的5种方法。
- 使用 sigprocmask 和 sigpending 这两个函数,必须包含标准头文件 <signal.h>。接下来对这些函数进行测试:
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
void PrintPending(sigset_t& pending)
{
for (int signo = 31; signo > 0; signo--)
{
if (sigismember(&pending, signo))
std::cout << 1;
else
std::cout << 0;
}
std::cout << std::endl;
}
void handler(int signo)
{
std::cout << "处理完成:" << signo << std::endl;
}
int main()
{
// 0.修改信号处理方法
signal(2, handler);
// 1.屏蔽信号
sigset_t block_set, old_set;
sigemptyset(&block_set);
sigemptyset(&old_set);
sigaddset(&block_set, 2);
int n = sigprocmask(SIG_BLOCK, &block_set, &old_set);
(void)n;
int cnt = 8;
while (true)
{
sigset_t pending;
int n = sigpending(&pending);
(void)n;
// 2. 查看pending表
PrintPending(pending);
//3. 解除屏蔽
if (cnt == 0)
{
int n = sigprocmask(SIG_SETMASK, &old_set, nullptr);
(void)n;
}
cnt--;
sleep(1);
}
return 0;
}

- 这个代码就是屏蔽 2 号信号,然后使用2号信号,查看pending表是否有被阻塞,信号对应比特位是否为1,5秒之后解除阻塞,查看pending表信号递达成功之后对应比特位是否为0。
- pending 信号对应比特位是在 handler 之前还原为0,还是在之后?是在之前,我们可以通过修改 handler 函数进行证明。
cpp
void handler(int signo)
{
std::cout << "------------------------------" << std::endl;
std::cout << "处理完成:" << signo << std::endl;
sigset_t pending;
sigpending(&pending);
PrintPending(pending);
std::cout << "------------------------------" << std::endl;
}

-
可以看到,在执行 handler 函数体的时候信号2对应比特位就已经变回0了。所以不可能在hanler函数调用之后,而是在之前,且 handler 函数本身没有改 pending 表能力。
-
被阻塞的普通信号,在解除阻塞的一瞬间,信号就会被立即递达,执行处理函数。
今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~
让我们共同努力, 一起走下去!