深入了解UNIX的IO模型和NIO的线程模型演变reactor和proactor线程模型

通常io操作包括对磁盘、网络socket和外连设备的读写,本文大致先呈现io操作中阻塞IO、非阻塞IO、同步IO和异步IO的概念,然后呈现IO的网络模型,最后讲解两种常见的NIO两种线程模型。

IO操作分为两个阶段第一个阶段是IO请求后数据准备阶段,第二阶段是内核空间把数据响应到用户进程

阻塞IO、非阻塞IO

阻塞和非阻塞发生在IO操作的第一阶段,IO请求数据准备阶段。

阻塞:用户线程发起io操作的请求后,在数据准备阶段,用户线程必阻塞等待,直到io请求返回结果

非阻塞:用户线程发起io请求后,会立即返回一个IO请求是否完成的标志,用户线程不会一直等待

同步IO、异步IO

同步异步发生在IO操作的第二个阶段,IO结果数据内核响应到用户线程

同步:IO请求响应的数据从内核发送到用户进程后,需要用户线程主动去询问和获取IO结果数据

异步:IO请求响应结果数据从内核发送到用户空间,内核会中回调通知用户线程

五种IO模型

阻塞IO模型

传统的IO模型,用户线程发起IO请求后,在内核把数据准备完成并拷贝到用户线程之前,用户线程都是出于阻塞状态,知道数据发送到用户线程后线程才会解除阻塞,网络IO的socket就是用的阻塞IO模型。

非阻塞IO模型

非阻塞IO模型,用户线程发起IO请求后,会立马返回用户线程,不关乎IO处理结果数据是否被拷贝到用户线程,但用户线程需要时不时的去询问内核是否处理完成,这中线程模型实际上没有提高IO效率,反而需要不停的去询问内核IO处理结果,不停的询问反而是要占用CPU。

多路复用IO模型

多路复用IO模型是目前使用得比较多的模型。Java NIO实际上就是多路复用IO,多路复用的核心组件就是多路复用器selector,所有建立的IO连接都会注册在多路复用器上,系统提供的多路复用器有select、poll和epoll三种,,其中epoll是UNIX系统特别持有的,一个线程可以维护多个甚至无数个连接,只有IO的读写事件,复用器才会收到事件并执行IO操作。

三种IO复用器:

select复用器的连接数受限制,因为select是线性轮训连接是否有IO事件,如果连接数过多会降低IO效率。

poll复用器同select一样UNIX和windows都支持,区别是poll的管理连接的数据结构链表故无连接数限制

epoll是UNIX系统所持有的,区别在于不需要去轮训连接是否有IO事件发生,而是当有连接发生时内核会通过回调用户线程

 

信号驱动IO模型

在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。

异步IO模型

异步IO模型才是最理想的IO模型,在异步IO模型中,当用户线程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个asynchronous read之后,它会立刻返回,说明read请求已经成功发起了,因此不会对用户线程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程。

reactor和proactor模型来源:https://blog.csdn.net/u013074465/article/details/46276967

Reactor模型

Reactor模式是处理并发I/O比较常见的一种模式,用于同步I/O,中心思想是将所有要处理的I/O事件注册到一个中心I/O多路复用器上,同时主线程/进程阻塞在多路复用器上;一旦有I/O事件到来或是准备就绪(文件描述符或socket可读、写),多路复用器返回并将事先注册的相应I/O事件分发到对应的处理器中。
  Reactor是一种事件驱动机制,和普通函数调用的不同之处在于:应用程序不是主动的调用某个API完成处理,而是恰恰相反,Reactor逆置了事件处理流程,应用程序需要提供相应的接口并注册到Reactor上,如果相应的事件发生,Reactor将主动调用应用程序注册的接口,这些接口又称为“回调函数”。

Reactor模式是已经制定好的IO事件处理流程,是事件驱动的,有一个或多个并发输入源,有一个Service Handler,有多个Request Handlers;这个Service Handler会同步的将输入的请求(Event)多路复用的分发给相应的Request Handler。

深入了解UNIX的IO模型和NIO的线程模型演变reactor和proactor线程模型

Reactor单线程模型

只有一个 Reactor 线程,即只有一个 Selector 事件通知器,也就是说,字节的读取 I/O 和后续的业务处理(process() 方法),均由 Reactor 线程来做,很显然业务的处理影响后续事件的分发,所以引出多线程版本进行优化。

深入了解UNIX的IO模型和NIO的线程模型演变reactor和proactor线程模型

Reactor多线程模型(单个Reactor)

一个 Reactor 线程和多个处理线程,将业务处理(process 交给线程池)进行了分离,Reactor 线程,只关注事件分发和字节的发送和读取(I/O)。注意,实际的发送和读取还是由 Reactor 处理,那么在高并发下,有可能连接来不及接收,继续优化,采用主从 Reactor。

深入了解UNIX的IO模型和NIO的线程模型演变reactor和proactor线程模型

Reactor主从Reactor模型

将 Reactor 分成两部分,mainReactor 负责监听并 Accept新连接,然后将建立的 socket 通过多路复用器(Acceptor)分派给subReactor。subReactor 负责多路分离已连接的 socket,读写网络数据;业务处理功能,其交给 worker 线程池完成。通常,subReactor 个数上可与 CPU 个数等同
深入了解UNIX的IO模型和NIO的线程模型演变reactor和proactor线程模型

Proacotr模型
       Proactor是和异步I/O相关的,在Reactor模式中,事件分离者等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分离器就把这个事件传给事先注册的处理器(事件处理函数或者回调函数),由后者来做实际的读写操作。
       在Proactor模式中,事件处理者(或者代由事件分离者发起)直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。发起时,需要提供的参数包括用于存放读到数据的缓存区,读的数据大小,或者用于存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分离者得知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事件处理者或者回调。

       可以看出两者的区别:Reactor是在事件发生时就通知事先注册的事件(读写由处理函数完成);Proactor是在事件发生时进行异步I/O(读写由OS完成),待IO完成事件分离器才调度处理器来处理。

 

 举个例子,将有助于理解Reactor与Proactor二者的差异,以读操作为例(类操作类似):


在Reactor(同步)中实现读:
 - 注册读就绪事件和相应的事件处理器
 - 事件分离器等待事件
 - 事件到来,**分离器,分离器调用事件对应的处理器。
 - 事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。
Proactor(异步)中的读:
 - 处理器发起异步读操作(注意:操作系统必须支持异步IO)。在这种情况下,处理器无视IO就绪事件,它关注的是完成事件。
 - 事件分离器等待操作完成事件
 - 在分离器等待过程中,操作系统利用并行的内核线程执行实际的读操作,并将结果数据存入用户自定义缓冲区,最后通知事件分离器读操作完成。
 - 事件分离器呼唤处理器。
 - 事件处理器处理用户自定义缓冲区中的数据,然后启动一个新的异步操作,并将控制权返回事件分离器。