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 作为用户态。系统调用触发切换的典型路径如下:
- 用户程序调用 glibc 封装的库函数(如
read())。 - glibc 将参数放入寄存器,执行
syscall指令(x86-64)或int 0x80(x86)。 - CPU 切换到 Ring 0,跳转到内核入口点(entry_SYSCALL_64)。
- 内核保存用户态寄存器上下文,切换到内核栈。
- 根据系统调用号查表,执行对应的内核函数。
- 返回前恢复用户态上下文,执行
sysret切回 Ring 3。
性能参考:一次空系统调用(如
getpid())在现代 x86-64 上约耗时 50~100 ns,其中上下文切换占主要开销。频繁系统调用会显著降低吞吐量。
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 轮询) |
| 返回时机 | 数据就绪或出错 | 立即返回,无论是否就绪 |
| 典型 errno | EINTR(被信号中断) | EAGAIN(资源暂不可用) |
| 适用场景 | 低并发、简单逻辑 | 高并发、事件驱动 |
| CPU 利用率 | 低(等待时不消耗) | 高(轮询时消耗) |
常见误区:非阻塞 I/O 不等于异步 I/O。非阻塞 I/O 仍然是同步操作——调用立即返回,但数据拷贝仍由当前线程完成。真正的异步 I/O(如 Linux AIO、io_uring)在内核完全就绪后才通知用户,不占用用户线程等待。
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优化策略的前提。