这篇笔记以 Unix 网络 I/O 的经典分类为线索,比较等待数据就绪和把数据交付给应用程序这两个阶段。就绪通知与操作完成通知的区别,是理解这些模型的关键。
1. 阻塞 IO 模型

应用调用阻塞式 recv 等接口后,如果数据尚未就绪,调用线程会等待;数据就绪后再把数据复制到用户缓冲区,随后返回。正常完成读取时,应用才获得数据;调用也可能因错误、中断或超时而返回。
典型例子是阻塞 socket。等待期间线程通常被挂起,不需要持续忙轮询;实现相对直观。并发连接较多时,如果为每个连接长期占用一个线程,会增加线程和调度成本,是否适合仍需结合工作负载评估。
2. 非阻塞 IO 模型

设置非阻塞标志后,读取时若暂时没有可读数据,接口通常以 EAGAIN 或 EWOULDBLOCK 返回,应用稍后重试。数据就绪时,读取调用仍需完成实际的数据交付;非阻塞不等于异步完成通知。
应用可以反复尝试读取,但忙轮询会消耗 CPU。实际程序常把非阻塞 socket 与 I/O 多路复用结合起来,避免逐个连接持续轮询。
3. IO 多路复用模型

一个线程可以等待多个文件描述符的就绪事件。select、poll 或 epoll_wait 返回后,应用再对就绪的描述符执行读写。这里监控的是文件描述符;可以属于同一进程内的多个连接,并不要求每个连接对应一个独立进程。
多路复用接口本身可以阻塞等待事件。事件就绪也不代表数据已经复制到用户缓冲区,因此通常会配合非阻塞读写,并处理暂时无数据、部分读写和连接关闭等情况。
select、poll、epoll 的区别
| 维度 | select | poll | epoll |
|---|---|---|---|
| 描述符集合 | 每次传入并在返回后重建集合 | 每次传入描述符数组 | 通过 epoll_ctl 维护兴趣集合,epoll_wait 获取就绪事件 |
| 数量限制 | Linux 常用 glibc 的 FD_SETSIZE 为1024,限制的是可表示的描述符编号 | 没有 select 的固定集合限制,仍受进程及系统资源限制 | 没有 select 的固定集合限制,仍受描述符、内存和监听资源限制 |
| 就绪结果处理 | 需要检查返回的集合 | 需要检查返回的数组 | 返回就绪事件,应用按返回结果处理 |
| 等待方式 | 可阻塞或设置超时 | 可阻塞或设置超时 | 可阻塞或设置超时,timeout为0时立即返回 |
| 通知模式 | 通常按水平触发理解 | 通常按水平触发理解 | 支持水平触发和边沿触发 |
epoll 的优势来自持续维护监听集合和就绪事件列表,不能概括为"永远不阻塞"或"整个流程都是 O(1)"。返回并处理 k 个事件至少需要与 k 相关的工作量。标准 epoll 接口也不是通过 mmap 把监听列表直接映射给用户空间。
上述行为可参考 Linux select 手册、epoll 手册和 epoll_wait 手册。
4. 信号驱动 IO 模型

应用请求在 I/O 就绪时收到信号,然后继续执行其他工作。就绪信号到来后,应用再读取数据。这里通知的是"可以进行 I/O",还不是"整个 I/O 操作已经完成"。具体通知行为取决于描述符类型和系统接口;信号处理也需要遵守相应的安全限制。
5. 异步 IO 模型

应用提交 I/O 请求后继续执行其他工作,操作完成时再通过事件、回调或其他机制获得结果。与就绪通知相比,完成通知表示请求的数据交付操作已经完成,而非要求应用在收到通知后再发起一次常规读取。
异步 API 的实现方式、支持的文件类型和限制依平台及运行时而异,不能仅凭接口名称判断内部是否占用了工作线程,也不能把所有异步接口都视为同一种内核实现。
图示补充
以下示意图沿用原学习笔记。部分图片带有极客时间水印,保留原有署名;具体接口语义以相应系统手册为准。



原文发布于我的个人博客:IO模型。