I/O模型

翻译《The Sockets Networking API: UNIX® Network Programming Volume 1, Third Edition》中第六章中Unix的五种I/O模型的区别,以及POIX对于同步I/O与异步I/O的定义。


Unix下5中I/O模型的基本区别:

  • 阻塞I/O(blocking I/O)
  • 非阻塞I/O(nonblocking I/O),例如selectpoll
  • I/O多路复用(I/O multiplexing)
  • 信号驱动I/O(singal driven I/O)
  • 异步IO(asynchronous I/O),POSIX中定义的aio_functions

在输入操作的过程中,一般有两个阶段

  • 等待数据准备完毕
  • 将数据从内核(内存)复制到进程(内存)
    socket上的输入操作,第一步一般是等待网络中的数据到达,当数据到达之后,会被复制到内核缓冲区。然后第二步就是将数据从内核缓冲区复制到应用缓冲区。

阻塞I/O模型

在I/O模型中,最普遍的就是阻塞I/O模型,默认情况下,所有sockets都是阻塞的。如下图
Figure 6.1. Blocking I/O model.
这个例子中,我们使用UDP而不是TCP,因为可以将“数据准备好可以被读取”简单地理解为数据是否已经接收到数据。如果是TCP,这会很复杂。
在例子中,我们继续用recvfrom作为系统调用,因为我们要区分应用和内核。不管recvfrom现的,调用的时候都会产生从用户态到内核态的切换,一段时间之后重新回到用户态继续执行。
在上图中,进程调用recvfrom直到数据接收完成并且复制到应用缓冲区,系统调用的返回结果或者抛出异常。最平常的错误就是系统调用被中断信号中断。进程从调用recvfrom这段时间都处于阻塞中,当recvfrom用进程开始操作数据。

非阻塞I/O模型

当我们设置socket为非阻塞之后,当应用发起I/O请求的时候,如果数据还没有准备完毕,内核不会挂起进程,而是返回一个error。
Figure 6.2. Nonblocking I/O model.
最开始的三次recvfrom系统调用没有数据返回,所以内核马上返回了EWOULDBLOCK错误,第四次调用recvfrom,数据已经准备好,它被复制到应用缓冲区,然后recvfrom返回成功,之后应用可以开始处理数据。
当应用循环地对一个非阻塞的描述符调用recvfrom,被称为轮训(polling)。应用需要进行轮训,向内核请求检查一些操作是否已经完成,这会很浪费CPU时间,但是这种模型偶尔会出现在专用于一个功能的系统上。

I/O多路复用模型

应用在调用selectpoll这两个系统调用的时候被阻塞(被select或者poll阻塞),而不是被实际的I/O系统调用阻塞。
Figure 6.3. I/O multiplexing model.
上图是调用select的例子,在调用select的时候,应用被阻塞,直到数据可读。当select返回数据可读的时候,我们调用recvfrom将数据复制到应用的缓冲区中。
与阻塞I/O模型对比,看起来没有任何优势,反而会有缺陷,因为使用了selectrecvfrom,总共两次系统调用,比阻塞I/O模型多了一次。但是select可以同时监听多个描述符等待数据。

  • 另外一个与I/O多路复用模型很相似的是通过多线程使用阻塞I/O模型。这种模型不通过select监听多个描述符,而是使用多个线程(每个文件描述符分配一个线程)去执行I/O操作,例如recvfrom

信号驱动I/O模型

我们也可以使用信号,让内核在数据准备好的时候,给应用发送SIGIO信号。这种方式称为信号驱动I/O模型,如下图所示
Figure 6.4. Signal-Driven I/O model.
首先需要设置socket启动信号驱动I/O功能,然后通过sigaction系统调用来设置信号处理方法。sigaction会马上返回结果,然后进程不会被阻塞,继续往下执行。当数据接收完毕,内核会产生一个SIGIO信号,触发信号处理方法,方法中可以通过recvfrom将数据复制到应用缓冲区,然后通知主循环去处理数据,也可以通知主循环,让其调用recvfrom
这种模型的优点在于,当还没有接收到数据的时候,进程不会被阻塞。主循环可以继续执行,只需要等待信号处理方法的通知去处理数据或者读取数据。

异步I/O模型

异步I/O由POSIX定义(融合了各种标准中的实时方法),与其他实时方法相似,它们的工作方式都是通知内核开始I/O操作,然后在I/O操作完成的时候通知应用(包括将数据从内核缓冲区复制到应用缓冲区)。异步I/O模型与信号驱动I/O模型最主要的区别在于,在信号驱动I/O模型中,当数据准备完毕的时候,内核就会进行通知,而在异步I/O模型中,只有当I/O操作完成的时候(复制到应用缓冲区),内核才会通知。如下图所示
Figure 6.5. Asynchronous I/O model.
通过系统调用aio_readPOSIX中定义异步I/O方法都是以aio_或者lio_开头),参数包括描述符、缓冲区指针、缓冲区大小(这三个参数与read的参数一样)、文件偏移位置(与lseek一样),以及如何通知应用I/O操作完成。系统调用会马上返回结果,进程可以继续执行而不会被阻塞。

五种I/O模型的对比

下图展示了五种I/O模型的区别,其中主要的区别在于第一步接收数据的时候。到了第二步的时候,前四种I/O模型都是一样的(调用recvfrom,进程阻塞直到数据从内核缓冲区复制到应用缓冲区)。异步I/O模型从第一步到第二步都与另外四种I/O模型不一样。
Figure 6.6. Comparison of the five I/O models.

同步I/O与异步I/O对比

POSIX定义如下:

  • 同步I/O操作,会让请求的进程阻塞,直到I/O操作完成。
  • 异步I/O操作不会造成进程的阻塞。

根据这两个定义,前四种模型(阻塞I/O、非阻塞I/O、I/O多路复用和信号驱动I/O)都属于同步I/O,因为在实际I/O操作(例如recvfrom)的时候会造成进程的阻塞,只有异步I/O模型符合异步I/O的定义。