Unix 五种IO模型

Unix IO 模型分为五种

  • 同步模型 synchronous IO
    • 阻塞 IO-bloking IO
    • 非阻塞 IO-non-blocking IO
    • 多路复用IO multiplexing IO
    • 信号驱动式IO signal-driven IO
  • 异步IO asynchronous IO
    注:由于 信号驱动IO 在实际中并不常用,所以这里只提及剩下的四种IO模型。

这里以我们平时使用最多的网络IO为例。网络IO的本质是socket的读取,socket在linux系统被抽象为流。对于一次IO访问,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:

第一阶段:等待数据准备
第二阶段:将数据从内核拷贝到进程中

阻塞IO

传统的IO模型,也是最常用最简单的一个模型。在linux中默认情况下所有的socket都是blocking的。在这个IO模型中,用户空间的应用程序执行一个系统调用(recvform)这会导致应用程序阻塞,直到数据准备好,然后将数据从内核复制到用户进程,最后进程再处理数据,在等待数据到处理数据的两个阶段,整个进程都被阻塞。不能处理别的网络IO。在调用recv()/recvfrom()函数时,发生在内核中等待数据和复制数据的过程,大致如下图:

blocking IO的特点就是在IO执行的两个阶段都被block了。

非阻塞 IO

非阻塞IO要求socket被设置为NONBLOCK,当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
所以,用户进程第一个阶段不是阻塞的,需要不断的主动询问kernel数据好了没有;第二个阶段依然总是阻塞的。这个过程通常被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。在linux下,可以通过设置socket使其变为non-blocking。
当对一个non-blocking socket执行读操作时,流程如图所示:

同步非阻塞方式相比同步阻塞方式,能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在同时执行)。但是任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。

IO多路复用

IO multiplexing 就是我们说的select,poll,epoll,也称为event driven IO。java中NIO使用的就是该模型,也就是使用的Linux的epoll库。JDK1.4之前只支持阻塞IO,在JDK1.4引入了NIO,在JDK1.7对NIO包进行了升级,支持了异步IO。现在手写NIO的比较少了,大都是直接使用netty进行开发。它们用到的就是经典的reactor模式。

由于同步非阻塞方式需要不断主动轮询,轮询占据了很大一部分过程,消耗大量的CPU时间,而 “后台” 可能有多个任务在同时进行,人们就想到了循环查询多个任务的完成状态,只要有任何一个任务完成,就去处理它。UNIX/Linux 下的 select、poll、epoll 就是干这个的(epoll 比 poll、select 效率高,做的事情是一样的)。这就是所谓的 “IO 多路复用”。

select调用是内核级别的,select轮询能实现同时对多个IO端口进行监听,当其中任何一个socket的数据准好了,就能返回进行可读,然后进程再进行recvform系统调用,将数据由内核拷贝到用户进程,当然这个过程是阻塞的。所以IO多路复用是阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的I/O系统调用如recvfrom之上。

对于多路复用其实也就是轮询多个socket。具体流程,如下图所示:

上面的图和6-1的图其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是多路复用的优势在于单个process可以同时处理多个connection。

如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

I/O多路复用的主要应用场景如下:
服务器需要同时处理多个处于监听状态或者多个连接状态的套接字。
服务器需要同时处理多种网络协议的套接字。

异步非阻塞 IO

asynchronous IO 即经典的Proactor设计模式相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。Linux提供了AIO库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如libevent、libev、libuv。异步过程如下图所示:

总结

blocking和non-blocking区别
调用 blocking IO 会一直阻塞相应的进程直到操作完成,而 non-blocking IO 在kernel还准备数据的情况下会立刻返回。
synchronous IO和asynchronous IO区别
在说明synchronous IO和asynchronous IO的区别之前,需要先给出两者的定义。POSIX的定义是这样子的:

A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;

An asynchronous I/O operation does not cause the requesting process to be blocked;

两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。
按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。
有人会说non-blocking IO并没有被block。其实定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内进程是被block的。而asynchronous IO则不一样,当进程发起IO操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。

各个IO Model的比较如图所示:

-------------本文结束-------------
坚持原创技术分享,您的支持将鼓励我继续创作!
0%