资料合集(福利需要仔细找)
链接:https://pan.quark.cn/s/b0a2f36933de
在之前的文章中,我们要么用 简单粗暴地抓信号,要么用
signal 优雅地设置屏蔽字。但你有没有想过一个深层次的问题:
sigaction
信号处理函数是我们写的(在用户区),但信号是由内核发出的。内核究竟是如何“把手伸到用户区”去执行我们写的函数的?执行完又是怎么跳回来的?
今天我们将深入 Linux 内核,揭秘信号捕捉的上下文切换过程,并探讨 中的几个关键标志位。
sa_flags
一、 信号捕捉的“环形之旅”
我们要明白一个核心概念:信号处理不是立即发生的,而是在“从内核态返回用户态”的前一刻检查并处理的。
1. 内核视角的五步流程
想象你的程序正在跑着(用户态),突然发生了一次系统调用(比如 )或者中断(比如时间片到了)。流程如下:
read
进入内核:程序因为中断、异常或系统调用,从 User Mode (用户态) 切换到 Kernel Mode (内核态)。内核处理:内核干完该干的活(比如读取磁盘、调度进程)。检查信号:准备返回用户态之前,内核会像“安检员”一样检查当前进程是否有未决信号(Pending)。
如果有,且该信号被捕捉,内核会修改栈帧。
执行捕捉 (User Mode):
内核“修改”了返回地址,使得程序不是返回到原来的断点,而是跳到了信号处理函数的入口。这一步是从内核态回到用户态执行代码。
再次返核 (sigreturn):
信号处理函数执行完毕,不能直接 到主程序(因为它不知道主程序刚才断在哪了)。函数末尾隐藏着一个特殊的系统调用
return。它再次进入内核,告诉内核“我处理完了”。
sigreturn
恢复执行:内核恢复主程序原本的上下文(寄存器、栈等),程序终于回到了被打断的地方继续运行。
总结:这是一个 User -> Kernel -> User(Handler) -> Kernel(sigreturn) -> User(Main) 的“∞”字形(或环形)流程。
2. 为什么需要
sigreturn?
sigreturn
这是笔记中的重点。普通的函数调用通过栈记录返回地址,但信号处理函数是被内核“强行插入”的。处理函数结束后,必须通过 系统调用重新进入内核,让内核把原来保存的“案发现场”(原程序执行位置)恢复出来,否则程序就迷路了。
sigreturn
二、 慢速系统调用与
SA_RESTART
SA_RESTART
在笔记的“扩展了解”中提到了慢速系统调用(如 ,
read)可能被信号中断。这是一个非常经典的“坑”。
pause
1. 现象演示:
read 被打断
read
当进程阻塞在 等待输入时,如果收到一个信号,默认情况下
read 会被强行唤醒,返回 -1,并设置
read(Interrupted system call)。
errno = EINTR
代码示例 ():
interrupt_test.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <string.h>
void sig_handler(int signum) {
printf("
>>> 信号 %d 被捕捉!
", signum);
}
int main() {
struct sigaction act;
act.sa_handler = sig_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0; // 默认行为:不自动重启系统调用
sigaction(SIGINT, &act, NULL);
char buf[1024];
printf("进程(PID: %d) 正在等待输入 (调用read阻塞中)...
", getpid());
// 这是一个慢速系统调用,会阻塞等待键盘输入
int n = read(STDIN_FILENO, buf, sizeof(buf));
if (n == -1) {
if (errno == EINTR) {
printf("Read被信号中断了!返回值: %d, errno: %d (%s)
", n, errno, strerror(errno));
} else {
perror("Read error");
}
} else {
printf("Read成功,读取了 %d 字节
", n);
}
return 0;
}
运行结果:
启动程序后,不要输入任何文字,直接按下 。
Ctrl+C
进程(PID: 12345) 正在等待输入 (调用read阻塞中)...
^C
>>> 信号 2 被捕捉!
Read被信号中断了!返回值: -1, errno: 4 (Interrupted system call)
2. 解决方案:使用
SA_RESTART
SA_RESTART
如果我们希望 在信号处理完后,能自动恢复等待状态,而不是报错返回,就需要设置
read。
sa_flags
修改上述代码中的一行:
// act.sa_flags = 0;
act.sa_flags = SA_RESTART; // 设置自动重启标志
再次运行:
启动后按 。
Ctrl+C
进程(PID: 12345) 正在等待输入 (调用read阻塞中)...
^C
>>> 信号 2 被捕捉!
(程序继续阻塞在这里,没有打印错误信息)
hello
Read成功,读取了 6 字节
结论: 让被中断的慢速系统调用在信号处理结束后,自动重新启动,这对编写健壮的网络服务非常重要。
SA_RESTART
三、
SA_NODEFER:允许嵌套处理
SA_NODEFER
之前的课程我们学过, 默认会在处理某信号时,自动屏蔽该信号(为了防止递归调用导致栈溢出)。但如果你确实需要“嵌套”处理同一种信号,可以使用
sigaction。
SA_NODEFER
代码示例 ():
nodefer_test.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void sig_handler(int signum) {
printf(">>> [Start] 捕捉到信号 %d
", signum);
// 模拟耗时操作 3秒
for(int i=0; i<3; i++) {
printf(" 处理中... %d
", i);
sleep(1);
}
printf(">>> [End] 信号 %d 处理完毕
", signum);
}
int main() {
struct sigaction act;
act.sa_handler = sig_handler;
sigemptyset(&act.sa_mask);
// 设置 SA_NODEFER:处理信号时,不屏蔽本信号
// 这意味着如果处理还没结束,又来了同一个信号,会立即发生嵌套调用
act.sa_flags = SA_NODEFER;
sigaction(SIGINT, &act, NULL);
printf("进程(PID: %d) 运行中,请快速连续按两次 Ctrl+C
", getpid());
while(1) sleep(1);
return 0;
}
运行结果:
快速按下两次 。
Ctrl+C
进程(PID: 12345) 运行中,请快速连续按两次 Ctrl+C
^C
>>> [Start] 捕捉到信号 2 <-- 第一次触发
处理中... 0
^C
>>> [Start] 捕捉到信号 2 <-- 第二次立即触发(嵌套插队!)
处理中... 0
处理中... 1
处理中... 2
>>> [End] 信号 2 处理完毕 <-- 第二次先处理完
处理中... 1 <-- 回到第一次继续处理
处理中... 2
>>> [End] 信号 2 处理完毕 <-- 第一次终于处理完
分析:由于设置了 ,第二次信号没有被阻塞,而是直接打断了第一次的处理过程,形成了递归调用。
SA_NODEFER
警告:除非有特殊需求,否则尽量不要使用
,因为无限递归会导致内核栈溢出,程序崩溃。
SA_NODEFER
四、 知识小结
| 知识点 | 核心内容 | 备注 |
|---|---|---|
| 信号捕捉流程 | 中断入核 -> 内核检查 -> 用户态处理 -> sigreturn返核 -> 恢复主流程 | 这是一个“8”字形的往返过程 |
| sigreturn | 专门用于从信号处理函数返回内核态的系统调用 | 程序员不可见,由内核自动插入 |
| sa_flags | : 自动重启被中断的系统调用 (如read) |
解决 EINTR 错误的利器 |
| sa_flags | : 不屏蔽本信号,允许嵌套/递归 |
慎用,防止栈溢出 |
| 注意事项 | 信号处理函数应尽量短小精悍 | 避免在处理函数中调用复杂 IO 或不可重入函数 |

