Linux 信号捕捉
信号捕捉流程
信号的捕捉流程图如下:

信号捕捉是指当进程收到信号时,内核将控制流转交给用户自定义的信号处理函数,并在处理完成后恢复原始执行流程的过程。整个过程涉及多次用户态↔内核态切换,并确保信号不会丢失。以下是详细流程:
1. 主程序在用户态执行(如 main 函数)
- 进程正常执行用户代码(如
main函数的指令)。 - 可能被中断:由于硬件中断(如时钟中断)、异常(如缺页异常)或系统调用,CPU 从用户态切换到内核态。
2. 进入内核态处理中断/异常
- 内核完成中断或异常的处理(如系统调用执行完毕)。
- 在返回用户态前,检查待处理信号(
do_signal):- 如果没有待处理信号,直接返回用户态,恢复
main的执行。 - 如果发现待处理信号(如
SIGQUIT),且该信号的处理方式是用户自定义函数,进入信号递送流程。
- 如果没有待处理信号,直接返回用户态,恢复
3. 内核准备执行信号处理函数
- 修改用户态返回地址:
- 内核不会直接返回到
main,而是修改栈和寄存器,使得 CPU 返回用户态时跳转到信号处理函数(如sighandler)。 - 信号处理函数运行在独立的栈帧(避免破坏
main的栈)。
- 内核不会直接返回到
- 记录原始上下文:
- 内核保存
main的完整执行现场(寄存器、栈指针等),以便后续恢复。
- 内核保存
4. 信号处理函数在用户态执行
- 信号处理函数(如
sighandler)开始执行,完成用户定义的逻辑(如打印日志、清理资源等)。 - 信号处理函数执行完毕后,不会直接返回
main,而是自动调用sigreturn系统调用,再次进入内核态。
5. sigreturn:内核恢复主程序上下文
sigreturn的作用:- 告诉内核“信号处理已完成”,并恢复之前保存的
main的上下文。 - 但在此之前,内核会再次检查是否有新的待处理信号(防止信号丢失)。
- 如果没有新信号:直接恢复
main的执行现场,返回用户态继续执行。 - 如果有新信号:重复信号递送流程(可能再次进入另一个信号处理函数)。
- 如果没有新信号:直接恢复
- 告诉内核“信号处理已完成”,并恢复之前保存的
6. 最终返回主程序继续执行
- 如果没有更多信号需要处理,内核恢复
main的寄存器、栈指针等,并返回到用户态。 - 主程序从被中断的位置继续执行,就像信号从未发生过一样(除非信号处理函数修改了全局状态)。
总结
- 信号是异步的,内核在每次返回用户态前(无论是从中断、异常还是
sigreturn)都会检查信号。- 信号处理函数和主程序是独立的控制流,它们使用不同的栈,没有直接的调用关系。
sigreturn是信号处理的关键:
- 它不仅是恢复现场,还会再次检查信号,确保不会遗漏新到达的信号。
- 递归信号处理:
- 如果信号处理函数执行期间又收到同一信号,可能会导致递归调用(除非使用
sigaction阻塞该信号)。
sigaction 函数
sigaction 是比 signal 更强大、更灵活的信号处理函数,它允许精确控制信号的处理行为,包括:
- 设置信号处理函数(
sa_handler或sa_sigaction) - 指定在执行信号处理函数时自动阻塞哪些信号(
sa_mask) - 配置额外的信号处理选项(
sa_flags)
函数原型
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数说明
| 参数 | 说明 |
|---|---|
signum |
要操作的信号编号(如 SIGINT、SIGTERM) |
act |
指向 struct sigaction,用于设置新的信号处理方式(若为 NULL,则仅读取旧设置) |
oldact |
用于保存旧的信号处理方式(若为 NULL,则不保存) |
返回值
- 成功:返回
0 - 失败:返回
-1,并设置errno
struct sigaction 结构体
struct sigaction {
void (*sa_handler)(int); // 普通信号处理函数
void (*sa_sigaction)(int, siginfo_t *, void *); // 实时信号处理函数
sigset_t sa_mask; // 额外阻塞的信号集
int sa_flags; // 控制信号行为的标志位
void (*sa_restorer)(void); // 已废弃,通常置 NULL
};
关键字段说明
sa_handler- 指定信号处理函数,可以是:
SIG_IGN(忽略信号)SIG_DFL(恢复默认行为)- 自定义函数(格式:
void handler(int signo))
- 例如:
void handler(int sig) { printf("Received signal %d\n", sig); } struct sigaction act; act.sa_handler = handler; // 设置自定义处理函数
- 指定信号处理函数,可以是:
sa_sigaction- 用于实时信号(如
SIGRTMIN以后的信号),提供更多信息(siginfo_t)。 - 需配合
sa_flags = SA_SIGINFO使用。
- 用于实时信号(如
sa_mask在执行信号处理函数时,自动阻塞的信号集(防止信号嵌套)。
默认阻塞当前信号(无需手动添加)。
例如:
sigemptyset(&act.sa_mask); // 清空信号集 sigaddset(&act.sa_mask, SIGQUIT); // 阻塞 SIGQUIT
sa_flags控制信号行为的标志位,常见选项:
SA_RESTART:被信号中断的系统调用自动重启。SA_NOCLDSTOP:仅对SIGCHLD有效,子进程停止时不通知父进程。SA_SIGINFO:使用sa_sigaction而非sa_handler。
例如:
act.sa_flags = SA_RESTART; // 系统调用被中断后自动恢复
与
signal函数的对比
特性 signalsigaction可移植性 不同系统行为可能不同 行为一致 信号屏蔽 无法指定额外阻塞的信号 支持 sa_mask实时信号 不支持 支持( sa_sigaction)系统调用重启 依赖实现 可通过 SA_RESTART控制
信号捕捉底层细节
在信号捕捉的底层实现中,内核会在调用信号处理函数之前(而非之后)将 pending 表中对应信号的位置清零(即 pending &= ~(1 << signo))。这一设计是为了正确处理信号的重复触发,同时结合**信号屏蔽字(blocking mask)**的自动管理机制。以下是详细解释:
步骤 1:信号到达,pending 位置 1
- 当信号(如
SIGINT)到达时,内核会:- 检查该信号是否被阻塞(通过
blocking mask):- 如果未被阻塞,准备递送。
- 如果被阻塞,仅将
pending对应位置1,暂不处理。
- 若信号未被阻塞,内核将其加入待处理队列。
- 检查该信号是否被阻塞(通过
步骤 2:准备调用信号处理函数
- 在从内核态返回用户态执行信号处理函数前,内核会:
- 清零
pending表中的对应位(表示该信号正在被处理)。 - 自动阻塞该信号(将
blocking mask中对应位置1),防止嵌套调用。
- 清零
步骤 3:执行信号处理函数
- 信号处理函数(如
handler)在用户态执行:- 如果在此期间同一信号再次到达:
- 由于该信号已被阻塞(
blocking mask置1),内核不会立即递送。 - 但会将
pending表中对应位重新置1(记录信号到达)。
- 由于该信号已被阻塞(
- 如果在此期间同一信号再次到达:
步骤 4:信号处理函数返回
- 函数返回时,内核通过
sigreturn系统调用恢复现场:- 恢复原来的
blocking mask(取消对该信号的阻塞)。 - 再次检查
pending表:- 如果发现该信号位为
1(表示在处理期间信号又到达了),则重新递送。 - 否则,正常恢复主程序执行。
- 如果发现该信号位为
- 恢复原来的
3. 为什么要在调用处理函数前清零 pending?
原因 1:避免信号丢失
- 如果在处理函数执行后才清零
pending,可能会出现:- 信号 A 正在处理时,同一信号 A 再次到达。
- 如果第一次的
pending未被清零,第二次到达的信号可能被忽略(内核认为“已在处理”)。
- 提前清零确保后续到达的信号能正确记录在
pending中。
原因 2:与阻塞机制配合
- 内核通过自动阻塞当前信号 + 提前清零
pending,实现以下行为:- 信号处理期间,同一信号不会被嵌套调用(避免递归)。
- 但后续到达的信号会被记录,并在处理完成后重新递送。
示例场景
假设进程收到两次 SIGINT:
- 第一次
SIGINT:pending置1→ 内核调用处理函数前清零pending并阻塞SIGINT。
- 处理函数执行期间,第二次
SIGINT到达:- 由于
SIGINT被阻塞,内核仅将pending置1,不立即处理。
- 由于
- 处理函数返回后:
- 内核发现
pending为1,重新递送SIGINT,再次触发处理函数。
- 内核发现
下面是验证示例:
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstring>
#include <sys/wait.h>
using namespace std;
// 打印当前进程的未决信号集(pending signals)
void print_pending_map()
{
sigset_t sigset;
sigpending(&sigset); // 获取当前未决信号集
// 从31到1遍历所有可能的信号
for(int i = 31; i >= 1; --i)
{
if(sigismember(&sigset, i))
cout << 1 << " "; // 该信号处于未决状态
else
cout << 0 << " "; // 该信号未产生
}
cout << endl << endl;
}
// 信号处理函数
void myhandler(int sign)
{
// 打印收到的信号信息
cout << "signal " << strsignal(sign) << " begin" << endl;
// 无限循环打印信号状态
while(true)
{
print_pending_map(); // 打印当前未决信号集
cout << "Processing signal: " << strsignal(sign) << endl;
sleep(1); // 每秒打印一次
}
}
int main()
{
// 设置SIGINT的信号处理
struct sigaction act, oact;
sigemptyset(&act.sa_mask); // 初始化信号屏蔽字
sigemptyset(&oact.sa_mask);
// 关键设置:在执行SIGINT处理函数时阻塞SIGQUIT
sigaddset(&act.sa_mask, SIGQUIT); // 将SIGQUIT加入阻塞集
act.sa_handler = myhandler; // 设置信号处理函数
sigaction(SIGINT, &act, &oact); // 注册SIGINT的处理方式
// 为SIGQUIT设置相同的处理函数(用于演示)
signal(SIGQUIT, myhandler); // 注意:这里使用signal而不是sigaction
// 创建子进程
pid_t ret = fork();
if(ret == 0) // 子进程代码
{
// 子进程无限循环打印自己的PID
while(true)
{
sleep(1);
cout << "Child process " << getpid() << " running..." << endl;
}
}
// 父进程代码
sleep(2); // 等待2秒让子进程启动
// 第一次向子进程发送SIGINT
cout << "Parent sending SIGINT to child process" << endl;
kill(ret, SIGINT); // 发送SIGINT信号
sleep(2); // 等待2秒
// 第二次向子进程发送SIGINT
cout << "Parent sending SIGINT to child process again" << endl;
kill(ret, SIGINT);
sleep(2); // 等待2秒
// 向子进程发送SIGQUIT
cout << "Parent sending SIGQUIT to child process" << endl;
kill(ret, SIGQUIT); // 发送SIGQUIT信号
sleep(2); // 等待2秒
// 向子进程发送SIGKILL(无法被捕获或忽略)
cout << "Parent sending SIGKILL to child process" << endl;
kill(ret, SIGKILL); // 发送SIGKILL信号强制终止子进程
wait(nullptr); // 等待子进程结束
return 0;
}
代码设计目标
本实验通过父子进程模型,演示以下Linux信号处理核心机制:
- 信号处理函数的注册方式差异(
signalvssigaction) - 信号阻塞掩码(sa_mask)的实际效果
- 未决信号集(pending signals)的动态变化
- 不可阻塞信号(SIGKILL)的特殊性
核心组件说明
信号监控函数
void print_pending_map() {
sigset_t sigset;
sigpending(&sigset); // 获取当前未决信号集
for(int i = 31; i >= 1; --i) {
cout << (sigismember(&sigset, i) ? 1 : 0) << " ";
}
cout << endl;
}
功能:实时打印信号未决状态,每个bit代表对应信号是否处于未决状态。
信号处理函数
void myhandler(int sign) {
cout << "Handler executing for: " << strsignal(sign) << endl;
while(true) { // 用于保持处理状态便于观察
print_pending_map();
sleep(1);
}
}
设计意图:通过无限循环模拟长时间信号处理,便于观察信号阻塞效果。
关键配置
// 配置SIGINT处理
struct sigaction act;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGQUIT); // 关键设置!
act.sa_handler = myhandler;
sigaction(SIGINT, &act, NULL);
// 配置SIGQUIT处理(对比组)
signal(SIGQUIT, myhandler);
核心设置:当处理SIGINT时,自动阻塞SIGQUIT信号。
实验流程
sequenceDiagram
participant Parent
participant Child
Parent->>Child: fork()
loop 子进程运行
Child->>Child: 打印运行状态
end
Parent->>Child: kill(SIGINT)
Child->>Child: 执行myhandler
Parent->>Child: kill(SIGINT)
Parent->>Child: kill(SIGQUIT)
Note right of Child: SIGQUIT被阻塞
保持pending状态
Parent->>Child: kill(SIGKILL)
结果输出如下:
╭─ljx@VM-16-15-debian ~/linux_review/sign
╰─➤ ./signal_cap.o
Child process 703969 running...
Parent sending SIGINT to child process
signal Interrupt begin
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Processing signal: Interrupt
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Processing signal: Interrupt
Parent sending SIGINT to child process again
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0
Processing signal: Interrupt
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0
Processing signal: Interrupt
Parent sending SIGQUIT to child process
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0
Processing signal: Interrupt
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0
Processing signal: Interrupt
Parent sending SIGKILL to child process