一、信号生命周期与堵塞
1. 信号的生命周期
- 信号的生命周期的概念
信号从产生到最终被处理,会经历三个核心阶段:信号产生 → 信号未决 → 信号递达

- 信号生命周期的完整流程

- 各阶段详解
- 信号产生:终端按键、系统调用、软件条件、硬件异常。
- 信号未决(Pending):信号产生后,进入未决状态。未决信号被记录在进程的未决信号集中,同一个信号多次产生,通常只记录一次
- 信号阻塞(Block):进程可以选择阻塞某些信号,被阻塞的信号保持在未决状态,不会递达,解除阻塞后,信号才会递达。
- 信号递达(Delivery):信号实际到达进程,执行处理动作,处理方式有三种:
·默认:按系统预设方式处理(通常是终止进程)
·忽略:什么也不做
·捕捉:执行用户自定义的处理函数 - 阻塞vs忽略

阻塞 ≠ 忽略。阻塞是"不让进",忽略是"进了当没看见"。
2. 信号在内核中的表示
- 信号在内核中的数据结构
进程 PCB(task_struct)内部维护三套信号管理结构,均采用位图存储,位规则:1 代表生效,0 代表未生效

状态举例:SIGQUIT 的 block 位 = 1、pending 位 = 1 → 信号被阻塞,进入未决队列,解除阻塞后才会执行对应处理逻辑。 - 标准信号与实时信号核心差异
- 标准信号(1~34):不支持排队
信号阻塞期间重复多次发送同一信号,pending 位只会置 1 一次,解除阻塞后仅处理一次,多余信号直接丢失。 - 实时信号(34~64):支持排队
多次触发会依次入队,逐一递达处理;本章仅讲解标准信号。
- PCB内核原码简化定义
struct task_struct {
//...
struct sigpending pending; // 未决信号集
sigset_t blocked; // 阻塞信号掩码(信号屏蔽字)
struct sighand_struct *sighand; // 信号处理函数表
//...
};
- 信号完整流转流程

流程文字梳理:
- 信号产生,内核将对应 pending 位标记为 1;
- 若信号处于阻塞状态,信号留在未决队列,等待解除阻塞;
- 内核仅在进程从内核态切换回用户态时,统一校验 pending 信号;
- 匹配 handler 规则,执行信号处理动作,完成递达。
- 信号递达时机说明
信号不会立刻递达,内核只会在以下场景检查 pending 信号:
- 进程从内核态切换回用户态(最常见)
- 系统调用执行完毕
- sleep()、pause() 等阻塞函数返回
- 进程被调度恢复运行
补充理解:
如果进程长期运行在纯用户态、不触发任何系统调用,pending 信号暂时不会处理。平时按下Ctrl+C可以快速响应,是因为终端发送信号后,进程很快会因为时钟中断、系统调用进入内核态,从而触发信号检测。
3. 阻塞信号的操作
- sigset_t 信号集
sigset_t 本质是位图结构,用来描述信号集合。阻塞信号集(信号屏蔽字)、未决信号集都使用该类型。
每一个信号仅占用 1 个 bit,只有 0/1 两种状态:
- 未决位图:bit=1 → 信号已产生、尚未递达;不记录同一个信号触发多少次
- 阻塞位图:bit=1 → 信号被阻塞
区分含义: - 在阻塞信号集中,bit 有效 = 信号被阻塞;
- 在未决信号集中,bit 有效 = 信号处于未决状态。
阻塞信号集又称为进程信号屏蔽字 (Signal Mask)。
⚠️ 注意:屏蔽 = 阻塞,不等于忽略。
重要知识点:标准信号 (1~34) 不排队
同一个信号阻塞期间多次发送,pending 位依旧保持 1,解除阻塞后只会处理一次;
实时信号(34~64)支持排队,本章只讨论标准信号。
- sigset_t 的操作函数
sigset_t内部存储格式由系统实现决定,禁止直接打印、强行解析内存,只能调用系统提供接口操作。
#include <signal.h>
// 初始化:清空信号集,所有bit置0
int sigemptyset(sigset_t *set);
// 初始化:填满信号集,所有bit置1
int sigfillset(sigset_t *set);
// 将指定信号加入信号集
int sigaddset(sigset_t *set, int signo);
// 将指定信号从信号集移除
int sigdelset(sigset_t *set, int signo);
// 判断信号是否在集合中:存在返回1,不存在返回0,出错返回-1
int sigismember(const sigset_t *set, int signo);
✅ 使用规范:定义sigset_t变量后,必须先调用 sigemptyset /sigfillset 初始化。
- 其它操作函数
- sigprocmask:修改进程信号屏蔽字
作用:读取 / 更改当前进程阻塞信号集(block 位图)
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
// 返回值:成功0,失败-1

how三种模式

硬性限制:SIGKILL(9)、SIGSTOP(19) 无法阻塞、捕获、忽略,内核直接忽略相关设置。
关键特性:
调用 sigprocmask 解除若干未决信号阻塞时,函数返回前,内核至少递达其中一个信号。
简单使用范例:
sigset_t new_mask, old_mask;
sigemptyset(&new_mask);
// 仅读取当前屏蔽字
sigprocmask(0, NULL, &old_mask);
// 阻塞 SIGINT
sigaddset(&new_mask, SIGINT);
sigprocmask(SIG_BLOCK, &new_mask, NULL);
// 解除阻塞
sigprocmask(SIG_UNBLOCK, &new_mask, NULL);
// 备份旧掩码,执行临界区,之后恢复
sigprocmask(SIG_SETMASK, &new_mask, &old_mask);
// 临界区代码
sigprocmask(SIG_SETMASK, &old_mask, NULL);
- sigpending:获取未决信号集
#include <signal.h>
int sigpending(sigset_t *set);
功能:读取进程 PCB 内的 pending 位图,拿到已经产生、被阻塞、还未递达的信号集合。
⚠️ 只能读取,无法主动修改 pending 位图,pending 由内核自动维护。
使用示例:
sigset_t pending_set;
sigprocmask(SIG_BLOCK, &some_set, NULL);
// 等待信号产生
sigpending(&pending_set);
if (sigismember(&pending_set, SIGINT)) {
printf("SIGINT处于未决状态!\n");
}
- 函数对比

关联逻辑:
只有信号被 block 阻塞时,信号才会停留在 pending;修改阻塞掩码可能立刻触发信号递达;pending 集合仅反映 “被阻塞且已产生” 的信号。
- 示例代码
功能:阻塞 SIGINT (Ctrl+C),循环打印未决信号集,5 秒后解除阻塞。
//signal-set.c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
//打印1~32号信号未决位图
void printsigset(const sigset_t *set)
{
for (int i = 1; i <= 32; i++) {
putchar(sigismember(set, i) ? '1' : '0');
}
putchar('\n');
}
int main()
{
sigset_t block_set, pending_set;
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT);
// 阻塞SIGINT
sigprocmask(SIG_BLOCK, &block_set, NULL);
printf("PID: %d\n", getpid());
printf("尝试按下 Ctrl+C,观察未决信号\n");
int count = 0;
while (1) {
sigpending(&pending_set);
printsigset(&pending_set);
//5秒后解除阻塞
if (++count == 5) {
printf("\n解除SIGINT阻塞!\n");
sigprocmask(SIG_UNBLOCK, &block_set, NULL);
}
sleep(1);
}
return 0;
}
运行现象:
阻塞期间按下Ctrl+C,SIGINT 对应 bit 置 1;5 秒解除阻塞,信号递达,进程终止。
补充:Ctrl+\(SIGQUIT)没有被阻塞,随时可以终止程序。
4. 深入理解:信号处理函数的限制
编写自定义信号处理函数(signal/sigaction注册的回调),存在两条核心约束:
- 可重入性
信号能够随时打断主程序运行,主程序可能正在修改全局变量、链表 数据结构。
一旦信号触发,立刻跳转执行信号处理函数。
如果处理函数继续操作同一套全局数据,极易造成数据错乱、程序崩溃。
规则:
信号处理函数内,尽量不使用非可重入函数。
典型危险函数:malloc、free、printf。
- 异步信号安全(Async-signal-safe)
信号属于异步事件,执行时机不可预测。
Linux内核规定:仅有少量系统调用属于异步信号安全函数,允许在信号回调中调用。
✅ 安全示例:write()、_exit()
❌ 不安全:printf(内部封装缓冲区malloc,非异步安全) - 大白话总结
1.可重入:防止多执行路径争抢数据,避免数据破坏;
2.异步信号安全:内核层面规定,禁止随意调用大部分库函数,防止死锁、内存异常。
拓展小知识点:
printf不安全双重原因:
① 内部维护缓冲区(全局状态,不满足可重入)
② 不属于POSIX规定的异步信号安全函数。
二、捕捉信号
捕捉信号:进程提前注册自定义处理函数,信号递达时,内核调用该函数,不再执行信号默认行为。
简单理解:捕捉信号 = 使用自定义函数,替换信号默认处理动作
1. 为什么需要信号捕捉
- 信号默认行为由内核执行:程序终止、产生 Core 文件、忽略信号等。
- 我们可以通过 signal() / sigaction() 向内核注册自定义回调函数。
- 核心目的:程序收到信号后,执行我们自己编写的逻辑,摆脱内核预设的处理方式。
- 信号捕捉常见用途:
- 程序收到退出信号时,先保存数据、释放资源,再优雅退出,防止数据丢失
- 拦截危险信号,避免程序直接崩溃
- 实现异步事件通知机制
2. 内核如何实现信号的捕捉
当信号处理方式设置为用户自定义函数,也就是捕捉信号。
- 场景假设
我们提前为 SIGQUIT 注册自定义处理函数 sighandler
进程此刻正常运行 main 主函数 - 信号捕捉流程图

- 用户态运行 main 函数,发生中断 / 异常,进程切换进入内核态
- 内核完成中断处理,检测到当前进程有待递达信号
- 准备从内核态返回用户态:不恢复 main 上下文,转而执行信号处理函数sighandler
- 用户态运行 sighandler;函数结束后,自动触发 sigreturn 再次进入内核态
- 内核借助 sigreturn 恢复主函数 main 的寄存器、运行上下文
- 切换回用户态,继续执行原先的 main 代码
- 关键点说明
- main 和 sighandler 是两条独立执行流,没有普通函数调用关系,拥有各自栈空间
- sigreturn 是系统调用,信号处理函数结束后自动调用
- 必须依靠 sigreturn 的原因:
sighandler 不是 main 主动调用的普通函数,无法依靠普通 return 正常回到主程序,需要内核协助恢复现场。
3. 信号处理函数
- 回调函数
回调函数:提前把函数注册给内核,由内核在合适时机调用,代码中不会手动调用。
信号场景下,sa_handler指向的函数就是回调函数:
- 不是 main 主动调用
- 信号递达时,内核自动调用
- 参数 signo 代表信号编号,同一个处理函数可以区分多种信号
示例代码 handler_demo.c
// handler_demo.c
// 功能:演示信号处理回调函数
// 编译:gcc handler_demo.c -o handler_demo
// 运行:./handler_demo
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
/**
* 信号处理函数(回调函数)
* 收到信号后由内核自动调用
* @param signo 信号编号(2=SIGINT, 3=SIGQUIT)
*/
void handler(int signo) {
if (signo == SIGINT) {
// 信号处理函数禁止使用printf,选用异步安全函数write
write(1, "Caught SIGINT\n", 14);
} else if (signo == SIGQUIT) {
write(1, "Caught SIGQUIT\n", 15);
}
}
int main() {
// 向内核注册回调函数
signal(SIGINT, handler);
signal(SIGQUIT, handler);
printf("PID: %d\n", getpid());
printf("Try Ctrl+C (SIGINT) or Ctrl+\\ (SIGQUIT)\n");
while (1) {
sleep(1);
}
return 0;
}
运行效果:按下 Ctrl+C / Ctrl+\ 不会终止程序,执行自定义打印;只能使用 kill -9 强制杀死进程。
$ ./handler_demo
PID: 1234
Try Ctrl+C (SIGINT) or Ctrl+\ (SIGQUIT)
^CCaught SIGINT
^CCaught SIGINT
^\Caught SIGQUIT
- signal 函数
signal() 是简易的信号注册接口,功能有限,可移植性较差。
函数原型:
#include <signal.h>
// 原始写法
void (*signal(int signo, void (*handler)(int)))(int);
// 使用typedef简化
typedef void (*sighandler_t)(int);
sighandler_t signal(int signo, sighandler_t handler);
返回值:成功返回旧的信号处理函数指针;失败返回 SIG_ERR,设置错误码
参数 signo:信号编号
参数 handler 三种取值:

返回值:成功返回旧处理函数指针;失败返回 SIG_ERR
三种使用场景:
- signal(SIGINT, SIG_IGN); 忽略信号,Ctrl+C 失效
- signal(SIGINT, SIG_DFL); 使用系统默认行为,按下 Ctrl+C 程序退出
- signal(SIGINT, my_handler); 注册自定义捕捉函数
signal主要缺点 - 可移植性差,不同系统实现行为不一致
- 执行信号处理函数时,不会自动屏蔽同类信号
- 无法在处理期间额外屏蔽其他信号,控制粒度粗糙
- sigaction 函数
sigaction 是 signal 的增强版本,POSIX 标准推荐,支持精细化管控信号行为。
函数原型:
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
- act:传入参数,新的信号配置;传 NULL 代表不修改
- oact:传出参数,保存旧配置;不需要可置 NULL
struct sigaction 结构体
struct sigaction {
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
};
- sa_handler:信号处理方式
- sa_mask:执行处理函数期间,额外临时屏蔽的信号集
- sa_flags:行为标志,常规使用置 0
核心机制:自动屏蔽
调用 sigaction 注册信号捕捉时:
执行信号处理函数期间,内核自动屏蔽当前信号;同类信号多次抵达会阻塞,等待处理函数结束后再递达。结合sa_mask可以额外屏蔽其他指定信号。
代码示例:
// sigaction_demo.c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
void handler(int signo) {
if (signo == SIGINT) {
write(1, "Caught SIGINT (Ctrl+C)\n", 23);
sleep(3);
} else if (signo == SIGQUIT) {
write(1, "Caught SIGQUIT (Ctrl+\\)\n", 24);
}
}
int main() {
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGQUIT); // 处理SIGINT时屏蔽SIGQUIT
act.sa_flags = 0;
sigaction(SIGINT, &act, NULL);
sigaction(SIGQUIT, &act, NULL);
printf("PID: %d\n", getpid());
while (1) sleep(1);
return 0;
}
现象:触发 SIGINT 后立刻按Ctrl+\,SIGQUIT 不会立刻响应,等待 3 秒处理结束后才执行,验证信号屏蔽效果。
- sigaction 相比 signal 的优势

✅ 开发建议:正式项目优先使用 sigaction;signal 仅用于简单测试、兼容老旧代码。
4. 不可捕捉的信号
不是所有信号都支持捕捉、忽略、阻塞。SIGKILL(9) 与 SIGSTOP(19) 是特例。

底层原因:
倘若进程能够捕获 SIGKILL,恶意 / 失控程序就可以拦截信号,系统将无法使用 kill -9 强制回收进程。
这两个信号是操作系统留给管理员的最终强制手段,保障系统控制权。
代码验证:
// 下面两行代码不会生效
signal(SIGKILL, handler);
signal(SIGSTOP, handler);

162

被折叠的 条评论
为什么被折叠?



