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

task_struct是 Linux 内核描述进程的核心结构体,其中与信号保存直接相关的字段如下:
struct task_struct {
// 信号处理动作结构体(存储每个信号的处理函数)
struct sighand_struct *sighand;
// 阻塞信号集(信号屏蔽字):标记哪些信号被阻塞
sigset_t blocked;
// 未决信号集:标记哪些信号已产生但未递达
struct sigpending pending;
// 其他字段...
};这三个字段的关系可以概括为:
blocked:“黑名单”,记录进程要阻塞的信号;pending:“待办清单”,记录已产生但未递达的信号;sighand:“处理手册”,记录每个信号的处理动作(默认 / 忽略 / 自定义)。 sigset_t是 Linux 内核定义的 “信号集” 类型,用于存储多个信号的 “有效” 或 “无效” 状态。其本质是一个位图(bitmap),每个 bit 对应一个信号(bit 位的位置对应信号编号,bit 位的值对应状态:1 为有效,0 为无效)。
// 不同系统的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已产生且未递达。sigemptyset、sigaddset),不能直接修改 bit 位(不同系统的实现可能不同,直接操作会导致兼容性问题);sigpending中的链表记录产生次数,本章不讨论。 sigpending结构体用于存储进程的未决信号,定义如下:
struct sigpending {
// 链表:用于存储实时信号(支持排队)
struct list_head list;
// 位图:用于存储常规信号的未决状态
sigset_t signal;
};signal字段(sigset_t 类型)记录;list链表记录,支持多次产生的排队处理。 假设进程的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类型的信号集(阻塞信号集和未决信号集)。这些函数是用户态程序控制信号保存的核心接口,必须熟练掌握。
这两个函数用于初始化sigset_t变量,任何 sigset_t 变量使用前必须先初始化,否则状态不确定。
#include <signal.h>
// 功能:将set指向的信号集初始化为空,所有信号的对应bit置0
int sigemptyset(sigset_t *set);sigaddset添加需要操作的信号。#include <signal.h>
// 功能:将set指向的信号集初始化为满,所有信号的对应bit置1
int sigfillset(sigset_t *set);sigdelset删除不需要操作的信号。#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;
}编译运行:
g++ sigset_init.cpp -o sigset_init
./sigset_init输出:
信号集初始化为空成功
信号集初始化为满成功这两个函数用于向信号集中添加或删除某个特定信号。
#include <signal.h>
// 功能:将signo对应的信号添加到set指向的信号集中(该信号的bit置1)
int sigaddset(sigset_t *set, int signo);set:指向要操作的信号集;signo:要添加的信号编号(如 2 对应 SIGINT);#include <signal.h>
// 功能:将signo对应的信号从set指向的信号集中删除(该信号的bit置0)
int sigdelset(sigset_t *set, int signo);sigaddset;#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;
}编译运行:
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号)在信号集中 sigismember函数用于查询某个信号是否在信号集中,是信号集操作中最常用的查询函数。
#include <signal.h>
// 功能:查询signo对应的信号是否在set指向的信号集中
int sigismember(const sigset_t *set, int signo); sigprocmask函数是用户态程序修改进程阻塞信号集(信号屏蔽字) 的核心接口,支持 “添加阻塞”“解除阻塞”“设置阻塞集” 三种操作。
#include <signal.h>
// 功能:读取或修改进程的阻塞信号集(信号屏蔽字)
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);(1)how:指定修改阻塞信号集的方式,有三种取值:

(2)set:指向要操作的信号集:
set为非空指针:根据how参数修改阻塞信号集;set为空指针:不修改阻塞信号集,仅通过oset读取当前阻塞信号集。(3)oset:用于存储修改前的阻塞信号集:
oset为非空指针:将修改前的阻塞信号集保存到oset中,便于后续恢复;oset为空指针:不保存修改前的阻塞信号集。errno(如EINVAL表示how参数无效)。sigprocmask解除了对某个未决信号的阻塞,则该信号会立即递达(在sigprocmask返回前);SIGKILL(9 号)和SIGSTOP(19 号)信号不可阻塞,即使通过sigprocmask添加到阻塞信号集,也不会生效。#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;
}编译运行:
g++ sigprocmask_block.cpp -o sigprocmask_block
./sigprocmask_block测试步骤:
Ctrl+C和Ctrl+\;输出示例:
进程PID:12345
已阻塞SIGINT和SIGQUIT信号,持续10秒...
期间按下Ctrl+C(SIGINT)或Ctrl+\(SIGQUIT),信号会被保存为未决状态
^C^\ # 按下Ctrl+C和Ctrl+\,无反应
已解除阻塞,未决信号会立即递达
捕获到信号:2(SIGINT),信号递达成功!
捕获到信号:3(SIGQUIT),信号递达成功!
进程正常运行中...
进程正常运行中...#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;
}编译运行:
g++ sigprocmask_kill.cpp -o sigprocmask_kill
./sigprocmask_kill测试步骤:
kill -9 进程PID;SIGKILL信号不可阻塞。 sigpending函数用于读取当前进程的未决信号集,让用户态程序可以查询哪些信号已产生但未递达。
#include <signal.h>
// 功能:读取当前进程的未决信号集,保存到set指向的变量中
int sigpending(sigset_t *set);set:指向存储未决信号集的变量;#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;
}编译运行:
g++ sigpending_print.cpp -o sigpending_print
./sigpending_print测试步骤:
Ctrl+C(2 号)和Ctrl+\(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 常规信号(1-33 号)在递达前产生多次,只会被记录一次(位图的 bit 位只能是 0 或 1)。例如:
SIGINT后,连续按下 3 次Ctrl+C,未决信号集中SIGINT的 bit 位仍为 1;#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,解除阻塞后处理函数仅执行一次,证明常规信号不支持排队。
sigset_t变量未初始化时,内部 bit 位状态不确定,直接调用sigaddset、sigdelset等函数会导致不可预期的结果。因此,任何 sigset_t 变量使用前,必须通过 sigemptyset 或 sigfillset 初始化。
Linux 中有两个信号无法通过sigprocmask阻塞:
SIGKILL(9 号):强制终止进程;SIGSTOP(19 号):强制暂停进程。这是内核的强制规定,目的是保证操作系统能绝对控制进程,避免进程通过阻塞信号变成 “无法管理的僵尸进程”。
信号处理函数可能在任意时刻被调用,若处理函数中调用了不可重入函数(如malloc、printf、标准 I/O 库函数),可能导致数据错乱或程序崩溃。因此,信号处理函数应尽量简洁,仅执行必要的操作(如设置全局变量、发送通知)。
信号保存是 Linux 信号机制的核心环节,理解其底层原理需要结合内核数据结构和实战验证。本文的所有代码都经过 Ubuntu 20.04 环境验证,建议大家亲手编译运行,通过修改代码(如阻塞不同信号、多次产生信号)加深理解。如果在学习过程中遇到问题,欢迎在评论区留言讨论!