通常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。
Reactor单线程模型
只有一个 Reactor 线程,即只有一个 Selector 事件通知器,也就是说,字节的读取 I/O 和后续的业务处理(process() 方法),均由 Reactor 线程来做,很显然业务的处理影响后续事件的分发,所以引出多线程版本进行优化。
Reactor多线程模型(单个Reactor)
一个 Reactor 线程和多个处理线程,将业务处理(process 交给线程池)进行了分离,Reactor 线程,只关注事件分发和字节的发送和读取(I/O)。注意,实际的发送和读取还是由 Reactor 处理,那么在高并发下,有可能连接来不及接收,继续优化,采用主从 Reactor。
Reactor主从Reactor模型
将 Reactor 分成两部分,mainReactor 负责监听并 Accept新连接,然后将建立的 socket 通过多路复用器(Acceptor)分派给subReactor。subReactor 负责多路分离已连接的 socket,读写网络数据;业务处理功能,其交给 worker 线程池完成。通常,subReactor 个数上可与 CPU 个数等同
Proacotr模型
Proactor是和异步I/O相关的,在Reactor模式中,事件分离者等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分离器就把这个事件传给事先注册的处理器(事件处理函数或者回调函数),由后者来做实际的读写操作。
在Proactor模式中,事件处理者(或者代由事件分离者发起)直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。发起时,需要提供的参数包括用于存放读到数据的缓存区,读的数据大小,或者用于存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分离者得知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事件处理者或者回调。
可以看出两者的区别:Reactor是在事件发生时就通知事先注册的事件(读写由处理函数完成);Proactor是在事件发生时进行异步I/O(读写由OS完成),待IO完成事件分离器才调度处理器来处理。
举个例子,将有助于理解Reactor与Proactor二者的差异,以读操作为例(类操作类似):
在Reactor(同步)中实现读:
- 注册读就绪事件和相应的事件处理器
- 事件分离器等待事件
- 事件到来,激活分离器,分离器调用事件对应的处理器。
- 事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。
Proactor(异步)中的读:
- 处理器发起异步读操作(注意:操作系统必须支持异步IO)。在这种情况下,处理器无视IO就绪事件,它关注的是完成事件。
- 事件分离器等待操作完成事件
- 在分离器等待过程中,操作系统利用并行的内核线程执行实际的读操作,并将结果数据存入用户自定义缓冲区,最后通知事件分离器读操作完成。
- 事件分离器呼唤处理器。
- 事件处理器处理用户自定义缓冲区中的数据,然后启动一个新的异步操作,并将控制权返回事件分离器。