上几篇博客我们简绍了信号的产生与保存,这篇博客我们将进入下一步,信号的处理,但是默认和忽略处理是很容易的,这里我们不做讨论,我们将升入探讨信号的自定义捕捉(处理)
在 Linux 进程信号的生命周期中,"信号捕捉" 是连接 "信号保存" 与 "信号处理" 的核心环节 ------ 它允许我们自定义信号的响应逻辑,而非依赖系统默认行为(终止、忽略等)。本文将从内核视角拆解信号捕捉的底层机制,结合实操代码讲清signal()与sigaction()的使用,帮你彻底搞懂 "内核如何调用用户写的处理函数"
一、信号捕捉的核心前提:为什么信号不能立即处理
信号是异步通知机制,进程在执行用户态代码时,随时可能收到信号(如键盘Ctrl+C触发 SIGINT)。但内核不会立刻打断进程执行,核心原因有两个:
- 进程可能处于关键执行阶段(如操作临界资源、执行原子逻辑),立即处理会导致数据不一致
- 用户自定义的处理函数运行在用户态,需通过严格的上下文切换保证安全,避免恶意代码篡改内核资源
因此,信号捕捉的关键规则是:信号仅在 "进程从内核态返回用户态" 前被检查和处理------ 这是内核预设的 "安全检查点"
二、信号捕捉的底层流程:四次特权级切换的秘密
信号捕捉的完整流程本质是 "用户态↔内核态" 的四次切换,可拆解为 8 个关键步骤,配合内核与用户栈的协作完成:
- 注册处理函数 :进程通过
signal()或sigaction()告知内核,某信号需执行自定义函数(如handle_sigint),内核将该映射记录在task_struct->sighand->action中 - 信号产生与保存:信号通过键盘、系统调用等方式产生,内核将其标记到进程的未决信号集(pending),并检查信号屏蔽字(block)确认是否可处理
- 进入内核态 :进程因系统调用(如
read)、中断(如时间片到期)或异常进入内核态,内核完成自身任务后准备返回用户态 - 内核检查信号:返回前内核扫描未决信号集,发现 "未被阻塞且需自定义处理" 的信号
- 构建处理上下文:内核在用户栈上构建特殊栈帧,保存原程序的寄存器状态、指令指针,同时修改返回地址为处理函数的入口地址
- 返回用户态执行处理函数 :进程从内核态返回,但不再执行原程序,而是跳转到自定义处理函数(如
handle_sigint) - 处理函数返回触发 sigreturn :处理函数执行完毕后,不会直接返回原程序,而是通过隐藏的
sigreturn()系统调用再次进入内核态 - 恢复上下文并继续执行:内核通过之前保存的状态,恢复原程序的栈帧和寄存器,进程回到被信号中断的位置,继续正常执行
这一流程可概括为 "用户态→内核态→用户态(处理函数)→内核态(sigreturn)→用户态(原程序)" 的环形切换,sigreturn()是确保流程闭环的关键 ------ 它负责通知内核 "处理完成",并恢复原程序的执行上下文
三、信号捕捉的两种实现方式:signal () vs sigaction ()
Linux 提供两种信号捕捉接口,sigaction()因功能更强大、行为更稳定,是生产环境的首选,signal()仅适用于简单场景
1. 简单入门:signal () 函数
signal()是 ANSI C 标准接口,用法简洁,适合快速绑定处理函数:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
// 自定义SIGINT信号处理函数
void handle_sigint(int signum) {
printf("捕捉到SIGINT信号(编号:%d),进程未终止!\n", signum);
}
int main() {
// 注册SIGINT信号的处理函数
signal(SIGINT, handle_sigint);
printf("进程运行中,按Ctrl+C触发SIGINT信号...\n");
while (1) {
sleep(1); // 持续运行,等待信号
}
return 0;
}
注意 :signal()的行为依赖系统实现,不同 Linux 发行版可能存在差异,且不支持信号屏蔽、标志位配置等高级功能
2. 生产首选:sigaction () 函数
sigaction()是 POSIX 标准接口,支持信号屏蔽、处理标志配置、获取旧处理方式等功能,完全覆盖signal()的能力,且行为一致
函数原型与核心参数
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signum:要处理的信号编号(如 SIGINT、SIGTERM);act:指向struct sigaction的指针,包含新的处理配置(核心结构体如下);oldact:保存信号原有的处理方式,可为 NULL。
关键结构体 struct sigaction
struct sigaction {
void (*sa_handler)(int); // 普通处理函数指针(与signal()兼容)
void (*sa_sigaction)(int, siginfo_t *, void *); // 带附加信息的处理函数
sigset_t sa_mask; // 处理信号期间需屏蔽的信号集
int sa_flags; // 处理标志(核心配置项)
void (*sa_restorer)(void); // 已废弃,由内核自动处理
};
实操示例:带标志位的信号捕捉
以下代码演示如何用sigaction()实现 "捕捉 SIGINT 并自动重启被中断的系统调用":
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
void handle_sigint(int signum) {
printf("捕捉到SIGINT信号(编号:%d)\n", signum);
}
int main() {
struct sigaction act, oldact;
// 初始化信号集:处理期间不额外屏蔽其他信号
sigemptyset(&act.sa_mask);
// 设置处理函数
act.sa_handler = handle_sigint;
// 关键标志:SA_RESTART------自动重启被信号中断的慢速系统调用(如read、sleep)
act.sa_flags = SA_RESTART;
// 注册SIGINT信号处理方式,保存旧方式到oldact
if (sigaction(SIGINT, &act, &oldact) == -1) {
perror("sigaction error");
return 1;
}
printf("进程运行中,按Ctrl+C触发SIGINT,输入任意字符回车继续...\n");
char buf[1024];
// 慢速系统调用:若未设置SA_RESTART,信号触发后read会返回-1并置errno=EINTR
ssize_t ret = read(STDIN_FILENO, buf, sizeof(buf));
if (ret == -1) {
if (errno == EINTR) {
printf("read被信号中断(未设置SA_RESTART)\n");
}
perror("read error");
return 1;
}
printf("读取到输入:%.*s\n", (int)ret, buf);
// 恢复信号原有处理方式
sigaction(SIGINT, &oldact, NULL);
return 0;
}
核心标志位 sa_flags 说明
SA_RESTART:自动重启被信号中断的慢速系统调用(如read、pause),避免返回错误SA_NODEFER:处理信号期间不屏蔽当前信号,允许嵌套触发(慎用,可能导致栈溢出)SA_SIGINFO:启用带附加信息的处理函数(sa_sigaction),可获取发送信号的进程 ID 等信息
四、信号捕捉的关键注意事项
- 处理函数需简洁高效:避免在处理函数中执行复杂逻辑(如循环、大量 IO),以免阻塞信号响应
- 严格控制信号屏蔽:通过
sa_mask合理设置处理期间的屏蔽信号,防止关键信号被遗漏 - 避免嵌套触发风险:默认情况下,处理某信号时会自动屏蔽该信号(无需手动设置),除非明确指定
SA_NODEFER - 注意系统调用中断:未设置
SA_RESTART时,慢速系统调用可能被信号中断,需在代码中处理EINTR错误