上几篇博客我们简绍了信号的产生与保存,这篇博客我们将进入下一步,信号的处理,但是默认和忽略处理是很容易的,这里我们不做讨论,我们将升入探讨信号的自定义捕捉(处理)
在 Linux 进程信号的生命周期中,“信号捕捉” 是连接 “信号保存” 与 “信号处理” 的核心环节 —— 它允许我们自定义信号的响应逻辑,而非依赖系统默认行为(终止、忽略等)。本文将从内核视角拆解信号捕捉的底层机制,结合实操代码讲清signal()与sigaction()的使用,帮你彻底搞懂 “内核如何调用用户写的处理函数”
一、信号捕捉的核心前提:为什么信号不能立即处理
信号是异步通知机制,进程在执行用户态代码时,随时可能收到信号(如键盘Ctrl+C触发 SIGINT)。但内核不会立刻打断进程执行,核心原因有两个:
因此,信号捕捉的关键规则是:信号仅在 “进程从内核态返回用户态” 前被检查和处理—— 这是内核预设的 “安全检查点”
二、信号捕捉的底层流程:四次特权级切换的秘密
信号捕捉的完整流程本质是 “用户态↔内核态” 的四次切换,可拆解为 8 个关键步骤,配合内核与用户栈的协作完成:
这一流程可概括为 “用户态→内核态→用户态(处理函数)→内核态(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 等信息

