IO 模型
IO 模型
本文讨论“程序如何等待和读取网络数据”。
select、poll、epoll、kqueue的具体用法集中在《IO多路复用》笔记,避免重复。
理解一次网络读取
以 TCP 的 recv() 为例,读取通常包含两个阶段:
1 | 阶段一:等待内核接收缓冲区中出现可读取的数据 |
其中“等待”可能让线程睡眠,也可能立即返回;“复制”通常仍需要 CPU 执行。理解这两个阶段,才能区分阻塞、非阻塞、就绪通知和异步完成通知。
注意:TCP 是字节流。即使 recv() 返回“可读”,也只表示当前至少能读到一些数据或连接状态发生了变化,不表示收到了完整业务消息。消息边界应由应用协议处理。
同步 I/O 与异步 I/O
这组概念最容易与“阻塞/非阻塞”混淆。
| 概念 | 关键问题 | 含义 |
|---|---|---|
| 阻塞 / 非阻塞 | 调用会不会立刻返回 | 阻塞调用会等待条件满足;非阻塞调用立即返回结果或“暂时不可做” |
| 同步 / 异步 | 谁负责完成实际 I/O | 同步 I/O 中,应用在就绪后自己调用 recv() / send();异步 I/O 中,内核完成请求后再通知应用 |
例如:epoll_wait() 等待“某个 fd 可读”,随后程序自行 recv(),这仍是同步 I/O,只是等待阶段通过 I/O 多路复用实现。Windows IOCP 或 Linux io_uring 的完成通知式用法更接近异步 I/O。
阻塞 I/O
默认创建的 Socket 通常处于阻塞模式。
1 | char buffer[4096]; |
如果没有数据,recv() 会让当前线程睡眠;有数据、对端关闭或出错时才返回。
| 返回值 | TCP 中的含义 |
|---|---|
> 0 |
实际读到的字节数 |
0 |
对端正常关闭了发送方向(收到 FIN) |
-1 |
出错;若 errno == EINTR,通常可重试 |
阻塞 I/O 的优点是代码直观、CPU 不会因空转而浪费;缺点是一个慢客户端可能长期占用一个线程。它适合连接数量很少的工具、学习示例,或“每个连接独占一个工作线程”的服务器。
非阻塞 I/O
非阻塞 Socket 上,如果当前不能继续读写,调用不会等待,而是立即返回 -1,并把 errno 设为 EAGAIN 或 EWOULDBLOCK(两者可能是同一个值)。
1 |
|
F_GETFL 读取现有状态标志,F_SETFL 写入新标志。必须保留原有标志并使用按位或加入 O_NONBLOCK,不能直接用 O_NONBLOCK 覆盖所有旧标志。
非阻塞读取的典型处理:
1 | char buffer[4096]; |
仅仅把 Socket 设为非阻塞,再用循环不断调用 recv(),会形成忙轮询并占满 CPU。非阻塞 I/O 通常应和 I/O 多路复用配合:没有事件时线程休眠,有事件后才读写。
I/O 多路复用
I/O 多路复用让一个线程等待多个文件描述符的事件。典型过程是:
1 | 注册或准备关注的 fd |
它适合大量连接大部分时间空闲、偶尔有数据到来的场景,例如聊天服务器、网关、Web 服务器。常见接口是 select、poll、Linux epoll 和 BSD/macOS kqueue。
多路复用报告的是就绪(readiness),不是“完整 I/O 已完成”:
- 可读:可能有数据、收到 FIN,或发生某些错误;仍须调用
recv()判断。 - 可写:通常表示内核发送缓冲区有空间;一次
send()仍可能只发送部分数据。 - 错误/挂起:应结合具体接口的错误标志、
recv()返回值和getsockopt(SO_ERROR)处理。
详细 API、触发模式及 C++ 示例见《IO多路复用》。
信号驱动 I/O
信号驱动 I/O 使用 SIGIO 等信号通知进程某个 fd 可能可读或可写。传统 Unix 可通过 fcntl() 设置 O_ASYNC 并指定接收信号的进程。
它的优点是等待时不必显式轮询;但信号处理受限、调试困难、可移植性与可维护性较差。现代网络服务器很少把它作为主要模型,通常优先使用 epoll、kqueue 或完成队列型异步 I/O。
异步 I/O
异步 I/O(AIO)的目标是:应用提交读写请求后立即继续执行,内核在数据传输完成后以完成事件通知应用。
1 | 应用提交异步读请求 |
它与 I/O 多路复用的区别在于:多路复用通知“现在可以自己读了”,异步 I/O 通知“此前提交的读已经完成,结果在这里”。
常见实现:
| 平台 | 机制 | 说明 |
|---|---|---|
| Windows | IOCP | 高性能完成端口,典型 Proactor 风格 |
| Linux | io_uring |
通过提交队列和完成队列支持多种异步操作 |
| POSIX | POSIX AIO | 历史接口,网络场景使用相对有限 |
异步 I/O 可减少等待线程,但会引入请求生命周期、缓冲区所有权、取消、超时、并发回调和错误处理等复杂性。是否采用应根据平台、库支持和实际性能需求决定。
常见错误与处理原则
| 情况 | 含义 | 常见处理 |
|---|---|---|
EINTR |
系统调用被信号中断 | 大多数情况下重试;同时遵守程序自身的退出逻辑 |
EAGAIN / EWOULDBLOCK |
非阻塞 fd 暂时不能读或写 | 停止当前读写,等待下一次对应事件 |
recv() == 0 |
TCP 对端关闭发送方向 | 处理完已读数据后关闭或进入协议规定的收尾流程 |
send() 部分成功 |
只写入了部分字节 | 记录已发送位置,等待可写后继续发送剩余数据 |
可读后 recv() 失败 |
就绪状态与调用间可能被其他线程改变,或发生错误 | 按 errno 和连接状态处理,不要假定“可读必然有正文” |
选择建议
| 场景 | 更合适的起点 |
|---|---|
| 学习、命令行小工具、连接数很少 | 阻塞 I/O |
| 每个连接需要长时间阻塞式业务处理 | 阻塞 I/O + 有上限的线程池 |
| 大量空闲长连接,业务处理很短 | 非阻塞 I/O + 多路复用 + 事件循环 |
| 高吞吐、跨平台且对平台原生能力有要求 | 评估 IOCP、io_uring 或成熟网络库 |
无论选择哪种模型,都不能跳过协议解析、超时控制、资源限制和背压(下游处理不过来时限制继续读取/发送)这些基础问题。



