4. EINTR 错误详解

EINTR(Error Interrupted)是 POSIX 标准中定义的一个关键错误码,表示系统调用在执行过程中被信号中断。理解 EINTR 的产生机制与正确处理方式,是编写健壮系统程序的基础。

4.1 EINTR 的定义与宏值

在 Linux 头文件 <errno.h> 中,EINTR 被定义为整数常量 4(x86 架构)。其宏定义如下:

#define EINTR 4   /* Interrupted system call */

当系统调用因信号中断而提前返回时,内核会将 errno 设置为 EINTR,并返回 -1。应用程序通过检查 errno == EINTR 来判断是否发生了信号中断。

属性 值
宏名称 EINTR
数值(x86) 4
含义 系统调用被信号中断
POSIX 要求 所有慢速系统调用都可能返回 EINTR
头文件 <errno.h>
关键认知:EINTR 不是错误,而是一种通知机制。它告诉应用程序:“系统调用被信号打断了,你可以决定是否重试。”

4.2 EINTR 的产生场景

EINTR 主要发生在慢速系统调用(slow system call)中。这类调用可能阻塞进程无限期等待,例如:

  • 文件 I/O:read()、write() 在操作管道、终端设备、网络套接字等“慢速”文件描述符时
  • 进程同步:wait()、waitpid() 等待子进程状态变化
  • 信号等待:pause()、sigsuspend()、sigwaitinfo()
  • 文件锁:flock()、fcntl() 的 F_SETLKW 操作
  • 消息队列:mq_receive()、mq_send()
  • Socket 操作:connect()、accept()、recv()、send()
注意:对普通磁盘文件的 read()/write() 通常不会返回 EINTR,因为内核使用 DMA 直接传输数据,不涉及阻塞等待。但若文件位于 NFS 等网络文件系统上,仍可能产生 EINTR。

4.3 read/write 被信号中断的典型流程

以下 SVG 图展示了 read() 系统调用被信号中断的完整生命周期:

read() 系统调用被信号中断的完整流程 用户态:应用程序调用 read(fd, buf, count) 内核态:系统调用入口,进入等待队列 进程睡眠等待数据到达(TASK_INTERRUPTIBLE) 正常:数据到达,唤醒进程,拷贝数据 信号到达:唤醒进程,不拷贝数据 返回 > 0(实际读取字节数) 返回 -1,errno = EINTR 用户态:继续处理读取的数据 用户态:检查 errno,决定重试 关键区别:正常路径拷贝了数据,中断路径没有拷贝数据,需要应用程序显式重试

4.4 POSIX 标准对 EINTR 的规定

POSIX.1-2001 及后续版本对 EINTR 有明确规范:

  • 慢速系统调用:如果系统调用被信号处理函数中断,且信号处理函数没有使用 SA_RESTART 标志,则系统调用应返回 EINTR。
  • 自动重启:使用 sigaction() 注册信号处理函数时,若设置 sa_flags |= SA_RESTART,内核会自动重启某些被中断的系统调用(如 read()、write()、wait()),应用程序不会看到 EINTR。
  • 不可重启的系统调用:部分系统调用即使设置了 SA_RESTART 也不会自动重启,例如 select()、poll()、epoll_wait()、sleep()、pause() 等。
系统调用类别 SA_RESTART 行为 示例
可自动重启 内核自动重试,不返回 EINTR read()、write()、wait()、accept()
不可自动重启 即使设置 SA_RESTART,仍返回 EINTR select()、poll()、epoll_wait()、sleep()
从不产生 EINTR 不适用 磁盘文件 read()/write()、getpid()
常见陷阱:不要依赖 SA_RESTART 解决所有 EINTR 问题。对于不可自动重启的系统调用,应用程序必须手动处理 EINTR。此外,SA_RESTART 的行为在不同 Unix 系统间存在细微差异,跨平台代码应显式处理 EINTR。

4.5 EINTR 与信号处理函数的交互

以下 SVG 图展示了信号处理函数、SA_RESTART 标志与系统调用返回三者之间的关系:

信号处理函数与 EINTR 的交互决策树 信号到达 SA_RESTART 已设置 内核自动重启系统调用 应用程序无感知 SA_RESTART 未设置 系统调用返回 -1 errno = EINTR 应用程序检查 errno 手动重试系统调用

4.6 典型代码模式:正确处理 EINTR

以下代码展示了在 read() 调用中正确处理 EINTR 的标准模式:

#include <unistd.h>
#include <errno.h>

ssize_t safe_read(int fd, void *buf, size_t count) {
    ssize_t ret;
    
    do {
        ret = read(fd, buf, count);
    } while (ret == -1 && errno == EINTR);
    
    return ret;
}

对于需要读取指定字节数的场景,更健壮的模式如下:

ssize_t readn(int fd, void *buf, size_t count) {
    size_t nleft = count;
    ssize_t nread;
    char *ptr = (char *)buf;
    
    while (nleft > 0) {
        nread = read(fd, ptr, nleft);
        
        if (nread == -1) {
            if (errno == EINTR) {
                continue;   /* 被信号中断,重试 */
            } else {
                return -1;  /* 真正的错误 */
            }
        } else if (nread == 0) {
            break;          /* EOF */
        }
        
        nleft -= nread;
        ptr += nread;
    }
    
    return count - nleft;
}
最佳实践:建议封装 safe_read()、safe_write() 等工具函数,在项目所有 I/O 操作中统一使用。这可以避免因遗漏 EINTR 检查而导致的间歇性数据丢失或程序异常。

4.7 EINTR 与多线程

在多线程程序中,EINTR 的处理需要额外注意:

  • 线程信号:使用 pthread_kill() 向特定线程发送信号时,只有目标线程中的系统调用会返回 EINTR。
  • 异步信号安全:信号处理函数中只能调用异步信号安全函数(async-signal-safe functions),如 write()、sig_atomic_t 操作。
  • 线程池场景:如果线程池中的工作线程被信号中断,应确保 EINTR 处理逻辑不会破坏共享状态。
场景 EINTR 行为 建议
单线程 + 信号处理 主线程系统调用可能被中断 使用 SA_RESTART 或手动重试
多线程 + 信号处理 只有接收信号的线程受影响 每个线程独立处理 EINTR
多线程 + 信号屏蔽 屏蔽信号的线程不会收到 EINTR 使用 pthread_sigmask() 控制