原文出处:http://miaoo.in/talk-about-unix-io-model.html

在实际应用中,数据操作通常分为输入和输出,那么以输入为例,在操作系统中,一个数据的输入通常分为以下两个过程:

a. 等待数据准备好.
b. 将准备好的数据从内核拷贝到用户空间.

下面我们将会分别讨论 I/O 模型中的两个大类,即 同步 I/O 与 异步 I/O。

1. 同步 I/O

同步与异步 I/O 的最大不同,就是在在进行数据复制时(即过程 b ),所有的同步 I/O 模型均会发生阻塞。进一步来看,又可以根据在等待数据准备好时( 过程 a )是否发生阻塞,将同步 I/O 分为:

  • 阻塞 I/O ( blocking I/O ). 其在过程 a 与 过程 b 中均会阻塞。
  • 非阻塞 I/O ( non-blocking I/O ).  过程 a 不阻塞, 过程 b 阻塞。在过程 a 阶段若无数据准备好,则内核立即返回 EWOULDBLOCK 错误 (通过设置 errno),用户进程立即返回因而不会发生阻塞。此时,用户进程可以不断的通过轮询( polling )的方式查询数据是否准备好。一旦数据准备就绪, 进程便会进入阻塞模式(即阻塞于过程 b ),进行数据的拷贝直至完成。
  • I/O 复用( I/O multiplexing, event-driven ).  I/O 复用有时又被称为 事件驱动 I/O, 它的最大优势在于,我们可以将感兴趣的多个I/O事件(更精确的说,应该是 I/O 所对应的文件描述符)注册到 select/poll/epoll/kqueue 之中某一个系统调用上(很多时候,这些系统调用又被称为多路复用器。假设此时我们选择了 select() )。此后,调用进程会阻塞在 select() 系统调用之上(而不是阻塞在真正的 I/O 系统调用(如 read(), write() 等)上)。select() 会负责监视所有已注册的 I/O 事件,一旦有任意一个事件的数据准备好,那么 select() 会立即返回,此时我们的用户进程便能够进行数据的复制操作(过程 b )。总而言之,I/O 复用的优点就在于可以同时等待多个I/O事件;而缺点是会进行两次系统调用(一次 select(), 一次 read() )。

通过上面的讨论可以清楚的看到,同步 I/O 总会有阻塞的过程,这就是“同步”最本质的特征。

2.异步 I/O

如前文所说,异步 I/O 的最大特点在于在过程 a 和过程 b 中, 用户进程均不阻塞。 用户进程告知内核启动某一 I/O 操作, 并让内核全权代为执行(包括等待数据及拷贝数据至用户空间),此后用户进程可以立即执行其它的任何操作。等到所有 I/O 过程执行完成后, 内核会通知用户程。由此可见,在整个过程中,用户进程均不阻塞。

以上谈到了 Unix 系统当中 I/O 操作的具体执行方式。在此基础之上, 我们介绍两种常见的 I/O 事件处理模型。事件处理模型的意义在于我们从更宏观的角度来看待实际应用中如何来处理 I/O 事件。我们还是以一个读操作( read() )为例, 根据 I/O 操作是基于同步或异步,分别介绍如下两种模型:

  • Reactor 模式( event loop ) . 基于同步 I/O
  1. 将要读的文件描述符注册到多路复用器中(如 select(), poll(), epoll() )。
  2. 调用进程阻塞在多路复用器上,其等待已注册的任意一个 I/O 事件发生。
  3.  一旦事件发生(即文件描述符变为可读, 多路复用器返回), 将调用用户提供的事件处理函数( event handler)进行实际的 I/O 复制操作。
  4. 读取完成后,事件处理函数可以对数据进行进一步的处理。
  • Proactor 模式. 基于异步 I/O
  1. 用户进程启动一个异步读文件操作, 同时将该操作注册到多路复用器上。多路复用器并不关心文件是否可读,而只关心该异步操作是否完成。
  2. 整个异步读文件操作由内核完成,用户进程不需要关心。多路复用器阻塞以等待某个完成通知的到达。
  3. 当内核完成了整个读文件操作 – 即数据已经准备好,并已由内核复制到了用户事先提供的缓冲区后 – 通知多路复用器读操作已完成。
  4. 多路复用器再调用相应的事件处理函数( event handler )处理位于用户缓冲区的数据。

由此我们可以看到 Reactor 与 Proactor 最大的区别在于数据准备好后,是由谁来将这些数据复制到用户空间中的缓存区之中。Reactor 由于调用的是同步 I/O , 所以当多路复用器由于数据准备好而返回之后,将会由用户的事件处理函数自行将数据复制到用户缓冲区中, 而 Proactor 由于调用的是异步 I/O ,  因此等待及复制数据均由内核完成,用户进程只需要等待内核的完成通知,此后由事件处理函数对已在缓冲区中的数据进行进一步的处理。

由于 Proactor 模型需要操作系统提供异步 I/O 的支持,要求较高,故基于上面所描述的思想, 我们可以用 Reactor 模型来模拟 Proactor (只需要用户额外提供用户缓冲区来存放读取出的数据)。简单来说,就是由多路复用器来代替用户进程来完成实际的 I/O 复制操作。

在 Reactor 模型的第 3 步中,当某个文件描述符可读使得多路复用器返回之后,多路复用器执行一个非阻塞读操作, 将数据从内核读至用户提供的缓冲区中,此操作完成后通知(调用)对应的事件处理函数,告知其 I/O 操作已完成。 这样一来,我们就可以在各种系统中(无论支持异步 I/O 与否)均对外提供统一的 Proactor 模型接口,而对用户隐藏其后的具体实现细节。

参考资料

1. < Unix network programming >, Chapter 6.

2. Comparing Two High-Performance I/O Design Pattern