2. 系统调用基础:概念、用户态/内核态切换与阻塞/非阻塞 I/O

系统调用是用户空间程序请求内核服务的唯一合法入口。理解系统调用的底层机制,是分析 EINTR 与 EAGAIN 错误的前提。本节从概念、特权级切换、I/O 模式三个维度建立基础。

2.1 系统调用的概念与定位

系统调用(System Call)将用户空间的请求安全地传递给内核,由内核代表进程执行特权操作。应用程序不能直接访问硬件或内核数据结构,必须通过系统调用接口。

特性说明
入口唯一性所有内核服务必须通过系统调用表(sys_call_table)分发
特权提升CPU 从用户态切换到内核态,获得执行特权指令的能力
同步/异步大部分系统调用是同步的,但可通过非阻塞标志或 AIO 实现异步
错误返回失败时返回 -1 并设置 errno,如 EINTR、EAGAIN
关键认知:系统调用不是函数调用。函数调用在用户态栈上完成,系统调用涉及 CPU 特权级切换、内核栈切换、以及可能的状态保存与恢复。每次系统调用都有固定的性能开销。

2.2 用户态与内核态切换

CPU 通过特权级(Ring 0 ~ Ring 3)隔离内核与用户空间。Linux 使用 Ring 0 作为内核态,Ring 3 作为用户态。系统调用触发切换的典型路径如下:

  1. 用户程序调用 glibc 封装的库函数(如 read())。
  2. glibc 将参数放入寄存器,执行 syscall 指令(x86-64)或 int 0x80(x86)。
  3. CPU 切换到 Ring 0,跳转到内核入口点(entry_SYSCALL_64)。
  4. 内核保存用户态寄存器上下文,切换到内核栈。
  5. 根据系统调用号查表,执行对应的内核函数。
  6. 返回前恢复用户态上下文,执行 sysret 切回 Ring 3。
性能参考:一次空系统调用(如 getpid())在现代 x86-64 上约耗时 50~100 ns,其中上下文切换占主要开销。频繁系统调用会显著降低吞吐量。
用户态与内核态切换流程 用户态 (Ring 3) 应用程序调用 read(fd, buf, len) glibc 封装:参数放入寄存器 执行 syscall 指令 内核态 (Ring 0) entry_SYSCALL_64 入口 保存用户态寄存器上下文 切换内核栈,查 sys_call_table syscall 指令 CPU 自动切换 执行内核函数 (如 sys_read) 处理 I/O、检查权限、操作设备 恢复用户态上下文 sysret 指令切回 Ring 3 说明: 每次系统调用至少经历两次特权级切换(用户→内核→用户),涉及寄存器保存/恢复、栈切换、 可能还有 TLB 刷新。这是系统调用比普通函数调用慢 10~100 倍的根本原因。

2.3 阻塞与非阻塞 I/O

系统调用的 I/O 模式直接影响进程的调度行为,也是 EAGAIN 和 EINTR 产生的直接场景。

2.3.1 阻塞 I/O

默认模式下,系统调用(如 read()、write()、accept())会使进程进入睡眠状态,直到操作完成或发生错误。进程被标记为 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE,从运行队列移出。

  • 优点:编程模型简单,同步等待,逻辑清晰。
  • 缺点:进程在等待期间不占用 CPU,但若并发量大,需要大量线程/进程。
  • 与 EINTR 的关系:阻塞在 TASK_INTERRUPTIBLE 状态的进程,收到信号后会被唤醒,系统调用返回 -1 且 errno 设为 EINTR。

2.3.2 非阻塞 I/O

通过设置文件描述符为 O_NONBLOCK 标志,系统调用在无法立即完成时不会阻塞,而是立即返回 -1 并设置 errno 为 EAGAIN(或 EWOULDBLOCK)。

  • 优点:进程可以轮询多个 fd,配合 epoll/select 实现高并发。
  • 缺点:需要循环检查返回值,编程复杂度增加。
  • 与 EAGAIN 的关系:非阻塞模式下资源暂不可用时,内核直接返回 EAGAIN,提示“现在无法完成,稍后重试”。
对比维度阻塞 I/O非阻塞 I/O
进程状态睡眠等待(不占用 CPU)立即返回(占用 CPU 轮询)
返回时机数据就绪或出错立即返回,无论是否就绪
典型 errnoEINTR(被信号中断)EAGAIN(资源暂不可用)
适用场景低并发、简单逻辑高并发、事件驱动
CPU 利用率低(等待时不消耗)高(轮询时消耗)
常见误区:非阻塞 I/O 不等于异步 I/O。非阻塞 I/O 仍然是同步操作——调用立即返回,但数据拷贝仍由当前线程完成。真正的异步 I/O(如 Linux AIO、io_uring)在内核完全就绪后才通知用户,不占用用户线程等待。
阻塞 vs 非阻塞 I/O 时序对比 阻塞 I/O read(fd, buf, len) 进程进入 TASK_INTERRUPTIBLE 等待数据就绪(可能被信号中断) 返回数据 或 -1 (EINTR) 非阻塞 I/O (O_NONBLOCK) read(fd, buf, len) 数据未就绪?立即返回 返回 -1, errno = EAGAIN 用户可轮询或等待下次 epoll 通知 对比 EINTR 与 EAGAIN 的触发场景 • EINTR:阻塞系统调用执行期间,进程收到信号(如 SIGTERM、SIGALRM),内核唤醒进程, 系统调用返回 -1,errno = EINTR。常见于 read、write、wait、sleep 等阻塞调用。 • EAGAIN:非阻塞系统调用无法立即完成(如 socket 缓冲区空、锁不可用), 内核不等待,直接返回 -1,errno = EAGAIN。提示“现在不行,稍后重试”。

2.4 系统调用与进程状态转换

理解进程状态转换有助于定位 EINTR 的根源。阻塞系统调用将进程置为 TASK_INTERRUPTIBLE,此时信号可以唤醒进程;而 TASK_UNINTERRUPTIBLE 状态(如等待磁盘 I/O)则忽略信号,不会产生 EINTR。

进程状态是否可被信号中断典型场景与 EINTR 关系
TASK_RUNNING—正在执行或就绪不涉及
TASK_INTERRUPTIBLE是阻塞 read、sleep、wait信号唤醒后返回 EINTR
TASK_UNINTERRUPTIBLE否磁盘 I/O、NFS 挂起不会产生 EINTR
TASK_KILLABLE仅 SIGKILL某些文件系统操作仅被 SIGKILL 中断
排查提示:当应用程序收到 EINTR 时,检查信号处理函数是否注册了 SA_RESTART 标志。如果注册了 SA_RESTART,内核会自动重入部分系统调用(如 read、write),用户态不会感知 EINTR。但 select、poll、epoll_wait 等调用即使有 SA_RESTART 也可能返回 EINTR,需要手动处理。

2.5 本节小结

  • 系统调用是用户态请求内核服务的唯一入口,涉及 CPU 特权级切换和上下文保存。
  • 阻塞 I/O 使进程进入 TASK_INTERRUPTIBLE 睡眠,信号可中断并产生 EINTR。
  • 非阻塞 I/O 立即返回,资源不可用时产生 EAGAIN,需要用户轮询或事件驱动。
  • 进程状态 TASK_UNINTERRUPTIBLE 不会产生 EINTR,但可能导致进程“不可杀”。
  • 理解这些基础机制,是后续章节处理 EINTR 重试逻辑和 EAGAIN 优化策略的前提。