翻译《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),例如
select
和poll
- 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都是阻塞的。如下图
这个例子中,我们使用UDP而不是TCP,因为可以将“数据准备好可以被读取”简单地理解为数据是否已经接收到数据。如果是TCP,这会很复杂。
在例子中,我们继续用recvfrom
作为系统调用,因为我们要区分应用和内核。不管recvfrom
现的,调用的时候都会产生从用户态到内核态的切换,一段时间之后重新回到用户态继续执行。
在上图中,进程调用recvfrom
直到数据接收完成并且复制到应用缓冲区,系统调用的返回结果或者抛出异常。最平常的错误就是系统调用被中断信号中断。进程从调用recvfrom
这段时间都处于阻塞中,当recvfrom
用进程开始操作数据。
非阻塞I/O模型
当我们设置socket为非阻塞之后,当应用发起I/O请求的时候,如果数据还没有准备完毕,内核不会挂起进程,而是返回一个error。
最开始的三次recvfrom
系统调用没有数据返回,所以内核马上返回了EWOULDBLOCK
错误,第四次调用recvfrom
,数据已经准备好,它被复制到应用缓冲区,然后recvfrom
返回成功,之后应用可以开始处理数据。
当应用循环地对一个非阻塞的描述符调用recvfrom,被称为轮训(polling)。应用需要进行轮训,向内核请求检查一些操作是否已经完成,这会很浪费CPU时间,但是这种模型偶尔会出现在专用于一个功能的系统上。
I/O多路复用模型
应用在调用select
或poll
这两个系统调用的时候被阻塞(被select
或者poll
阻塞),而不是被实际的I/O系统调用阻塞。
上图是调用select
的例子,在调用select的时候,应用被阻塞,直到数据可读。当select
返回数据可读的时候,我们调用recvfrom
将数据复制到应用的缓冲区中。
与阻塞I/O模型对比,看起来没有任何优势,反而会有缺陷,因为使用了select
和recvfrom
,总共两次系统调用,比阻塞I/O模型多了一次。但是select
可以同时监听多个描述符等待数据。
- 另外一个与I/O多路复用模型很相似的是通过多线程使用阻塞I/O模型。这种模型不通过
select
监听多个描述符,而是使用多个线程(每个文件描述符分配一个线程)去执行I/O操作,例如recvfrom
。
信号驱动I/O模型
我们也可以使用信号,让内核在数据准备好的时候,给应用发送SIGIO
信号。这种方式称为信号驱动I/O模型,如下图所示
首先需要设置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操作完成的时候(复制到应用缓冲区),内核才会通知。如下图所示
通过系统调用aio_read
(POSIX
中定义异步I/O方法都是以aio_
或者lio_
开头),参数包括描述符、缓冲区指针、缓冲区大小(这三个参数与read
的参数一样)、文件偏移位置(与lseek
一样),以及如何通知应用I/O操作完成。系统调用会马上返回结果,进程可以继续执行而不会被阻塞。
五种I/O模型的对比
下图展示了五种I/O模型的区别,其中主要的区别在于第一步接收数据的时候。到了第二步的时候,前四种I/O模型都是一样的(调用recvfrom
,进程阻塞直到数据从内核缓冲区复制到应用缓冲区)。异步I/O模型从第一步到第二步都与另外四种I/O模型不一样。
同步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的定义。