【转载】高性能IO设计 & Java NIO & 同步/异步 阻塞/非阻塞 Reactor/Proactor

时间:2022-09-09 20:56:26

开始准备看Java NIO的,这篇文章:http://xly1981.iteye.com/blog/1735862

里面提到了这篇文章 http://xmuzyq.iteye.com/blog/783218 同步、异步、阻塞、非阻塞、reactive、proactive等讲的不错。

在高性能的I/O设计中,有两个比较著名的模式Reactor和Proactor模式,
其中Reactor模式用于同步I
/O,
而Proactor运用于异步I
/O操作。

什么是同步和异步

同步和异步是针对应用程序和内核的交互而言的,
同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪,
而异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知。

什么是阻塞和非阻塞

阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,
说白了是一种读取或者写入操作函数的实现方式,
阻塞方式下读取或者写入函数将一直等待,
而非阻塞方式下,读取或者写入函数会立即返回一个状态值。

组合一下,就会发现一般来说I/O模型可以分为:同步阻塞,同步非阻塞,异步阻塞,异步非阻塞IO

同步阻塞IO:
在此种方式下,用户进程在发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才能运行。
JAVA传统的IO模型属于此种方式!
同步非阻塞IO: 在此种方式下,用户进程发起一个IO操作以后边可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪
这就要求用户进程不停的去询问,从而引入不必要的CPU资源浪费。其中目前JAVA的NIO就属于同步非阻塞IO
异步阻塞IO:
此种方式下是指应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序
这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问IO是否完成,而异步接受通知。
那么为什么说是阻塞的呢
因为此时是通过select系统调用来完成的,而select函数本身的实现方式是阻塞的
而采用select函数有个好处就是它可以同时监听多个文件句柄,从而提高系统的并发性!
(注:同步异步区别不在与IO由操作系统还是应用程序完成,而是在于主动询问还是等待通知;reactor和proactor区别,在于IO操作由哪边完成,下面有提到)
异步非阻塞IO:
在此种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知
此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了
目前Java中还没有支持此种IO模型。

 

搞清楚了以上概念以后,我们再回过头来看看,Reactor模式和Proactor模式(貌似都是非阻塞)

首先来看看Reactor模式,Reactor模式应用于同步I/O的场景

我们分别以读操作和写操作为例来看看Reactor中的具体步骤:
读取操作:
1. 应用程序注册读就需事件和相关联的事件处理器
2. 事件分离器等待事件的发生
3. 当发生读就需事件的时候,事件分离器调用第一步注册的事件处理器
4. 事件处理器首先执行实际的读取操作,然后根据读取到的内容进行进一步的处理
写入操作类似于读取操作,只不过第一步注册的是写就绪事件

再看Proactor模式,Proactor主要应用于异步场景

下面我们来看看Proactor模式中读取操作和写入操作的过程:
读取操作:
1. 应用程序初始化一个异步读取操作,然后注册相应的事件处理器,此时事件处理器不关注读取就绪事件,而是关注读取完成事件,这是区别于Reactor的关键
2. 事件分离器等待读取操作完成事件
3. 在事件分离器等待读取操作完成的时候,操作系统调用内核线程完成读取操作,并将读取的内容放入用户传递过来的缓存区中
这也是区别于Reactor的一点,Proactor中,应用程序需要传递缓存区
4. 事件分离器捕获到读取完成事件后,激活应用程序注册的事件处理器,事件处理器直接从缓存区读取数据而不需要进行实际的读取操作
Proactor中写入操作和读取操作,只不过感兴趣的事件是写入完成事件

从上面可以看出,Reactor和Proactor模式的主要区别就是真正的读取和写入操作是有谁来完成的

Reactor中需要应用程序自己读取或者写入数据

而Proactor模式中,应用程序不需要进行实际的读写过程,它只需要从缓存区读取或者写入即可,操作系统会读取缓存区或者写入缓存区到真正的IO设备

 

综上所述,同步和异步是相对于应用和内核的交互方式而言的同步需要主动去询问,而异步的时候内核在IO事件发生的时候通知应用程序;

阻塞和非阻塞仅仅是系统在调用系统调用的时候函数的实现方式而已。

ReactorProactor这两种常见的网络编程模式,区别在于IO操作是由操作系统还是应用程序来完成

 

还有这篇文章: http://blog.csdn.net/shallwake/article/details/5265287 增加理解,图不错。

首先,介绍几种常见的I/O模型及其区别,如下:

  • blocking I/O

  • nonblocking I/O

  • I/O multiplexing (select and poll)

  • signal driven I/O (SIGIO)

  • asynchronous I/O (the POSIX aio_functions)

 

blocking I/O 

【转载】高性能IO设计 & Java NIO & 同步/异步 阻塞/非阻塞 Reactor/Proactor

首先application调用 recvfrom()转入kernel,注意kernel有2个过程wait for datacopy data from kernel to user。直到最后copy complete后,recvfrom()才返回。此过程一直是阻塞的。

 

nonblocking I/O: 

与blocking I/O对立的,非阻塞套接字,调用过程图如下:

【转载】高性能IO设计 & Java NIO & 同步/异步 阻塞/非阻塞 Reactor/Proactor

如果直接操作它,那就是个轮询。。直到内核缓冲区有数据,然后会阻塞进行拷贝到用户缓冲区,然后返回。

 

I/O multiplexing (select and poll) 

看下select的过程:

 

【转载】高性能IO设计 & Java NIO & 同步/异步 阻塞/非阻塞 Reactor/Proactor

select先阻塞,有活动套接字才返回,然后用recvfrom去获取数据。与blocking I/O相比,select会有两次系统调用,但是select能处理多个套接字

 

signal driven I/O (SIGIO) 

只有Unix系统支持。

【转载】高性能IO设计 & Java NIO & 同步/异步 阻塞/非阻塞 Reactor/Proactor

 

I/O multiplexing (select and poll)相比,它的优势是免去了select的阻塞与轮询,当有活跃套接字时,由注册的handler处理。 

 

asynchronous I/O (the POSIX aio_functions) 

很少有*nix系统支持,windows的IOCP则是此模型

【转载】高性能IO设计 & Java NIO & 同步/异步 阻塞/非阻塞 Reactor/Proactor

完全异步的I/O复用机制,因为纵观上面其它四种模型,至少都会在由kernel copy data to appliction时阻塞。而该模型是当copy完成后才通知application,可见是纯异步的。好像只有windows的完成端口是这个模型,效率也很出色。

 

下面是以上五种模型的比较

【转载】高性能IO设计 & Java NIO & 同步/异步 阻塞/非阻塞 Reactor/Proactor

可以看出,越往后,阻塞越少,理论上效率也是最优。

 

5种模型的比较比较清晰了,剩下的就是把select,epoll,iocp,kqueue按号入座那就OK了。

selectiocp分别对应第3种第5种模型,那么epoll与kqueue呢? 其实也于select属于同一种模型,只是更高级一些,可以看作有了第4种模型的某些特性,如callback机制。

那么,为什么epoll,kqueue比select高级? 

答案是,他们无轮询。因为他们用callback取代了。想想看,当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

 

windows or *nix (IOCP or kqueue/epoll)?

Windows的IOCP非常出色,目前很少有支持asynchronous I/O的系统,但是由于其系统本身的局限性,大型服务器还是在UNIX下。

而且正如上面所述,kqueue/epoll 与 IOCP相比,就是多了一层从内核copy数据到应用层的阻塞,从而不能算作asynchronous I/O类。但是,这层小小的阻塞无足轻重,kqueue与epoll已经做得很优秀了。

 

提供一致的接口,IO Design Patterns

不管是哪种模型,都可以抽象一层出来,提供一致的接口,广为人知的有ACE,Libevent这些,他们都是跨平台的,而且他们自动选择最优的I/O复用机制,用户只需调用接口即可。

说到这里又得说说2个设计模式,Reactor and Proactor。有一篇经典文章 http://www.artima.com/articles/io_design_patterns.html 值得阅读,Libevent是Reactor模型ACE提供Proactor模型。实际都是对各种I/O复用机制的封装

 

Java nio包是什么I/O机制?

文中说NIO本质是select()模型。上一篇文章说NIO是同步非阻塞(轮询),而select是异步阻塞,可能是因为分类方法不同吧。

 

总结一些重点:

  1. 只有IOCP是asynchronous I/O,其他机制或多或少都会有一点阻塞。
  2. select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善
  3. epoll, kqueue是Reacor模式,IOCP是Proactor模式。
  4. java nio包是select模型。。

以上出处:link

 

下面这篇文章,是上面两篇的补充 

http://www.smithfox.com/?e=191

 

首先这篇文章,说异步没有阻塞非阻塞区别。异步就是异步! 只有同步时才有阻塞和非阻塞之分。

 

说 阻塞和 非阻塞 时, 要区分场合范围, 比如 Linux中说的 非阻塞I/O 和 Java的NIO1.0中的 非阻塞I/O 不是相同的概念. 

从最根本来说, 阻塞就是进程 "被" 休息, CPU处理其它进程去了. 非阻塞可以理解成: 将大的整片时间的阻塞分成N多的小的阻塞, 所以进程不断地有机会 "被" CPU光顾, 理论上可以做点其它事. 看上去 Linux非阻塞I/O 要比阻塞好, 但CPU会很大机率因socket没数据而空转. 虽然这个进程是爽了, 但是从整个机器的效率来说, 浪费更大了!  Java NIO1.0中的非阻塞I/O中的 Selector.select()函数还是阻塞的, 所以不会有无谓的CPU浪费.

Java NIO1.0, 与其说是非阻塞I/O, 还不如说是, 多路复用I/O, 更好让人理解!

 

异步

异步可以说是I/O最理想的模型: CPU的原则是, 有必要的时候才会参与, 既不浪费, 也不怠慢。

理想中的异步I/O: Application无需等待socket数据(也正是因此进程而被 "休息"), 也无需 copy socket data, 将由其它的同学(理想状态, 不是CPU) 负责将socket data copy到Appliation事先指定的内存后, 通知一声Appliation(一般是回调函数).

传统的阻塞socket有什么问题?

最传统的阻塞socket, 为了不致使处理一个client的请求时, 让其它的client一直等, 一般会一个client连接, 就会起一个Thread.

实际情况是, 有的业务, 只是client连接多, 但每个client连接上的通讯并不是非常频繁, 就算是很频繁, 也会因网络延迟, 
而使得大部分时间内,Thread们都在被"休息"(因为等待scoket上数据), 因为相对cpu的运算速度, 网络延迟产生的间歇时间相当多.

这就造成了: 虽然Thread多, 并不能处理太多的socket请求,
要知道在JVM中每个Thread都会单独分配栈的(JVM默认好象是1M, 可以通过 -Xss来调整),
而且需CPU要不断地在很多线程之间switch, 保存/恢复 Thread Context 代价非常大!

多路复用

为了解决阻塞I/O的问题, 就有了 I/O多路复用 模型, 多路复用就是用单独的线程(是内核级的, 可以认为是高效的优化的) 来统一等待所有的socket上的数据, 一当某个socket上有数据后, 就启用用户线程(可能是从线程池中取出, 而不是重新生成), copy socket data, 并且处理message.  

因为网络延迟的原因, 同时在处理socket data的用户线程往往比实际的socket数量要少很多. 所以实际应用中, 大部分是用线程池, 池中thread数量可随socket的高峰和低谷 而动态调整.

上面说的 多路复用I/O, 很多文章称之为   同步非阻塞(第一篇文章认为是异步阻塞). 个人认为, 不要老揪着那四个词不放! 多累呀!

多路复用, 既可以理解成 "非阻塞", 也可以理解成 "阻塞"

多路复用I/O 中内核中统一的 wait socket data那部分可以理解成 是 "非阻塞", 也可以理解成"阻塞".

可以理解成"非阻塞" 是因为它不是等到socket数据全部到达再处理, 而是有了一部分数据就会调用用户线程来处理,

理解成"阻塞", 是因为它和用户空间(Appliction)层的"非阻塞"socket的不同是: socket中没有数据时, 内核还是wait(阻塞)的,

而用户空间的非阻塞socket没有数据也会返回, 会造成CPU的浪费(上面已经解释过了).

select 和 poll

Linux下的 select和poll 就是 多路复用模式, poll 相对 select, 没有了句柄数的限制, 但他们都是在内核层通过轮询socket句柄的方式来实现的, 没有利用更底层的 notify 机制.  但就算是这样,相对阻塞socket 也已经进步了很多很多了! 毕竟用一个内核线程就解决了阻塞socket中N多线程都在无谓地wait的局面.

多路复用I/O 还是让用户层来copy socket data. 这个过程是将内核中的socket buffer copy 到用户空间的 buffer. 这有两个问题: 一是多了一次内核空间switch到用户空间的过程, 二是用户空间层不便暴露很低层但很高效的copy 方式(比如DMA), 所以如果由内核层来做这个动作, 可以更好地提高效率! DMA是指外部设备不通过CPU而直接与系统内存交换数据的接口技术。

epoll, Linux的AIO

于是, 在Linux2.6 epoll出现了, epoll是Linux下 AIO(异步IO)的实现方式, 实际上在epoll成为最终方案之前, 也有其它的方案, 而且在其它的操作系统中都有着不同的AIO实现.

epoll 已经采用了更为底层的 notify 机制, 而不是肓目地轮询来实现, 这样既减少了内核层的CPU消耗, 也使得上层的Application能更集中地关注应该关注的socket, 而不必每次都要将所有的 socket 通过 FD_ISSET来判断一下.

更为重要的是, epoll 因为采用 mmap的机制, 使得 内核socket buffer和 用户空间的 buffer共享, 从面省去了 socket data copy, 这也意味着, 当epoll 回调上层的 callback函数来处理 socket 数据时, 数据已经从内核层 "自动" 到了用户空间, 虽然和 用poll 一样, 用户层的代码还必须要调用 read/write, 但这个函数内部实现所触发的深度不同了,如下:

用 poll 时, poll  通知用户空间的Appliation时, 数据还在内核空间, 
所以Appliation调用 read API 时, 内部会做 copy socket data from kenel space to user space.
而用 epoll 时, epoll 通知用户空间的Appliation时, 数据已经在用户空间,
所以 Appliation调用 read API 时, 只是读取用户空间的 buffer, 没有 kernal space和 user space的switch了.

Java NIO和epoll

Java NIO也就是 NIO1.0 在Linux JDK6时已经改用 epoll 来作为 default selectorProvider了.

所以, 我有一个最大的疑问: 是否可以说, Java7中的 NIO2.0中的 AIO 改进已经无法压榨出 Linux2.6下epoll所带来的好处了?! 毕竟NIO1.0 在JDK6时已经用过 epoll 了.

还没有来得及研究Java7中的NIO2.0, 但无论如何, NIO2.0从 framework层面所带来的好处肯定是非常深远的.

Zero Copy

上面多次提到 内核空间 和 用户空间 的switch, 在socket read/write这么小的粒度频繁调用, 代价肯定是很大的.

所以可以在网上看到 Zero Copy的技术, 说到底 Zero Copy的思路就是:

分析你的业务, 看看是否能避免不必要的 跨空间copy,

比如可以用 sendfile() 函数充分利用 内核可以调用DMA 的优势,  直接在内核空间将文件的内容通过socket发送出去, 而不必经过用户空间.

显然, sendfile是有很多的前提条件的, 如果你想让文件内容作一些变换再发出去, 就必须要经过 用户空间的 Appliation logic, 也是无法使用sendfile了.  

还有一种方式就是象 epoll 所做的, 用内存映射 mmap.

 

另,评论中有一句:应该说Java NIO中的Selector是多路复用,但NIO中也有非阻塞I/O的部分,因为我们可以用Channel而不用Selector。不知所云,待理解。

以上原文链接: http://www.smithfox.com/?e=191

 

proactor模式两个要点: 
1.不注册开始读取状态而注册读取完成状态; 
2.应用程序把内存空间给到内核,而不是通过读取或写入。 

 

对第一篇的一个总结:

阻塞IO不必说了 
非阻塞IO ,IO请求时加上O_NONBLOCK一类的标志位,立刻返回,IO没有就绪会返回错误,需要请求进程主动轮询不断发IO请求直到返回正确
IO复用同非阻塞IO本质一样,不过利用了新的select系统调用,由内核来负责本来是请求进程该做的轮询操作
看似比非阻塞IO还多了一个系统调用开销,不过因为可以支持多路IO,才算提高了效率。
信号驱动IO,调用sigaltion系统调用,当内核中IO数据就绪时以SIGIO信号通知请求进程,请求进程再把数据从内核读入到用户空间,这一步是阻塞的。
异步IO,如定义所说,不会因为IO操作阻塞,IO操作全部完成才通知请求进程。
这样以来,同步和阻塞,异步和非阻塞就不会被混淆了,它们不是同一个方面上的概念,不能比较区别
同步和异步是只跟IO操作过程中进程的状态变化有关
阻塞和非阻塞就是进程的两种状态

以上是这篇:http://xly1981.iteye.com/blog/1735862

 

下面这篇文章讲了 Java NIO

http://www.iteye.com/topic/472333

 

下面又是另一种分类的解释。。理解意思就行,都是文字游戏。。

按照《Unix网络编程》的划分,IO模型可以分为:阻塞IO、非阻塞IO、IO复用、信号驱动IO和异步IO,

按照POSIX标准来划分只分为两类:同步IO和异步IO。

如何区分呢?

首先一个IO操作其实分成了两个步骤:发起IO请求和实际的IO操作。

同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果实际的IO读写阻塞请求进程,那么就是同步IO,因此阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步IO;

如果不阻塞,而是操作系统帮你做完IO操作再将结果返回给你,那么就是异步IO

阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。

 

Java nio 2.0的主要改进就是引入了异步IO(包括文件和网络)。

参考非阻塞nio框架的设计,一般都是采用Reactor模式,Reacot负责事件的注册、select、事件的派发;相应地,异步IO有个Proactor模式,Proactor负责 CompletionHandler的派发,查看一个典型的IO写操作的流程来看两者的区别:

     Reactor:  
send(msg) -> 消息队列是否为空,
如果为空 -> 向Reactor注册OP_WRITE,然后返回 -> Reactor select -> 触发Writable,通知用户线程去处理
->先注销Writable(很多人遇到的cpu 100%的问题就在于没有注销),处理Writeable,如果没有完全写入,继续注册OP_WRITE。
注意到,写入的工作还是用户线程在处理。
Proactor:
send(msg)
-> 消息队列是否为空,如果为空,发起read异步调用,并注册CompletionHandler,然后返回。
-> 操作系统负责将你的消息写入,并返回结果(写入的字节数)给Proactor -> Proactor派发CompletionHandler。
可见,写入的工作是操作系统在处理,无需用户线程参与。事实上在aio的API 中,AsynchronousChannelGroup就扮演了Proactor的角色。

 

下面这篇介绍nio: http://www.360doc.com/content/12/0914/09/820209_236025242.shtml

NIO出来之后,有了这么几个角色,ServerSocketChannel,SelectionKey,Selector. NIO中的对象跟reactor的对象对个象。

Acceptor:ServerSocketChannel;

Initiation Dispatcher:Selector;

HTTP Handler:针对SocketChannel进行实际处理的个性化对象;

Events:在SelectionKey中:

static int OP_ACCEPT 
          Operation-set bit for socket-accept operations.
static int OP_CONNECT 
          Operation-set bit for socket-connect operations.
static int OP_READ 
          Operation-set bit for read operations.
static int OP_WRITE 
          Operation-set bit for write operations.

上面一篇文章写的很乱,看下面这篇吧:

http://blog.csdn.net/shirdrn/article/details/6263692

Java NIO模式的Socket通信,是一种同步非阻塞IO设计模式,它为Reactor模式实现提供了基础。

NIO模式的基本原理描述如下:
服务端打开一个通道(ServerSocketChannel),并向通道中注册一个选择器(Selector),
这个选择器是与一些感兴趣的操作的标识(SelectionKey,即通过这个标识可以定位到具体的操作,从而进行响应的处理)相关联的,
然后基于选择器(Selector)轮询通道(ServerSocketChannel)上注册的事件,并进行相应的处理。
客户端在请求与服务端通信时,也可以向服务器端一样注册(比服务端少了一个SelectionKey.OP_ACCEPT操作集合),
并通过轮询来处理指定的事件,而不必阻塞。

下面写一下Java NIO的server和client。

另起一篇文章了,参考这篇文章:http://www.cnblogs.com/charlesblc/p/6074057.html