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())正在执行时,如果进程收到一个信号,并且该信号注册了处理函数,内核会执行以下步骤:

  1. 信号到达:内核检测到有未决信号,且进程处于可中断的睡眠状态(TASK_INTERRUPTIBLE)。
  2. 唤醒进程:内核将进程状态置为就绪,准备返回用户态。
  3. 执行信号处理:在系统调用返回路径上,内核发现需要递送信号,于是保存系统调用的返回值和错误码,转而执行信号处理函数。
  4. 系统调用失败:信号处理函数执行完毕后,内核恢复上下文,系统调用返回 -1,并将 errno 设置为 EINTR。
核心结论:信号中断系统调用的本质是内核将“信号处理”的优先级置于“系统调用完成”之上。系统调用被强制返回 EINTR,让应用程序有机会响应信号。
信号中断系统调用流程图 进程发起系统调用 如 read() / accept() 内核执行,进程进入睡眠 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 系统编程中“防御性编程”的基本要求。