目录
[3.1 总结](#3.1 总结)
Linux 信号是系统用来通知进程事件的异步方式。很多时候进程暂时没空处理信号,系统就需要先把信号暂时"存起来",避免丢失或乱序。日常开发中遇到的信号不生效、延迟触发、莫名丢失等问题,大多都出在信号的保存和状态管理上。本文就聚焦信号如何被内核保存、如何暂存、状态如何切换,通俗讲清未决、阻塞、递达的区别,以及内核位图、信号集和信号屏蔽的底层逻辑,并结合代码演示完整过程。
当前内容:

一信号其他相关常见概念
• 实际执行信号的处理动作称为信号递达(Delivery)
• 信号从产生到递达之间的状态,称为信号未决(Pending)。
• 进程可以选择阻塞 (Block )某个信号。
• 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
• 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动
作。
二信号在内核中的表示

在 Linux 内核中,每一个运行的进程(无论是 bash、nginx 还是你的 hello_world)都对应一个名为 task_struct 的庞大结构体。可以把它想象成进程的"身份证"和"档案袋"。
这张图的核心在于,它提取了 task_struct 中关于信号管理的三个最关键的数据成员:
-
block (阻塞位图) 这是一个位图(bitmap),每一位对应一个信号编号。例如位 2 对应
SIGINT。如果该位为1,表示进程屏蔽/阻塞了这个信号。内核收到这个信号后,不会立即处理它,而是会将其"挂起"。 -
pending (待处理位图) 这也是一个位图,记录了已经发给进程,但尚未被处理 的信号。当内核收到一个信号时,首先查看
block位图:-
如果 block 位为 0 :直接将对应的
pending位设为 1。 -
如果 block 位为 1 :将对应的
pending位设为 1,然后将信号丢入"等待队列"。
-
-
handler (函数处理映射表)
这是信号处理流程的最终出口。当进程决定真正处理 一个信号时(即
pending中有信号,且对应的block被解除),内核会查找这张表。表中每一项(对于每个信号)提供了三种处理选项:SIG_DFL (Default) :执行系统默认动作。对于大多数信号,默认动作就是 终止进程 。SIG_IGN (Ignore) :彻底忽略 这个信号。即便
pending位为 1,内核也会直接清空它,不执行任何动作。用户自定义函数 :此时指向一个内存地址。当信号处理时,内核会从内核态切入到用户空间 ,跳转到这个地址,执行用户写的void sighandler(int signo) { ... }代码。
这三者就像三道关卡,决定了信号从产生到最终执行的命运。
源码:

struct sighand_struct定义

sigset_t定义:

struct sigpending定义:


三sigset_t
定义:
cpp
typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t;

其中:
-
_NSIG_WORDS是一个宏,定义为(_NSIG / _NSIG_BPW)。 -
_NSIG是信号总数(通常是 64,对应 1~64 号信号)。 -
_NSIG_BPW是unsigned long的位数(在 32 位系统上是 32)。 -
所以
sigset_t本质就是一个unsigned long类型的数组
对于 32 位系统,sigset_t 展开后就是:
cpp
typedef struct {
unsigned long sig[2]; // 64 / 32 = 2
} sigset_t;
-
sig[0]存储信号 1~32 的位图。 -
sig[1]存储信号 33~64 的位图。
每一位(bit)对应一个信号:
-
0:信号未产生/未阻塞。
-
1:信号已产生(pending)或被阻塞(block)。
从上图来看,每个信号只有一个bit的未决标志, 非0即1, 不记录该信号产生了多少次,阻塞标志也是这样表示的。因此, 未决和阻塞标志可以用相同的数据类型sigset_t来存储, , 这个类型可以表示每个信号的"有效"或"无效"状态, 在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞, 而在未决信号集中"有 效"和"无效"的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。塞信号集也叫做当前进程的 这里的"屏蔽"应该理解为阻塞而不是忽略。sigset_t称为信号集信号屏蔽字(Signal Mask),
3.1 总结
-
本质 :
sigset_t是一个位图 ,用数组unsigned long sig[N]实现,每个 bit 代表一个信号。 -
作用 :它被用于
task_struct中的blocked(阻塞)和pending(待处理)位图。
四信号集操作函数
sigset_t类型对于每种信号用一个bit表示"有效"或"无效"状态, 至于这个类型内部如何存储这些
bit则依赖于系统实现, 从使用者的角度是不必关心的, 使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释, 比如用printf直接打印sigset_t变量是没有意义的。
cpp
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
• 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含
任何有效信号。
• 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号
包括系 统支持的所有信号。
• 注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。 初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删
除某种有效信号。这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含 某种 信号,若包含则返回1,不包含则返回0,出错返回-1。
五sigprocmask
调用函数sigprocmask 可以读取或更改进程的信号屏蔽字(阻塞信号集)。
cpp
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1

| 参数 | 说明 | 取值 |
|---|---|---|
| how | 指定如何修改信号屏蔽字 | SIG_BLOCK、SIG_UNBLOCK、SIG_SETMASK |
| set | 指向要设置的信号集 | 非空:用于修改;NULL:忽略 |
| oldset | 保存旧的信号屏蔽字 | 非空:保存旧值;NULL:不保存 |
how参数的可选值:

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一
个信号递达。
六sigpending
sigpending可以读取当前进程的未决信号集,通过set参数传出。
cpp
#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。
调用成功则返回0,出错则返回-1

七演示代码
1)这个实验演示了信号的阻塞、未决和递达三态转换:
阻塞SIGINT后发送的信号会进入未决状态,解除阻塞后信号才被递达处理。
cpp
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main() {
sigset_t set;
// 创建信号集并阻塞SIGINT
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigprocmask(SIG_BLOCK, &set, NULL);
printf("SIGINT被阻塞,发送信号测试:\n");
kill(getpid(), SIGINT); // 发送信号,但会被阻塞
// 检查未决信号
sigset_t pending;
sigpending(&pending);
printf("未决信号中是否有SIGINT: %s\n",
sigismember(&pending, SIGINT) ? "是" : "否");
// 解除阻塞
sigprocmask(SIG_UNBLOCK, &set, NULL);
printf("解除阻塞\n");
return 0;
}

2)这个实验演示了信号屏蔽、未决状态和递达的过程:
核心流程:
-
先屏蔽2号信号(SIGINT)
-
期间发送的2号信号会处于未决状态(pending位图置1)
-
15秒后解除屏蔽 ,未决信号被递达,执行自定义处理函数
cpp
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cstdio>
#include <iostream>
void PrintPending(sigset_t& pending) {
std::cout << "curr process[" << getpid() << "]pending: ";
for (int signo = 31; signo >= 1; signo--) {
if (sigismember(&pending, signo)) {
std::cout << 1;
} else {
std::cout << 0;
}
}
std::cout << "\n";
}
void handler(int signo) {
std::cout << signo << " 号信号被递达!!!" << std::endl;
std::cout << "-------------------------------" << std::endl;
sigset_t pending;
sigpending(&pending);
PrintPending(pending);
std::cout << "-------------------------------" << std::endl;
}
int main() {
// 0. 捕捉2号信号
signal(2, handler); // 自定义捕捉
// signal(2, SIG_IGN); // 忽略一个信号
// signal(2, SIG_DFL); // 信号的默认处理动作
// 1. 屏蔽2号信号
sigset_t block_set, old_set;
sigemptyset(&block_set);
sigemptyset(&old_set);
sigaddset(&block_set,
SIGINT); // 我们有没有修改当前进行的内核block表呢???10
// 1.1 设置进入进程的Block表中
sigprocmask(
SIG_BLOCK, &block_set,
&old_set); // 真正的修改当前进行的内核block表,完成了对2号信号的屏蔽!
int cnt = 15;
while (true) {
// 2. 获取当前进程的pending信号集
sigset_t pending;
sigpending(&pending);
// 3. 打印pending信号集
PrintPending(pending);
cnt--;
// 4. 解除对2号信号的屏蔽
if (cnt == 0) {
std::cout << "解除对2号信号的屏蔽!!!" << std::endl;
sigprocmask(SIG_SETMASK, &old_set, &block_set);
}
sleep(1);
}
}

八结语
简单来说,Linux 内核就是靠位图和信号集来统一管理信号的保存状态。通过阻塞位图、未决位图配合信号处理函数表,系统可以精准记住哪些信号被挡住、哪些信号已经产生但还没来得及处理,完美解决了异步信号容易丢失、混乱的问题。搞懂这套信号保存机制,就能彻底理解信号阻塞、延迟、未处理的本质,平时排查程序异常、信号失效问题也能更加得心应手。