信号保存
信号相关概念详解
1.核心概念
(1) 信号递达(Delivery)
定义:信号递达是指信号被进程实际处理的过程。
触发时机:当进程从内核态返回用户态时,内核检查并处理未决信号。
处理方式:
默认动作(SIG_DFL):系统预定义行为(如终止、暂停、忽略等)。
忽略(SIG_IGN):直接丢弃信号,不执行任何操作。
自定义处理函数:通过signal()或sigaction()注册的用户函数。
(2) 信号未决(Pending)
定义:信号从生成到递达之间的中间状态。
原因:
信号被进程阻塞(Blocked)(通过sigprocmask()设置)。
进程正在处理其他同类型信号(非实时信号可能合并)。
未决信号集:内核为每个进程维护一个集合,记录所有未决信号(可通过sigpending()读取)。
(3) 信号阻塞(Block)
定义:阻止信号递达(即使信号已生成),使其保持未决状态。
关键点:
阻塞是未决的前提,未被阻塞的信号会立即递达(除非进程正处理其他信号)。
阻塞解除后,未决信号会递达(除非被忽略)。
(4) 信号忽略(Ignore)
1.定义:信号递达后,进程选择不执行任何操作(通过SIG_IGN或空处理函数)。
与阻塞的区别:
阻塞:信号未递达,停留在未决状态。
忽略:信号已递达,但处理动作为“丢弃”。
2. 三者的关系
信号生成
│
├── 未被阻塞 ──── 立即递达(执行默认/自定义动作或忽略)
│
└── 被阻塞 ────── 加入未决信号集
│
└── 阻塞解除后 ──── 递达
3.关键区别总结
概念 | 阶段 | 效果 | 系统调用 |
---|---|---|---|
阻塞(Block) | 信号递达前 | 信号保持未决,不处理 | sigprocmask() |
忽略(Ignore) | 信号递达后 | 丢弃信号,不执行任何动作 | signal(sig, SIG_IGN) |
未决(Pending) | 信号生成到递达间 | 信号等待被处理 | sigpending() |
4. 示例代码说明
复制
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void handler(int sig) {
printf("递达信号 %d\n", sig);
}
int main() {
// 注册SIGINT处理函数
signal(SIGINT, handler);
// 阻塞SIGINT
sigset_t block_set;
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT);
sigprocmask(SIG_BLOCK, &block_set, NULL);
printf("按下 Ctrl+C,SIGINT 将被阻塞...\n");
sleep(3); // 此时发送SIGINT,信号处于未决状态
// 检查未决信号
sigset_t pending_set;
sigpending(&pending_set);
if (sigismember(&pending_set, SIGINT)) {
printf("SIGINT 处于未决状态!\n");
}
// 解除阻塞,信号递达
sigprocmask(SIG_UNBLOCK, &block_set, NULL);
printf("SIGINT 已解除阻塞。\n");
return 0;
}
输出结果:
在sleep(3)期间按下Ctrl+C,SIGINT被阻塞,处于未决状态。
sigpending()检测到未决的SIGINT。
解除阻塞后,handler函数执行,信号递达。
(5)注意事项
信号丢失:非实时信号(如SIGINT)在未决期间多次生成,可能仅保留一次。
实时信号(如SIGRTMIN)支持排队,不会丢失。
原子性:信号处理函数应使用可重入函数,避免竞态条件。
(6)常见问题
Q:阻塞和忽略有何本质区别?
A:阻塞阻止信号递达(未决状态),忽略是递达后的处理方式之一。
Q:如何永久阻塞一个信号?
A:通过sigprocmask()设置阻塞掩码后,除非显式解除,否则信号一直未决。
Q:sigpending()的作用?
A:获取当前进程所有未决信号,用于调试或条件处理。
信号集(sigset_t)及操作函数详解
(1) 信号集(sigset_t)的基本概念
用途:表示一组信号,每个信号对应一个比特位(1有效,0无效)。
未决信号集(Pending Set):记录信号是否处于未决状态(已产生但未递达)。
阻塞信号集(Block Set / Signal Mask):记录信号是否被阻塞(屏蔽)。
特性:
仅记录信号是否有效,不记录次数(多次触发同一信号仍只显示一次)。
内部实现由系统决定,用户只能通过标准函数操作,不可直接访问比特位。
(2)信号集操作函数
以下函数均来自 <signal.h>,成功返回 0,失败返回 -1(sigismember 返回布尔值)
:
函数 | 功能 |
---|---|
sigemptyset(sigset_t *set) | 初始化信号集为空(所有比特置 0)。 |
sigfillset(sigset_t *set) | 初始化信号集包含所有信号(所有比特置 1)。 |
sigaddset(sigset_t *set, int signo) | 将指定信号(如 SIGINT)添加到信号集。 |
sigdelset(sigset_t *set, int signo) | 从信号集中删除指定信号。 |
sigismember(const sigset_t *set, int signo) | 检查信号是否在集合中(1=存在,0=不存在)。 |
注意:
使用 sigset_t 前必须初始化(sigemptyset 或 sigfillset),否则行为未定义。
不可直接打印 sigset_t 的内容(如 printf),需通过 sigismember 逐信号检查。
(3)信号屏蔽字操作(sigprocmask)
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
功能:读取或更改进程的阻塞信号集(Signal Mask)。
参数:
how:修改方式,可选值:
SIG_BLOCK:将 set 中的信号添加到当前阻塞集(mask = mask | set)。
SIG_UNBLOCK:从当前阻塞集中移除 set 中的信号(mask = mask & ~set)。
SIG_SETMASK:直接设置阻塞集为 set(mask = set)。
set:新信号集(若为 NULL,则不修改)。
oset:保存旧的阻塞集(若为 NULL,则不保存)。
返回值:成功返回 0,失败返回 -1。
关键行为:
若解除对未决信号的阻塞,sigprocmask 返回前至少会递达一个信号。
(4)获取未决信号集(sigpending)
int sigpending(sigset_t *set);
功能:读取当前进程的未决信号集,通过 set 传出。
返回值:成功返回 0,失败返回 -1。
(5) 实验代码解析
以下代码演示了信号屏蔽与未决信号的检测:
#include <iostream>
#include <signal.h>
#include <unistd.h>
void PrintPending(sigset_t &pending) {
std::cout << "Pending signals: ";
for (int signo = 31; signo >= 1; signo--) {
std::cout << (sigismember(&pending, signo) ? "1" : "0");
}
std::cout << std::endl;
}
void handler(int signo) {
std::cout << "Signal " << signo << " delivered!\n";
}
int main() {
signal(SIGINT, handler); // 捕捉 SIGINT (Ctrl+C)
sigset_t block_set, old_set;
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT); // 屏蔽 SIGINT
sigprocmask(SIG_BLOCK, &block_set, &old_set); // 设置阻塞
int cnt = 5;
while (cnt--) {
sigset_t pending;
sigpending(&pending); // 获取未决信号
PrintPending(pending);
sleep(1);
}
// 解除屏蔽
sigprocmask(SIG_SETMASK, &old_set, NULL);
return 0;
}
运行结果:
按下 Ctrl+C 后,SIGINT 被阻塞,未决信号集中对应位变为 1。
解除阻塞后,SIGINT 递达,触发 handler 函数。
(6) 关键区别与注意事项
阻塞 vs 忽略:
阻塞(Block):信号被暂时屏蔽,处于未决状态,解除阻塞后会递达。
忽略(Ignore):信号直接被丢弃,不会递达(通过 signal(signo, SIG_IGN) 设置)。
信号集初始化:未初始化的 sigset_t 可能导致未定义行为,必须调用 sigemptyset 或 sigfillset。
多线程环境:sigprocmask 仅影响当前线程,应使用 pthread_sigmask。
(7)总结图示
+-------------------+ sigprocmask() +-------------------+
| Block Set | <---------------------- | 用户设置的信号集 |
| (Signal Mask) | | (sigset_t) |
+-------------------+ +-------------------+
| ^
| 信号产生但被阻塞 | sigpending()
v |
+-------------------+ +-------------------+
| Pending Set | ----------------------> | 未决信号集 |
| (待处理信号) | | (sigset_t) |
+-------------------+ +-------------------+
|
| 解除阻塞后
v
+-------------------+
| 信号递达 |
| (执行处理函数) |
+-------------------+
信号捕捉
信号捕捉的详细流程解析
信号捕捉的核心概念
当信号的处理动作用户自定义函数(而非 SIG_IGN 或 SIG_DFL),内核会在信号递达时调用该函数,这一过程称为 信号捕捉。
由于信号处理函数运行在用户态,其执行流程涉及 用户态-内核态切换。
信号捕捉的完整流程(以 SIGQUIT 为例)
1.注册信号处理函数
用户程序通过 signal() 或 sigaction() 注册信号处理函数(如 sighandler):
signal(SIGQUIT, sighandler); // 自定义SIGQUIT处理函数
2.主程序执行时发生中断/异常
假设 main() 函数正在执行,此时发生 硬件中断(如时钟中断)或 异常(如缺页异常),CPU 切换到 内核态 处理中断。
3.内核检查未决信号
中断处理完成后,内核在 返回用户态前 检查进程是否有未决信号(如 SIGQUIT)。
若发现未决信号且其动作为“捕捉”,内核准备切换到用户态执行 sighandler。
4.切换到信号处理函数
内核 不恢复 main() 的上下文,而是:
为用户态信号处理函数 sighandler 创建独立的 临时栈帧(与 main() 栈隔离)。
修改用户态栈和寄存器,使 sighandler 成为下一个执行入口。
切换回用户态后,CPU 开始执行 sighandler。
5.信号处理函数执行完毕
sighandler 执行完成后,会 隐式调用 sigreturn() 系统调用,再次进入内核态。
内核此时:
销毁临时栈帧。
恢复 main() 的原始上下文(寄存器、栈指针等)。
若无其他未决信号,返回用户态继续执行 main()。
(3) 关键点与注意事项
独立性:
sighandler 和 main() 是 两个独立的控制流,它们:
使用不同栈空间(内核为 sighandler 分配临时栈)。
无直接调用关系(非函数调用,而是通过内核调度切换)。
可重入性问题:
信号处理函数中应避免调用 非异步信号安全函数(如 printf、malloc),否则可能导致死锁或数据损坏。
内核态-用户态切换:
一次完整的信号捕捉涉及 两次状态切换:
用户态 → 内核态(中断/异常处理)。
内核态 → 用户态(执行 sighandler)。
用户态 → 内核态(sigreturn)。
内核态 → 用户态(恢复 main())。
信号屏蔽与竞态条件:
内核会在执行 sighandler 时 自动阻塞同一信号(除非显式设置 SA_NODEFER),防止递归触发。
(4)代码示例与验证
以下代码演示信号捕捉流程,并打印栈地址以验证独立性:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sighandler(int signo) {
printf("Signal %d caught!\n", signo);
printf("sighandler stack frame: %p\n", (void*)&signo); // 打印处理函数栈地址
sleep(1); // 模拟处理耗时
}
int main() {
printf("Main stack frame: %p\n", (void*)&main); // 打印main函数栈地址
signal(SIGQUIT, sighandler); // 注册SIGQUIT处理函数
while (1) {
printf("Main running...\n");
sleep(1);
}
return 0;
}
输出分析:
main 和 sighandler 的栈地址差异显著,说明二者栈空间独立。
按下 Ctrl+\(触发 SIGQUIT)后,sighandler 打断 main 的执行流。
(5) 信号捕捉的底层机制
内核数据结构:
进程的 task_struct 中维护:
pending:未决信号集。
blocked:阻塞信号集。
sighand:指向信号处理函数表。
sigreturn 的作用:
恢复原始上下文的关键系统调用,由 glibc 在信号处理函数返回前自动触发。
(6)总结图示