Skip to content

信号中断系统调用:EINTR 与 SA_RESTART ​

注册了信号处理函数的程序,经常在处理函数执行完之后出问题:阻塞在 read()、accept() 上的系统调用莫名返回 -1,errno 是 EINTR;没料到这一层的程序轻则报错退出,重则状态错乱、出现堆栈异常。本文写给写信号处理逻辑、维护长驻服务或网络服务的 C/C++ 开发者,读完可以弄清 EINTR 的产生机制、两种标准应对方式,以及 SA_RESTART 标志到底能覆盖哪些系统调用——它在 Linux 上并不是万能的。

信号机制本身(产生、阻塞、捕捉的原理)见 Linux 信号机制:产生、阻塞与捕捉,本文专注「信号处理函数与系统调用的交互」这一个专题。

问题现场 ​

典型症状:程序捕获信号并执行信号处理函数,返回原执行流后,原本阻塞的系统调用不再继续等待,而是以失败告终——返回 -1,errno 被置为 EINTR(Interrupted system call)。上层代码若只按普通 I/O 错误处理,就可能异常退出或进入未定义状态。

中断是如何发生的 ​

程序阻塞在系统调用中(如 read()、write()、accept())时收到信号,完整时序是:

  1. 系统调用被中断:阻塞状态被立即打断;
  2. 执行信号处理函数:内核转去执行已注册的处理函数;
  3. 系统调用返回失败:处理函数完成后,被中断的系统调用默认返回 -1,不再自动继续;
  4. 错误码设置:errno 被设置为 EINTR;
  5. 程序异常退出:若调用方未处理该错误,程序可能异常终止或出现堆栈错误。

这是内核的默认行为,不是 bug——它把「要不要重试」的决定权交给了应用程序。

易受影响的系统调用 ​

未设置 SA_RESTART 时,以下系统调用被信号中断后会返回 -1 并置 errno = EINTR:

  • read() / write():文件、socket 读写
  • accept() / connect():网络连接
  • nanosleep():休眠(返回 -1/EINTR,剩余时间通过 rem 参数带出)
  • wait() / waitpid():进程等待
  • select() / poll() / epoll_wait():I/O 多路复用

注意一个特例:sleep(3) 被中断时不报错,而是返回剩余未休眠的秒数(Linux 上基于 nanosleep 实现,见 man 3 sleep)。

方案一:设置 SA_RESTART 标志(推荐) ​

注册信号处理函数时用 sigaction,把 sa_flags 设置为 SA_RESTART,被中断的系统调用会在处理函数返回后自动重新执行,调用方感知不到中断:

cpp
#include <signal.h>
#include <unistd.h>
#include <iostream>

void signal_handler(int signum) {
    std::cout << "Caught signal " << signum << std::endl;
    // 处理信号...
}

int main() {
    struct sigaction sa;
    sa.sa_handler = signal_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;  // 关键:被中断的系统调用自动重启

    if (sigaction(SIGINT, &sa, nullptr) == -1) {
        perror("sigaction");
        return 1;
    }

    char buffer[1024];
    ssize_t n = read(STDIN_FILENO, buffer, sizeof(buffer));

    if (n == -1) {
        perror("read");
    } else {
        std::cout << "Read " << n << " bytes" << std::endl;
    }
    return 0;
}

方案二:手动检查 EINTR 并重试 ​

不能用 SA_RESTART(或需要更精细控制)时,手动检查 errno 并重试是通用做法,适合封装成基础库函数:

cpp
#include <unistd.h>
#include <cerrno>

ssize_t safe_read(int fd, void* buf, size_t count) {
    ssize_t n;
    while ((n = read(fd, buf, count)) == -1) {
        if (errno == EINTR) {
            continue;   // 被信号中断,重试
        } else {
            return -1;  // 其他错误,返回失败
        }
    }
    return n;
}

SA_RESTART 的边界:哪些调用不会自动重启 ​

SA_RESTART 并非对全部系统调用生效。依据 man 7 signal,Linux 上以下调用即使设置了 SA_RESTART 也不会自动重启,被中断时始终返回 EINTR:

  • I/O 多路复用:select() / pselect() / poll() / ppoll() / epoll_wait() / epoll_pwait()——不是「行为依赖于实现」,Linux 上明确不重启
  • 休眠:nanosleep() / clock_nanosleep() / usleep(),通过剩余时间参数返回进度
  • 等待信号类:pause() / sigsuspend() / sigtimedwait() / sigwaitinfo()
  • System V IPC:msgrcv() / msgsnd() / semop() / semtimedop()
  • 设置了超时的 socket 调用:通过 setsockopt() 设置了 SO_RCVTIMEO/SO_SNDTIMEO 的 recv() / send() / accept() / connect() 等

相对应地,以下调用设置 SA_RESTART 后会自动重启:

  • 慢速设备(终端、管道、socket)上的 read() / readv() / write() / writev() / ioctl()
  • wait() / waitpid() / wait3() / wait4()(注意 waitid() 例外,不重启)
  • 未设置超时的 socket 调用(accept() / connect() / recv() / send() 等)
  • flock()、fcntl() 的 F_SETLKW/F_OFD_SETLKW、POSIX 消息队列、sem_wait()、getrandom() 等

最佳实践 ​

  1. 默认使用 SA_RESTART:多数信号处理场景下,设置此标志可以免除一层不必要的错误处理;
  2. 关键路径手动检查 EINTR:涉及 poll/epoll、nanosleep 等「SA_RESTART 管不到」的调用,或需要精细控制重试语义时,手动检查 EINTR 并决定是否重试;
  3. 信号处理函数保持简洁:只做置标志位等最小动作,避免在处理函数中执行复杂操作,减少与主流程的竞态窗口。

要点 ​

  • 信号处理函数返回后,被中断的阻塞系统调用默认失败返回,errno = EINTR——不是崩溃,是需要处理的标准行为
  • SA_RESTART 让 read/write/accept/waitpid 等自动重启,但 select/poll/epoll_wait、nanosleep、SysV IPC、设超时的 socket 调用无论如何都不重启,必须手动处理 EINTR
  • sleep(3) 特例:被中断不报错,返回剩余秒数
  • 两种方案可以并存:全局 SA_RESTART 兜底,关键调用再包一层 EINTR 重试

相关文章 ​

最近更新

基于 VitePress 构建