Java NIO之理解I/O模型(二)

前言

上一篇文章讲解了I/O模型的一些基本概念,包括同步与异步,阻塞与非阻塞,同步IO与异步IO,阻塞IO与非阻塞IO。此次一块儿来了解一下现有的几种IO模型,以及高效IO的两种设计模式,也都是属于IO模型的基础知识。html

UNIX下可用的五种I/O模型

根据UNIX网络编程对IO模型的分类,UNIX提供了5种IO模型,下面分别来介绍一下。linux

阻塞I/O模型

最多见的一种IO模型,以前介绍过,一个read操做是分两个阶段的,第一个阶段是,等待数据准备就绪,第二个阶段是将数据拷贝到调用这个IO的线程中。阻塞是发生在第一个阶段的,当数据没有准备好时,会一直阻塞用户线程,当数据就绪后再将数据拷贝到线程中,并返回结果给用户线程。编程

大体过程以下图。设计模式

其实,大部分的socket接口都是典型的阻塞型。所谓阻塞型的接口是指系统调用(通常是IO接口)不返回调用结果并让当前线程一直阻塞,只有当该系统调用得到结果或者超时出错时才返回。缓存

经过介绍了阻塞IO,咱们很容易就会发现它的问题,那就是阻塞会是用户线程没法进行任何运算和请求。通常咱们的处理这种问题的状况是使用多线程,每一个连接建立一个线程,或是使用线程池来管理线程,或许能够缓解部分压力,可是不能解决全部问题。多线程模型能够方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,能够用非阻塞接口来尝试解决这个问题。网络

非阻塞I/O模型

非阻塞IO模型是这样一个过程,当应用程序发起一个read操做时,并不会阻塞,而是马上会收到一个结果。应用程序的线程发现返回结果是一个error时,它就知道数据尚未准备好,因而它能够再次发送read操做。一旦数据准备好了,而且又再次收到了用户线程的请求,那么它立刻就将数据拷贝到了用户内存,而后返回。多线程

这样的一个过程,实际上是须要用户线程不断的去询问系统是否准备好了数据,这样就会一直占用CPU资源。可是这种模型是在只专门提供某种功能的系统才有。异步

大体过程以下:
socket

多路I/O复用模型

在介绍多路复用I/O时就要先简单说明一下,select函数和poll函数。函数

select函数

select函数容许进程指示内核等待多个事件中的任何一个事件发生,而且只在有一个或多个事件发生或经历一段指定的时间后才唤醒它。

  • 举个例子,咱们能够调用select,告知内核仅在下列状况发生时才返回:
  • 集合 {1,4,5} 中的任何描述符准备好读;
  • 集合 {2,7} 中的任何描述符准备好写;
  • 集合 {1,4} 中的任何描述符有异常条件待处理;
  • 已经经历10.2秒;

也就是说,咱们调用select告知内核对哪些描述符(读、写或异常条件)感兴趣以及等待多长时间。

poll函数

poll函数起源于SVR3,最初局限于流设备。SVR4取消了这种限制,容许poll工做在任何描述符上。poll函数提供的功能与select函数相似,可是poll没有最大文件描述符数量的限制。

select函数和poll函数将就绪的文件描述符告诉进程后,若是进程没有对其进行IO操做,那么下次调用select函数或者poll函数时会再次报告这些文件描述符, 因此他们通常不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。

简单的解了select函数和poll函数后,下面咱们就继续说多路I/O复用模型。多路IO复用模型就是调用select或poll函数,而且此模型的阻塞过程就是发生在调用这两个函数中的,而不是发生在真正的的I/O系统调用上的,使用select或poll的好处在于能够用单个线程或进程,处理多个网络链接的IO。整个过程就是select或poll函数会不断的轮询所负责的socket,当某个socket有数据到达了,就通知用户线程或进程。

大概调用以下:

Java中的NIO实际上就是使用的多路IO复用模型,经过selector.select()去查询每一个通道是否有到达事件,若是没有事件,则一直阻塞在那里,所以多路复用IO模型也会阻塞用户线程,只不过线程是被select函数阻塞的而不是被scoket IO阻塞的。

因此多路复用IO模型和非阻塞IO有相似之处,可是多路复用IO模型的效率是比非阻塞IO模型要高的,由于在非阻塞IO中,不断的询问scoket状态的是经过用户线程去进行的,而多路复用IO模型,轮询每一个scoket状态是内核在进行的,这个效率是比用户线程要高不少的。这样也能看出来多路复用IO模型比较适合连接数比较多的状况。

不过此模型也是存在问题的,因为多路复用IO模型是经过轮询的方式来检测是否有事件到达,并对到达的事件逐一响应,一旦事件响应体很大或是响应事件数量过多,就会消耗大量的时间去处理事件,从而影响整个过程的及时性。为了应对这种状况linux系统提供了epoll接口,可是除了linux的其余操做系统对epoll接口的支持又有不少差别,因此虽然epoll解决了事件检测的时效性问题,可是在跨平台能力上却并不能获得很好的支持。

信号驱动IO模型

在信号驱动IO模型中,让内核在数据报准备就绪时发送SIGIO信号通知用户线程。

整个过程以下:

首先开启套接字的信号驱动式IO功能,并经过sigaction系统调用安装一个信号处理函数。该系统调用将当即返回,进程继续工做,也就是说没有被阻塞。当数据报准备好读取时,内核就为该进程产生一个SIGIO信号。咱们随后就能够在信号处理函数中调用recvfrom读取数据报,并通知用户进程数据已经准备好了,能够读取了。

这种模型的优势在于等待数据报到达期间不会被阻塞,用户进程能够继续执行,只要等待来自信号处理函数的通知便可。

异步IO模型

异步IO模型的过程是这样的,当用户线程发起read操做时,告知内核启动读取数据操做,并让内核在整个操做(包括将数据从内核复制到咱们本身的缓冲区)完成后通知咱们。这样在内核执行读取数据操做时,用户线程能够继续执行,当接收到内核在整个操做都完成的信号时,就能够直接去使用数据了。

大体过程以下:

在异步IO模型中,IO操做的两个阶段都不会阻塞用户线程或进程,这两个阶段都是由内核完成的,而后发送一个信号告知用户线程或进程操做已完成。异步IO模型与信号驱动IO模型的区别在于,信号驱动IO模型是由内核通知用户线程什么时候启动一个IO操做,而异步IO模型是由内核通知咱们IO操做什么时候完成,异步IO模型中用户线程并不须要进行实际的读写操做,只须要在内核操做完成后,接到读取完成信号后,直接使用数据便可。

异步IO是须要操做系统底层支持的,Linux从内核2.6版本才开始支持异步IO。在Java 7中就已经支持异步IO了。

两种高性能IO设计模式Reactor和Proactor

Reactor模式

Reactor的意思是反应器,字面意思就是当即反应。

Reactor的工做方式:

(1)应用程序注册读就绪事件和相关联的事件处理器

(2)Reactor阻塞等待内核事件通知

(3)Reactor收到通知,而后分发可读写事件(读写准备就绪)到用户事件处理函数

(4)用户读取数据,并处理数据

(5)事件处理器完成实际的读操做,处理读到的数据,注册新的事件,而后返还控制权。

大体过程是,每一个应用程序宣布它对某个socket感兴趣,而后就须要到Reactor中注册感兴趣事件以及相关的处理函数。当socket发现有事件到达时,就会按顺序对每一个事件进行处理(调用处理函数),当全部事件处理完成后,会继续循环这整个操做。

过程以下图所示:

从这个设计模式的处理过程当中能够看出,多路IO复用模型就是使用的 Reactor模式,而且这种设计模式仍是体现的同步IO。

Proactor模式

Proactor的意思是主动器,主动去完成相应的工做不影响主流程。

Proactor模式的工做方式:

(1)应用程序初始化一个异步读取操做,而后注册相应的事件处理器,此时事件处理器不关注读取就绪事件,而是关注读取完成事件,这是区别于Reactor的关键。

(2)事件分离器等待读取操做完成事件

(3)在事件分离器等待读取操做完成的时候,操做系统调用内核线程完成读取操做,并将读取的内容放入用户传递过来的缓存区中。这也是区别于Reactor的一点,Proactor中,应用程序须要传递缓存区。

(4)事件分离器捕获到读取完成事件后,激活应用程序注册的事件处理器,事件处理器直接从缓存区读取数据,而不须要进行实际的读取操做。

异步IO模型就是使用的Proactor模式。





参考资料:

《Unix网络编程》

www.cnblogs.com/dolphin0520…

www.cnblogs.com/findumars/p…

ifeve.com/io%E6%A8%A1…

相关文章
相关标签/搜索