YON Blog

I/O 模型

Overview

I/O 模型是 操作系统 抽象出来的概念, 方便操作系统统一的管理. 各种各样的模型之间到底有什么区别呢? 阻塞 (blocking) 和 同步 (synchronous) 是什么关系?

I/O 过程

简单来讲, I/O 的过程分为两个部分 (以读取为例):

  1. 等待数据就绪
  2. 把数据从内核拷贝到用户进程空间

用 socket 来举例, 第一个步骤就是等待网卡接收到数据, 当接收到数据, 处理器把数据拷贝到内核的缓冲区. 第二个步骤就是把数据从内核的缓冲区拷贝到用户程序的缓冲区.

第一个步骤还是很直观的, 但是为什么要有第二个步骤呢? 数据拷来拷去不是很没效率么? 这牵扯到处理器的特权级别了. 现代的处理器在运行时是分为不同的级别的. 低级别程序会受到一定的限制, 比如只能访问一部分内存, 只能执行特定的指令. 所以需要将内核的数据拷贝到用户进程能访问的空间去.

五种 I/O 模型

下面的讨论主要基于 UNIX 操作系统.

Blocking I/O

最常见的 I/O 模型就是阻塞型的. 以 socket 来举例, 当我们调用 recv(), send(), connect(), accept() 这四个方法时, 线程都会阻塞住. 也就是说从等待数据就绪到数据拷贝到用户进程都是阻塞的.

Nonblocking I/O

当我们把 socket 设置成 nonblocking 的, 这时候调用 recv(), send(), connect(), accept(), 如果没有数据会返回 EWOULDBLOCK 错误而不是阻塞住, 如果有数据那么就会把数据从内核拷贝到用户进程. 虽然叫 nonblocking 但是在把数据从内核拷贝到用户进程 (写数据是从用户进程到内核) 时还是会阻塞住.

I/O multiplexing

在这种模型下, 我们一个线程能 同时 处理多个 I/O. 还是以 socket 为例, 我们将一个 socket 的集合传给 select() 函数, 调用 select() 后线程会阻塞住, 当传入的集合中有一个 socket 有数据了, select() 就会返回, 然后通过相应的方法将数据读取, 同样的, 在读取数据时线程会被阻塞住. 传给 select() 的 socket 可以是 blocking 的. 因为阻塞与否影响的只是 recv(), send(), connect(), accept() 这四个函数. 但是最好是 nonblocking 的, 因为即使 select() 返回了, socket 也可能在调用那四个函数时被阻塞. (见 select with blocking and non-blocking socket)

select, poll, epoll

这三个函数是 I/O 多路复用模型常用的函数, 各自实现的功能差不多.

select 传入的文件描述符的集合的大小是有限制的, 通常是 1024. poll 和 epoll 没有限制 (主要的限制是你的硬件资源了). select 和 poll 都会随着传入的文件描述符的数量增多而效率变低, epoll 的效率只和活跃的文件描述符数量有关. epoll 使用消息驱动来通知用户进程数据就绪.

epoll 怎么实现的我还蛮好奇的, 为什么 epoll 通常情况下效率要高呢? epoll 比 select 高效的原因

Signal-Driven I/O

在这个模型里, 我们向操作系统注册 handler (通过 sigaction 系统调用), 当数据就绪时, handler 会被操作系统调用, 这时候我们再去调用相应的方法获取数据 (会阻塞住).

Asynchronous I/O

和 Signal-Driven 类似, 我们需要向操作系统注册一个 handler. 这个模型和 Signal-Driven 最大的不同是, 当操作系统调用我们的 handler 时, 数据已经从内核态拷贝到用户进程了 (显然我们在注册 handler 的时候需要让操作系统知道把数据拷贝到哪里).

Synchronous I/O versus Asynchronous I/O

POSIX (Portable Operating System Interface) 其实只定义了两种类型的 I/O:

  • 同步的: 进行 I/O 操作时, 之前提到的两个过程中只要有一个会阻塞就算同步的
  • 异步的: 进行 I/O 操作时, 之前提到的两个过程都不会阻塞就是异步的

所以, 上面提到的五种 I/O 模型, 只有 Asynchronous I/O 是属于异步的, 其他的都属于同步的, 因为他们在把数据从内核拷贝到用户进程时都会阻塞住.

举个例子

找资料过程中发现用钓鱼来类比上面的几个模型是很合适的. 因为钓鱼成两个步骤, 等鱼儿上钩 (数据就绪), 把鱼儿放进桶里 (把数据从内核拷贝到用户进程)

  • Blocking: 下饵之后就一直等着直到鱼儿上钩, 再把鱼儿放到桶里.
  • Nonblocking: 下饵之后隔一会就来检查下有没有鱼儿上钩, 没有就做其他事情, 有就把鱼儿放进桶里.
  • I/O multiplexing: 同时用很多鱼竿, 然后在旁边看 (不能干其他事情), 哪个鱼竿有鱼了就捞上来放进桶里.
  • Signal-Driven: 用的是比较高级的鱼竿, 当有鱼上钩时会发出声音提醒 (可以专心做其它事情, 不用像 nonblocking 一样亲自来检查), 听到声音提醒之后把鱼儿捞起来放进桶里.
  • Asynchronous: 直接雇了个人 (操作系统), 当鱼上钩后捞起来放进桶里, 然后被雇佣的人会过来告诉你鱼钓到了.

6.2 I/O Models @ UNIX Network Programming

poll vs select vs event-based

Linux IO模式及 select、poll、epoll详解