I/O 模型
I/O 模型描述调用者等待数据与接收结果的方式。本文以 Linux socket 为主,负责阻塞、就绪和完成语义。
1. 面试核心
先记住这几条,再读正文- 阻塞与非阻塞。 阻塞读取可等待数据,非阻塞读取在暂不可用时返回 EAGAIN。卡 · 面试核心
- 就绪与完成。 就绪通知允许应用尝试 I/O,完成通知交付已提交操作的结果。
- 读取结果。 TCP recv 返回正数表示已读字节,正长度读取返回 0 表示 EOF。卡 · 读取结果与部分写入
- 部分写入。 部分写入只接受部分字节,应用需保留并继续发送剩余部分。
2. 等待数据与执行读取
网络数据经协议栈进入接收缓冲区。应用调用 read 或 recv 取得字节。阻塞 socket 在数据不可用时让线程等待。O_NONBLOCK socket 则返回暂不可用的错误。
数据就绪和复制数据是不同阶段。非阻塞读取仍由本次系统调用交付读取结果。一次读取可以只返回部分数据。
TCP 的消息拆分由应用协议负责,见 RPC。
3. 就绪与完成
就绪模型:等待可读 → 调用 recv → 使用返回字节
完成模型:提交读取 → 获取完成结果 → 使用结果缓冲区
| 模型 | 调用者行为 |
|---|---|
| 阻塞 I/O | 调用时等待数据与操作结果。 |
| 非阻塞 I/O | 暂不可用时返回,调用者稍后重试。 |
| I/O 多路复用 | 等待多个对象就绪,再执行读写。 |
| 信号驱动 I/O | 通过信号获知 I/O 就绪,再执行读写。 |
| 异步 I/O | 提交操作,再通过完成机制取得结果。 |
Reactor 分派就绪事件,由处理器执行 I/O。Proactor 分派已完成操作的结果。库可用线程池封装阻塞操作,再暴露异步接口。接口形式与内核执行方式需要分别判断。
Linux io_uring 使用提交队列和完成队列。应用提交读取后,仍需维持缓冲区有效。收到对应完成结果后,才能安全复用该缓冲区。
4. 读取结果与部分写入
对正长度 TCP recv:
| 返回值 | 含义与处理 |
|---|---|
| 大于 0 | 取得部分或全部可读字节,交给协议缓冲区。 |
| 等于 0 | 对端有序关闭发送方向,且此前数据已读完。 |
| -1,EAGAIN / EWOULDBLOCK | 当前无可读数据,回到事件等待。 |
| -1,EINTR | 操作在返回数据前被信号中断,可按取消策略重试。 |
| 其他错误 | 按具体错误清理或恢复连接。 |
UDP 允许零长度数据报。UDP recv 返回 0 也可表示收到空数据报。判断 EOF 需要先明确 socket 类型。
非阻塞 send 返回正数时,只接受了相应字节数。应用保存剩余部分,等待可写后继续发送。返回 EAGAIN 时保留全部未发送数据。
5. 使用边界
反复重试 EAGAIN 会忙轮询并占用 CPU。事件等待可让线程在无事可做时休眠。事件循环内的慢业务应交给其他执行资源。
Linux 普通文件上的 O_NONBLOCK 对磁盘等待无效。普通文件读写仍会等待设备完成。讨论非阻塞时,应说明对象和系统调用。
6 · 面试题卡片
本页题目与参考回答
6.1 阻塞和非阻塞 I/O 有什么区别,非阻塞是否就等于异步?
考察重点:暂不可用时的返回行为、就绪通知与异步完成的区别。
参考回答
阻塞调用会在数据不可用时等待。
非阻塞调用返回 EAGAIN,由应用稍后再次读取。异步 I/O 则先提交操作,再接收完成结果。非阻塞描述的是调用行为,异步描述的是操作完成方式。
6.2 非阻塞 recv 返回 0、EAGAIN 和部分数据时分别怎样处理?
考察重点:TCP EOF、暂不可用与协议缓冲。
相关内容:读取结果与部分写入。
参考回答
正长度 TCP recv 返回 0 表示已读完对端关闭前的数据。
EAGAIN 表示暂时无数据,应继续等待事件。正数表示取得相应字节,要交给协议缓冲区。UDP 的零长度数据报也会返回 0,应区别处理。
6.3 Reactor 与 Proactor 的事件分别表示什么?
考察重点:就绪分派和操作完成。
相关内容:就绪与完成。
参考回答
Reactor 分派就绪事件,处理器随后执行读写。
Proactor 分派已完成 I/O 的结果。异步操作的缓冲区必须存活到操作结束。库用线程池模拟异步时,也要区分接口与内核模型。