前言
在 Linux 进程信号的生命周期中,"信号保存" 是连接 "信号产生" 与 "信号处理" 的关键桥梁。当信号被操作系统产生后,并不会立即递达给进程执行处理动作 ------ 进程可能正在执行高优先级任务,也可能主动阻塞了该信号。此时,信号会被 "暂存" 起来,直到满足递达条件。
你是否好奇:信号被保存在哪里?进程如何记录 "有信号待处理"?阻塞信号和未决信号有何区别?信号集又是如何管理这些状态的?本文将基于 Linux 内核原理,结合实战代码,从概念解析、内核存储结构、信号集操作函数、实战验证四个维度,深度拆解信号保存的底层逻辑,带你彻底搞懂信号保存的核心机制。
一、信号保存的核心概念:未决、阻塞与递达
在深入底层实现之前,我们必须先理清三个核心概念 ------信号递达 、信号未决 、信号阻塞。这三个概念是理解信号保存的基础,也是面试高频考点。
1.1 三大核心概念的通俗解释
我们依然用 "快递" 场景类比,帮助快速理解:
- 信号递达(Delivery):快递被你成功签收并处理(执行默认 / 忽略 / 自定义动作),是信号生命周期的终点;
- 信号未决(Pending):快递已到达楼下(信号产生),但你暂时无法取件(如正在开会),快递处于 "等待签收" 状态;
- 信号阻塞(Block):你提前告知快递员 "暂时不要送货"(进程主动设置阻塞),即使快递已到达,也会一直处于未决状态,直到你取消阻塞。
1.2 三大概念的官方定义与关键区别
1.2.1 信号递达
实际执行信号处理动作的过程,称为信号递达。递达的动作只有三种:
- 执行默认动作 (如
SIGINT默认终止进程);- 执行忽略动作(进程收到信号后不做任何操作);
- 执行自定义动作(调用用户注册的信号处理函数)。
1.2.2 信号未决
信号从产生到递达之间的状态,称为信号未决。此时信号已被操作系统识别并记录在进程的 PCB 中,但由于某些原因(如进程阻塞该信号、进程正在执行高优先级任务),尚未执行处理动作。
1.2.3 信号阻塞
进程可以通过设置**"信号屏蔽字"(Signal Mask)**,主动阻止某个信号的递达。被阻塞的信号产生后,会一直处于未决状态,直到进程解除对该信号的阻塞,才会执行递达动作。
1.3 阻塞与忽略的核心区别(重点!)
很多初学者会混淆 "阻塞" 和 "忽略",但二者本质完全不同:
- 阻塞是 "不让信号递达":信号处于未决状态,根本不会触发处理动作;
- 忽略是 "信号已递达,但不处理":信号已经到达进程,只是进程选择不执行任何操作。
举个例子:
- 阻塞
SIGINT信号 :按下Ctrl+C后,信号被保存在 PCB 的未决信号集中,进程完全感知不到该信号,继续正常运行;- 忽略
SIGINT信号 :按下Ctrl+C后,信号正常递达,但进程执行 "忽略" 动作,不终止也不反馈,继续运行。
1.4 信号保存的核心流程
结合三大概念,信号保存的完整流程可以总结为:
信号产生 → 操作系统检查进程是否阻塞该信号 →
① 未阻塞:直接递达并执行处理动作 → 流程结束;
② 已阻塞:将信号标记为"未决",保存到进程PCB的未决信号集中 →
进程解除阻塞 → 操作系统检测到未决信号 → 信号递达并执行处理动作 → 流程结束
二、内核中的信号保存:PCB 中的关键数据结构
信号的保存本质是操作系统在进程的 PCB(进程控制块)中记录信号的未决状态和阻塞状态 。Linux 内核(2.6.18 版本)中,与信号保存相关的核心数据结构主要有三个:task_struct、sigset_t、sigpending。下面是信号在内核中的表示示意图:

2.1 进程控制块(task_struct)中的信号相关字段
task_struct是 Linux 内核描述进程的核心结构体,其中与信号保存直接相关的字段如下:
cpp
struct task_struct {
// 信号处理动作结构体(存储每个信号的处理函数)
struct sighand_struct *sighand;
// 阻塞信号集(信号屏蔽字):标记哪些信号被阻塞
sigset_t blocked;
// 未决信号集:标记哪些信号已产生但未递达
struct sigpending pending;
// 其他字段...
};
这三个字段的关系可以概括为:
blocked:"黑名单",记录进程要阻塞的信号;pending:"待办清单",记录已产生但未递达的信号;sighand:"处理手册",记录每个信号的处理动作(默认 / 忽略 / 自定义)。
2.2 信号集(sigset_t):阻塞与未决状态的存储载体
sigset_t是 Linux 内核定义的 "信号集" 类型,用于存储多个信号的 "有效" 或 "无效" 状态。其本质是一个位图(bitmap),每个 bit 对应一个信号(bit 位的位置对应信号编号,bit 位的值对应状态:1 为有效,0 为无效)。
2.2.1 sigset_t 的底层实现(简化版)
cpp
// 不同系统的sigset_t长度不同,通常为32位或64位,对应支持32个或64个信号
typedef struct {
unsigned long sig[2]; // 假设为64位,支持64个信号(编号1-64)
} sigset_t;
- 对于阻塞信号集 (
blocked):某个 bit 位为 1,表示该信号被阻塞;- 对于未决信号集 (
pending->signal):某个 bit 位为 1,表示该信号已产生且未递达。
例如:
- 若
blocked的第 2 位(对应SIGINT信号)为 1,表示SIGINT被阻塞;- 若
pending->signal的第 3 位(对应SIGQUIT信号)为 1,表示SIGQUIT已产生且未递达。
2.2.2 sigset_t 的核心特点
- 信号集的操作必须通过内核提供的专用函数(如
sigemptyset、sigaddset),不能直接修改 bit 位(不同系统的实现可能不同,直接操作会导致兼容性问题);- 每个信号在信号集中只有一个 bit 位,因此常规信号(1-33)在递达前产生多次,只会被记录一次(位图无法记录产生次数);
- 实时信号(34-64)支持排队,会通过
sigpending中的链表记录产生次数,本章不讨论。
2.3 未决信号集结构体(sigpending)
sigpending结构体用于存储进程的未决信号,定义如下:
cpp
struct sigpending {
// 链表:用于存储实时信号(支持排队)
struct list_head list;
// 位图:用于存储常规信号的未决状态
sigset_t signal;
};
- 常规信号的未决状态通过
signal字段(sigset_t 类型)记录;- 实时信号的未决状态通过
list链表记录,支持多次产生的排队处理。
2.4 内核中信号保存的直观示例
假设进程的blocked、pending->signal、sighand字段如下表所示,我们来分析信号的保存与处理逻辑:
| 信号 | 信号编号 | blocked(阻塞) | pending(未决) | 处理动作 |
|---|---|---|---|---|
| SIGHUP | 1 | 0(未阻塞) | 0(未产生) | SIG_DFL(终止) |
| SIGINT | 2 | 1(已阻塞) | 1(已产生) | SIG_IGN(忽略) |
| SIGQUIT | 3 | 1(已阻塞) | 0(未产生) | 自定义函数 |
| SIGKILL | 9 | 0(不可阻塞) | 0(未产生) | SIG_DFL(终止) |
分析结果:
- **
SIGINT**信号已产生,但被阻塞,因此处于未决状态,即使处理动作是 "忽略",也不会递达;- **
SIGHUP**信号未产生、未阻塞,若产生则直接递达并执行 "终止" 动作;- **
SIGQUIT**信号未产生、已阻塞,若产生则进入未决状态,直到解除阻塞才会执行自定义函数;- **
SIGKILL**信号不可阻塞(内核强制规定),若产生则直接递达并执行 "终止" 动作。
三、信号集操作函数:用户态控制信号的阻塞与未决
Linux 内核提供了一组专用函数,用于操作sigset_t类型的信号集(阻塞信号集和未决信号集)。这些函数是用户态程序控制信号保存的核心接口,必须熟练掌握。
3.1 信号集初始化函数:sigemptyset 与 sigfillset
这两个函数用于初始化**sigset_t**变量,任何 sigset_t 变量使用前必须先初始化,否则状态不确定。
3.1.1 sigemptyset:初始化信号集为空(所有 bit 置 0)
cpp
#include <signal.h>
// 功能:将set指向的信号集初始化为空,所有信号的对应bit置0
int sigemptyset(sigset_t *set);
- 返回值:成功返回 0,失败返回 - 1;
- 用途:初始化一个空的信号集,后续通过
sigaddset添加需要操作的信号。
3.1.2 sigfillset:初始化信号集为满(所有 bit 置 1)
cpp
#include <signal.h>
// 功能:将set指向的信号集初始化为满,所有信号的对应bit置1
int sigfillset(sigset_t *set);
- 返回值:成功返回 0,失败返回 - 1;
- 用途:初始化一个包含所有信号的信号集,后续通过
sigdelset删除不需要操作的信号。
实战代码:信号集初始化
cpp
#include <iostream>
#include <signal.h>
using namespace std;
int main()
{
sigset_t set;
// 初始化信号集为空
int ret = sigemptyset(&set);
if (ret == -1)
{
perror("sigemptyset failed");
return 1;
}
cout << "信号集初始化为空成功" << endl;
// 重新初始化为满
ret = sigfillset(&set);
if (ret == -1)
{
perror("sigfillset failed");
return 1;
}
cout << "信号集初始化为满成功" << endl;
return 0;
}
编译运行:
bash
g++ sigset_init.cpp -o sigset_init
./sigset_init
输出:
信号集初始化为空成功
信号集初始化为满成功
3.2 信号集添加 / 删除函数:sigaddset 与 sigdelset
这两个函数用于向信号集中添加或删除某个特定信号。
3.2.1 sigaddset:向信号集中添加一个信号(bit 置 1)
cpp
#include <signal.h>
// 功能:将signo对应的信号添加到set指向的信号集中(该信号的bit置1)
int sigaddset(sigset_t *set, int signo);
- 参数:
set:指向要操作的信号集;signo:要添加的信号编号(如 2 对应 SIGINT);
- 返回值:成功返回 0,失败返回 - 1。
3.2.2 sigdelset:从信号集中删除一个信号(bit 置 0)
cpp
#include <signal.h>
// 功能:将signo对应的信号从set指向的信号集中删除(该信号的bit置0)
int sigdelset(sigset_t *set, int signo);
- 参数与返回值同
sigaddset;- 若信号集原本不包含该信号,删除操作仍视为成功。
实战代码:信号集添加与删除
cpp
#include <iostream>
#include <signal.h>
using namespace std;
// 打印信号是否在信号集中
void print_sig_member(sigset_t &set, int signo, const char *sig_name)
{
int ret = sigismember(&set, signo);
if (ret == 1)
{
cout << sig_name << "(" << signo << "号)在信号集中" << endl;
}
else if (ret == 0)
{
cout << sig_name << "(" << signo << "号)不在信号集中" << endl;
}
else
{
perror("sigismember failed");
}
}
int main()
{
sigset_t set;
// 初始化信号集为空
sigemptyset(&set);
cout << "初始化后:" << endl;
print_sig_member(set, SIGINT, "SIGINT");
print_sig_member(set, SIGQUIT, "SIGQUIT");
// 向信号集中添加SIGINT(2号)
int ret = sigaddset(&set, SIGINT);
if (ret == -1)
{
perror("sigaddset SIGINT failed");
return 1;
}
cout << "\n添加SIGINT后:" << endl;
print_sig_member(set, SIGINT, "SIGINT");
print_sig_member(set, SIGQUIT, "SIGQUIT");
// 向信号集中添加SIGQUIT(3号)
ret = sigaddset(&set, SIGQUIT);
if (ret == -1)
{
perror("sigaddset SIGQUIT failed");
return 1;
}
cout << "\n添加SIGQUIT后:" << endl;
print_sig_member(set, SIGINT, "SIGINT");
print_sig_member(set, SIGQUIT, "SIGQUIT");
// 从信号集中删除SIGINT(2号)
ret = sigdelset(&set, SIGINT);
if (ret == -1)
{
perror("sigdelset SIGINT failed");
return 1;
}
cout << "\n删除SIGINT后:" << endl;
print_sig_member(set, SIGINT, "SIGINT");
print_sig_member(set, SIGQUIT, "SIGQUIT");
return 0;
}
编译运行:
bash
g++ sigset_add_del.cpp -o sigset_add_del
./sigset_add_del
输出:
初始化后:
SIGINT(2号)不在信号集中
SIGQUIT(3号)不在信号集中
添加SIGINT后:
SIGINT(2号)在信号集中
SIGQUIT(3号)不在信号集中
添加SIGQUIT后:
SIGINT(2号)在信号集中
SIGQUIT(3号)在信号集中
删除SIGINT后:
SIGINT(2号)不在信号集中
SIGQUIT(3号)在信号集中
3.3 信号集查询函数:sigismember
sigismember函数用于查询某个信号是否在信号集中,是信号集操作中最常用的查询函数。
cpp
#include <signal.h>
// 功能:查询signo对应的信号是否在set指向的信号集中
int sigismember(const sigset_t *set, int signo);
- 返回值:
- 1:信号在信号集中;
- 0:信号不在信号集中;
- -1:调用失败(如信号编号无效)。
3.4 阻塞信号集操作函数:sigprocmask(核心!)
sigprocmask函数是用户态程序修改进程阻塞信号集(信号屏蔽字) 的核心接口,支持 "添加阻塞""解除阻塞""设置阻塞集" 三种操作。
3.4.1 函数原型
cpp
#include <signal.h>
// 功能:读取或修改进程的阻塞信号集(信号屏蔽字)
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
3.4.2 参数详解
(1)how:指定修改阻塞信号集的方式,有三种取值:

(2)set:指向要操作的信号集:
- 若
set为非空指针:根据how参数修改阻塞信号集;- 若
set为空指针:不修改阻塞信号集,仅通过oset读取当前阻塞信号集。
(3)oset:用于存储修改前的阻塞信号集:
- 若
oset为非空指针:将修改前的阻塞信号集保存到oset中,便于后续恢复;- 若
oset为空指针:不保存修改前的阻塞信号集。
3.4.3 返回值
- 成功返回 0;
- 失败返回 - 1,并设置
errno(如EINVAL表示how参数无效)。
3.4.4 关键特性
- 若调用
sigprocmask解除了对某个未决信号的阻塞,则该信号会立即递达 (在sigprocmask返回前);SIGKILL(9 号)和SIGSTOP(19 号)信号不可阻塞,即使通过sigprocmask添加到阻塞信号集,也不会生效。
实战代码 1:设置信号阻塞与解除阻塞
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
// 自定义信号处理函数
void sig_handler(int signo)
{
cout << "\n捕获到信号:" << signo << "(" << (signo == SIGINT ? "SIGINT" : "SIGQUIT") << "),信号递达成功!" << endl;
}
int main()
{
cout << "进程PID:" << getpid() << endl;
// 注册SIGINT和SIGQUIT的处理函数
signal(SIGINT, sig_handler);
signal(SIGQUIT, sig_handler);
sigset_t block_set, old_set;
// 初始化阻塞信号集为空
sigemptyset(&block_set);
sigemptyset(&old_set);
// 向阻塞信号集中添加SIGINT(2号)和SIGQUIT(3号)
sigaddset(&block_set, SIGINT);
sigaddset(&block_set, SIGQUIT);
// 设置阻塞信号集(SIG_BLOCK:新增阻塞)
int ret = sigprocmask(SIG_BLOCK, &block_set, &old_set);
if (ret == -1)
{
perror("sigprocmask SIG_BLOCK failed");
return 1;
}
cout << "已阻塞SIGINT和SIGQUIT信号,持续10秒..." << endl;
cout << "期间按下Ctrl+C(SIGINT)或Ctrl+\\(SIGQUIT),信号会被保存为未决状态" << endl;
// 睡眠10秒,期间可以按下Ctrl+C或Ctrl+\测试
sleep(10);
// 解除阻塞(SIG_SETMASK:恢复为原来的阻塞信号集)
ret = sigprocmask(SIG_SETMASK, &old_set, NULL);
if (ret == -1)
{
perror("sigprocmask SIG_SETMASK failed");
return 1;
}
cout << "\n已解除阻塞,未决信号会立即递达" << endl;
// 继续运行,观察信号递达情况
while (true)
{
sleep(1);
cout << "进程正常运行中..." << endl;
}
return 0;
}
编译运行:
bash
g++ sigprocmask_block.cpp -o sigprocmask_block
./sigprocmask_block
测试步骤:
- 运行程序后,10 秒内按下
Ctrl+C和Ctrl+\;- 观察终端:程序不会立即执行信号处理函数,因为信号被阻塞并保存为未决状态;
- 10 秒后解除阻塞,未决信号会立即递达,终端打印信号捕获信息。
输出示例:
进程PID:12345
已阻塞SIGINT和SIGQUIT信号,持续10秒...
期间按下Ctrl+C(SIGINT)或Ctrl+\(SIGQUIT),信号会被保存为未决状态
^C^\ # 按下Ctrl+C和Ctrl+\,无反应
已解除阻塞,未决信号会立即递达
捕获到信号:2(SIGINT),信号递达成功!
捕获到信号:3(SIGQUIT),信号递达成功!
进程正常运行中...
进程正常运行中...
实战代码 2:验证 SIGKILL 不可阻塞
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
int main()
{
cout << "进程PID:" << getpid() << endl;
sigset_t block_set;
sigemptyset(&block_set);
// 尝试阻塞SIGKILL(9号)
sigaddset(&block_set, SIGKILL);
// 设置阻塞信号集
int ret = sigprocmask(SIG_BLOCK, &block_set, NULL);
if (ret == -1)
{
perror("sigprocmask failed");
return 1;
}
cout << "尝试阻塞SIGKILL信号(实际无效),持续20秒..." << endl;
cout << "可以通过另一个终端执行:kill -9 " << getpid() << " 测试" << endl;
// 睡眠20秒
sleep(20);
cout << "进程正常结束(若被kill -9终止,则不会打印此行)" << endl;
return 0;
}
编译运行:
bash
g++ sigprocmask_kill.cpp -o sigprocmask_kill
./sigprocmask_kill
测试步骤:
- 运行程序后,打开另一个终端,执行
kill -9 进程PID;- 观察原终端:程序会立即终止,证明
SIGKILL信号不可阻塞。
3.5 未决信号集读取函数:sigpending
sigpending函数用于读取当前进程的未决信号集,让用户态程序可以查询哪些信号已产生但未递达。
3.5.1 函数原型
cpp
#include <signal.h>
// 功能:读取当前进程的未决信号集,保存到set指向的变量中
int sigpending(sigset_t *set);
- 参数:
set:指向存储未决信号集的变量;- 返回值:成功返回 0,失败返回 - 1。
实战代码:读取并打印未决信号集
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
// 打印未决信号集(遍历1-31号常规信号)
void print_pending(sigset_t &pending)
{
cout << "当前未决信号集(1-31号):";
for (int signo = 1; signo <= 31; signo++)
{
if (sigismember(&pending, signo))
{
cout << signo << " ";
}
}
cout << endl;
}
int main()
{
cout << "进程PID:" << getpid() << endl;
sigset_t block_set, pending;
// 初始化信号集
sigemptyset(&block_set);
sigemptyset(&pending);
// 阻塞SIGINT(2号)和SIGQUIT(3号)
sigaddset(&block_set, SIGINT);
sigaddset(&block_set, SIGQUIT);
sigprocmask(SIG_BLOCK, &block_set, NULL);
cout << "已阻塞SIGINT(2号)和SIGQUIT(3号),持续15秒..." << endl;
cout << "期间按下Ctrl+C(2号)或Ctrl+\\(3号),观察未决信号集变化" << endl;
// 每隔2秒读取并打印一次未决信号集
for (int i = 0; i < 7; i++)
{
// 读取未决信号集
sigpending(&pending);
print_pending(pending);
sleep(2);
}
return 0;
}
编译运行:
bash
g++ sigpending_print.cpp -o sigpending_print
./sigpending_print
测试步骤:
- 运行程序后,在 15 秒内按下
Ctrl+C(2 号)和Ctrl+\(3 号);- 观察终端输出:未决信号集会新增 2 和 3 号信号,证明信号被成功保存。
输出示例:
进程PID:12346
已阻塞SIGINT(2号)和SIGQUIT(3号),持续15秒...
期间按下Ctrl+C(2号)或Ctrl+\(3号),观察未决信号集变化
当前未决信号集(1-31号):
当前未决信号集(1-31号):
^C # 按下Ctrl+C
当前未决信号集(1-31号):2
当前未决信号集(1-31号):2
^\ # 按下Ctrl+\
当前未决信号集(1-31号):2 3
当前未决信号集(1-31号):2 3
当前未决信号集(1-31号):2 3
四、常见问题与注意事项
4.1 常规信号的未决状态不支持排队
常规信号(1-33 号)在递达前产生多次,只会被记录一次(位图的 bit 位只能是 0 或 1)。例如:
- 阻塞
SIGINT后,连续按下 3 次Ctrl+C,未决信号集中SIGINT的 bit 位仍为 1;- 解除阻塞后,信号仅递达一次,处理函数仅执行一次。
验证代码
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void sigint_handler(int signo)
{
cout << "捕获到SIGINT信号(" << signo << "号),处理函数执行一次" << endl;
}
int main()
{
signal(SIGINT, sigint_handler);
sigset_t block_set;
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT);
sigprocmask(SIG_BLOCK, &block_set, NULL);
cout << "已阻塞SIGINT,连续按下3次Ctrl+C,然后等待5秒..." << endl;
sleep(5);
sigprocmask(SIG_UNBLOCK, &block_set, NULL);
cout << "已解除阻塞" << endl;
sleep(3);
return 0;
}
运行后连续按下 3 次Ctrl+C,解除阻塞后处理函数仅执行一次,证明常规信号不支持排队。
4.2 信号集操作前必须初始化
sigset_t变量未初始化时,内部 bit 位状态不确定,直接调用sigaddset、sigdelset等函数会导致不可预期的结果。因此,任何 sigset_t 变量使用前,必须通过 sigemptyset 或 sigfillset 初始化。
4.3 不可阻塞的信号
Linux 中有两个信号无法通过**sigprocmask**阻塞:
SIGKILL(9 号):强制终止进程;SIGSTOP(19 号):强制暂停进程。
这是内核的强制规定,目的是保证操作系统能绝对控制进程,避免进程通过阻塞信号变成 "无法管理的僵尸进程"。
4.4 信号处理函数中避免使用不可重入函数
信号处理函数可能在任意时刻被调用,若处理函数中调用了不可重入函数(如malloc、printf、标准 I/O 库函数),可能导致数据错乱或程序崩溃。因此,信号处理函数应尽量简洁,仅执行必要的操作(如设置全局变量、发送通知)。
总结
信号保存是 Linux 信号机制的核心环节,理解其底层原理需要结合内核数据结构和实战验证。本文的所有代码都经过 Ubuntu 20.04 环境验证,建议大家亲手编译运行,通过修改代码(如阻塞不同信号、多次产生信号)加深理解。如果在学习过程中遇到问题,欢迎在评论区留言讨论!
