@@ -211,3 +211,103 @@ IBM资深工程师 Rusty Russell 在开发Lguest(Linux 内核中的的一个hy
211211
212212 Rusty Russell工程师在2008年在“ACM SIGOPS Operating Systems Review”期刊上发表了一篇论文“virtio: towards a de-facto standard for virtual I/O devices”,提出了给虚拟环境(Virtual Machine)中的操作系统提供一套统一的设备抽象,这样操作系统针对每类设备只需写一种驱动程序就可以了,这极大降低了系统虚拟机(Virtual Machine Monitor)和Hypervisor,以及运行在它们提供的虚拟环境中的操作系统的开发成本,且可以显著提高I/O的执行效率。目前virtio已经有相应的规范,最新的virtio spec版本是v1.1。
213213
214+
215+ I/O执行模型
216+ --------------------
217+
218+ 从用户进程的角度看,用户进程是通过I/O相关的系统调用(简称I/O系统调用)来进行I/O操作的。在UNIX环境中,I/O系统调用有多种不同类型的执行模型。根据Richard Stevens的经典书籍“UNIX Network Programming Volume 1: The Sockets Networking ”的6.2节“I/O Models ”的介绍,大致可以分为五种I/O执行模型(I/O Execution Model,简称IO Model, IO模型):
219+
220+ - blocking IO
221+ - nonblocking IO
222+ - IO multiplexing
223+ - signal driven IO
224+ - asynchronous IO
225+
226+ 当一个用户进程发出一个 ``read `` I/O系统调用时,主要经历两个阶段:
227+
228+ 1. 等待数据准备好 (Waiting for the data to be ready)
229+ 2. 把数据从内核拷贝到用户进程中(Copying the data from the kernel to the process)
230+
231+ 上述五种IO模型在这两个阶段有不同的处理方式。需要注意,阻塞与非阻塞关注的是进程的执行状态:
232+
233+ - 阻塞:进程执行系统调用后会被阻塞
234+ - 非阻塞:进程执行系统调用后不会被阻塞
235+
236+ 同步和异步关注的是消息通信机制:
237+
238+ - 同步:用户进程与操作系统(设备驱动)之间的操作是经过双方协调的,步调一致的
239+ - 异步:用户进程与操作系统(设备驱动)之间并不需要协调,都可以随意进行各自的操作
240+
241+ 阻塞IO(blocking IO)
242+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
243+
244+ 基于阻塞IO模型的文件读系统调用 -- ``read `` 的执行过程是:
245+
246+ 1. 用户进程发出 ``read `` 系统调用;
247+ 2. 内核发现所需数据没在I/O缓冲区中,需要向磁盘驱动程序发出I/O操作,并让用户进程处于阻塞状态;
248+ 3. 磁盘驱动程序把数据从磁盘传到I/O缓冲区后,通知内核(一般通过中断机制),内核会把数据从I/O缓冲区拷贝到用户进程的buffer中,并唤醒用户进程(即用户进程处于就绪态);
249+ 4. 内核从内核态返回到用户态的用户态进程,此时 ``read `` 系统调用完成。
250+
251+
252+ 所以阻塞IO(blocking IO)的特点就是用户进程在I/O执行的两个阶段(等待数据和拷贝数据两个阶段)都是阻塞的。
253+
254+ 当然,如果正好用户进程所需数据位于内存中,那么内核会把数据从I/O缓冲区拷贝到用户进程的buffer中,并从内核态返回到用户态的用户态进程, ``read `` 系统调用完成。这个由于I/O缓冲带了的优化结果不会让用户进程处于阻塞状态。
255+
256+
257+ 非阻塞IO(non-blocking IO)
258+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
259+
260+ 基于非阻塞IO模型的文件读系统调用 -- ``read `` 的执行过程是:
261+
262+ 1. 用户进程发出 ``read `` 系统调用;
263+ 2. 内核发现所需数据没在I/O缓冲区中,需要向磁盘驱动程序发出I/O操作,并不会让用户进程处于阻塞状态,而是立刻返回一个error;
264+ 3. 用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作(这一步操作可以重复多次);
265+ 4. 磁盘驱动程序把数据从磁盘传到I/O缓冲区后,通知内核(一般通过中断机制),内核在收到通知且再次收到了用户进程的system call后,会马上把数据从I/O缓冲区拷贝到用户进程的buffer中;
266+ 5. 内核从内核态返回到用户态的用户态进程,此时 ``read `` 系统调用完成。
267+
268+ 所以,在非阻塞式IO的特点是用户进程不会被内核阻塞,而是需要不断的主动询问内核所需数据准备好了没有。非阻塞系统调用相比于阻塞系统调用的的差异在于在被调用之后会立即返回。
269+
270+ 使用系统调用 ``fcntl( fd, F_SETFL, O_NONBLOCK ) `` 可以将对某文件句柄 ``fd `` 进行的读写访问设为非阻塞IO模型的读写访问。
271+
272+
273+ 多路复用IO(IO multiplexing)
274+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
275+
276+ IO multiplexing对应的I/O系统调用是 ``select `` 和 ``epoll `` 等,也称这种IO方式为事件驱动IO(event driven IO)。 ``select `` 和 ``epoll `` 的优势在于,采用单进程方式就可以同时处理多个文件或网络连接的I/O操作。其基本工作机制就是通过 ``select `` 或 ``epoll `` 系统调用来不断的轮询用户进程关注的所有文件句柄或socket,当某个文件句柄或socket有数据到达了,``select `` 或 ``epoll `` 系统调用就会返回到用户进程,用户进程再调用 ``read `` 系统调用,让内核将数据从内核的I/O缓冲区拷贝到用户进程的buffer中。
277+
278+ 在多路复用IO模型中,对于用户进程关注的每一个文件句柄或socket,一般都设置成为non-blocking,只是用户进程是被``select`` 或 ``epoll `` 系统调用阻塞住了。``select/epoll `` 的优势并不是对于单个文件或socket的I/O访问性能更好,而是在于有很多个文件或socket的I/O访问下,其总体效率会高。
279+
280+ 信号驱动IO(signal driven I/O)
281+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
282+
283+ 当进程发出一个 ``read `` 系统调用时,会向内核注册一个信号处理函数,然后系统调用返回,进程不会被阻塞,而是继续执行。当内核中的IO数据就绪时,会发送一个信号给进程,进程便在信号处理函数中调用IO读取数据。这是模型的特点是,采用了回调机制,这样开发和调试应用的难度加大。
284+
285+ 异步IO(Asynchronous I/O)
286+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
287+
288+ 用户进程发起 ``read `` 异步系统调用之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度看,当它收到一个 ``read `` 异步系统调用之后,首先它会立刻返回,所以不会对用户进程产生任何阻塞情况。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会通知用户进程,告诉它read操作完成了。
289+
290+ .. note ::
291+
292+ **Linux异步IO的历史 **
293+
294+ 2003年,Suparna Bhattacharya提出了AIO在Linux kernel的设计方案,里面谈到了用Full async state machine模型来避免阻塞,把一系列的阻塞点用状态机来驱动,把用户态的buffer映射到内核来驱动,这个模型被应用到Linux kernel 2.4中。在出现io_uring 之前,虽然还出现了一系列的异步IO的探索(syslet、LCA、FSAIO、AIO-epoll等),但性能一般,实现和使用复杂,应该说Linux没有提供完善的异步IO(网络IO、磁盘IO)机制。io_uring 是由 Jens Axboe提供的异步 I/O 接口,io_uring围绕高效进行设计,采用一对共享内存ringbuffer用于应用和内核间通信,避免内存拷贝和系统调用。io_uring的实现于 2019 年 5 月合并到了 Linux kernel 5.1 中,现在已经在多个项目中被使用。
295+
296+
297+ 五种IO执行模型对比
298+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
299+
300+ 这里总结一下阻塞IO、非阻塞IO、同步IO、异步IO的特点:
301+
302+ - 阻塞IO:在用户进程发出IO系统调用后,进程会等待该IO操作完成,而使得进程的其他操作无法执行。
303+ - 非阻塞IO:在用户进程发出IO系统调用后,如果数据没准备好,该IO操作会立即返回,之后进程可以进行其他操作;如果数据准备好了,用户进程会通过系统调用完成数据拷贝并接着进行数据处理。
304+ - 同步IO:导致请求进程阻塞/等待,直到I/O操作完成。
305+ - 异步IO:不会导致请求进程阻塞。
306+
307+ 从上述分析可以得知,阻塞和非阻塞的区别在于内核数据还没准备好时,用户进程是否会阻塞(一阶段是否阻塞);同步与异步的区别在于当数据从内核copy到用户空间时,用户进程是否会阻塞/参与(二阶段是否阻塞)。
308+
309+ 所以前述的阻塞IO(blocking IO),非阻塞IO(non-blocking IO),多路复用IO(IO multiplexing),信号驱动IO都属于同步IO(synchronous IO)。这四种模型都有一个共同点:在二阶段阻塞/参与,也就是在真正IO操作 ``read `` 的时候需要用户进程参与,因此以上四种模型均称为同步IO模型。
310+
311+ 有人可能会说,执行非阻塞IO系统调用的用户进程并没有被阻塞。其实这里定义中所指的 **IO操作 ** 是指实际的 **IO操作 ** 。比如,非阻塞IO在执行 ``read `` 系统调用的时候,如果内核中的IO数据没有准备好,这时候不会block进程。但是当内核中的IO数据准备好且收到用户进程发出的 ``read `` 系统调用时(处于第二阶段), 内核中的 ``read `` 系统调用的实现会将数据从kernel拷贝到用户内存中,这个时候进程是可以被阻塞的。
312+
313+ 而异步IO则不一样,当用户进程发起IO操作之后,就直接返回做其它事情去了,直到内核发送一个通知,告诉用户进程说IO完成。在这整个过程中,用户进程完全没有被阻塞。
0 commit comments