3. 信号与中断:Unix信号机制、信号处理函数、信号如何中断系统调用
信号(Signal)是 Unix/Linux 系统中用于进程间通信和异步事件通知的核心机制。当信号到达时,正在执行的系统调用可能被强制中断,导致返回 EINTR 错误。理解信号如何中断系统调用,是排查 EINTR 故障的基础。
3.1 Unix 信号机制概述
信号是一种软件中断,用于通知进程发生了某个事件。信号可以由内核、其他进程或进程自身产生。每个信号都有一个唯一的编号和默认行为。
| 信号编号 | 信号名称 | 默认行为 | 典型触发场景 |
|---|---|---|---|
| 1 | SIGHUP | 终止进程 | 终端断开、挂起 |
| 2 | SIGINT | 终止进程 | 键盘中断(Ctrl+C) |
| 3 | SIGQUIT | 终止+Core Dump | 键盘退出(Ctrl+\) |
| 9 | SIGKILL | 终止进程 | 强制杀死(不可捕获/忽略) |
| 14 | SIGALRM | 终止进程 | 定时器到期 |
| 17 | SIGCHLD | 忽略 | 子进程状态改变 |
关键概念:信号是异步的。进程无法预知信号何时到达,信号处理函数会在用户态和内核态切换时被内核调用。
3.2 信号处理函数(Signal Handler)
进程可以通过 signal() 或 sigaction() 系统调用为特定信号注册处理函数。当信号到达时,内核会暂停当前进程的执行流,保存上下文,然后跳转到用户注册的处理函数中执行。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
// 信号处理函数
void sigint_handler(int signo) {
// 注意:在信号处理函数中调用非异步信号安全的函数(如 printf)是不安全的
write(STDOUT_FILENO, "Caught SIGINT!\n", 15);
}
int main() {
struct sigaction sa;
sa.sa_handler = sigint_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0; // 未设置 SA_RESTART
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
// 阻塞等待信号
pause();
return 0;
}
重要限制:信号处理函数中只能调用异步信号安全(Async-Signal-Safe)的函数。例如
printf()、malloc() 是不安全的,而 write()、_exit() 是安全的。
3.3 信号如何中断系统调用
当一个慢速系统调用(如 read()、write()、accept()、wait())正在执行时,如果进程收到一个信号,并且该信号注册了处理函数,内核会执行以下步骤:
- 信号到达:内核检测到有未决信号,且进程处于可中断的睡眠状态(TASK_INTERRUPTIBLE)。
- 唤醒进程:内核将进程状态置为就绪,准备返回用户态。
- 执行信号处理:在系统调用返回路径上,内核发现需要递送信号,于是保存系统调用的返回值和错误码,转而执行信号处理函数。
- 系统调用失败:信号处理函数执行完毕后,内核恢复上下文,系统调用返回
-1,并将errno设置为EINTR。
核心结论:信号中断系统调用的本质是内核将“信号处理”的优先级置于“系统调用完成”之上。系统调用被强制返回
EINTR,让应用程序有机会响应信号。
3.4 可中断与不可中断的睡眠
Linux 内核将进程睡眠状态分为两类,这直接决定了系统调用是否会被信号中断:
| 睡眠状态 | 内核标志 | 是否可被信号中断 | 典型场景 |
|---|---|---|---|
| 可中断睡眠 | TASK_INTERRUPTIBLE | 是 | 等待 socket 数据、等待终端输入 |
| 不可中断睡眠 | TASK_UNINTERRUPTIBLE | 否 | 等待磁盘 I/O 完成、等待内核锁 |
排查技巧:使用
ps -eo pid,stat,wchan,comm 查看进程状态。如果进程处于 S 状态(可中断睡眠),则其系统调用可能被信号中断;如果处于 D 状态(不可中断睡眠),则信号无法中断其系统调用。
3.5 SA_RESTART 标志:自动重试机制
为了避免应用程序频繁处理 EINTR,POSIX 提供了 SA_RESTART 标志。当使用 sigaction() 注册信号处理函数时,如果设置了 sa_flags |= SA_RESTART,内核会在信号处理函数执行完毕后,自动重新启动被中断的系统调用,而不是返回 EINTR。
struct sigaction sa;
sa.sa_handler = sigint_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART; // 自动重启被中断的系统调用
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
exit(1);
}
注意:并非所有系统调用都支持
SA_RESTART。例如 poll()、select()、epoll_wait() 等 I/O 多路复用函数,即使设置了 SA_RESTART,在某些系统上仍可能返回 EINTR。因此,不能完全依赖 SA_RESTART,应用程序仍需具备处理 EINTR 的能力。
3.6 信号中断与 EINTR 的关系总结
- 触发条件:慢速系统调用 + 可中断睡眠状态 + 信号到达且注册了处理函数。
- 返回结果:系统调用返回
-1,errno设置为EINTR。 - 处理策略:应用程序应检查
errno == EINTR,并根据业务逻辑决定重试或退出。 - 优化手段:使用
SA_RESTART标志减少手动重试代码,但不能完全替代EINTR检查。
最佳实践:在编写健壮的服务器程序时,所有可能阻塞的系统调用返回值都应检查
EINTR,并实现重试循环。这是 Unix 系统编程中“防御性编程”的基本要求。