阻塞I/O(blocking I/O)模型,进程调用recvfrom,其系统调用直到数据报到达且被拷贝到应用进程的缓冲区中或者发生错误才返回。进程从调用recvfrom开始到它返回的整段时间内是被阻塞的。java
当一个应用进程像这样对一个非阻塞描述字循环调用recvfrom时,咱们称之为轮询(polling)。应用进程持续轮询内核,以查看某个操做是否就绪。react
根据上述5种IO模型,前4种模型-阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步I/O模型,由于其中真正的I/O操做(recvfrom)将阻塞进程,在内核数据copy到用户空间时都是阻塞的。linux
一个IO操做能够分为两个步骤:发起IO请求和实际的IO操做
例如:
一、操做系统的一次写操做分为两步:将数据从用户空间拷贝到系统空间;从系统空间往网卡写。
二、一次读操做分为两步:将数据从网卡拷贝到系统空间;将数据从系统空间拷贝到用户空间。编程
阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,若是阻塞直到完成那么就是传统的阻塞IO,若是不阻塞,那么就是非阻塞IO。windows
同步IO和异步IO的区别就在于第二个步骤是否阻塞,若是实际的IO读写阻塞请求进程,那么就是同步IO,所以阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步IO,若是不阻塞,而是操做系统作完IO两个阶段的操做再将结果返回,那么就是异步IO。缓存
IO多路复用,就是经过一种机制,一个进程能够监视多个描述符,一旦某个描述符就绪(通常是读就绪或者写就绪),可以通知程序进行相应的读写操做。性能优化
从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操做,效率更差。可是,使用select之后最大的优点是用户能够在一个线程内同时处理多个socket的IO请求。用户能够注册多个socket,而后不断地调用select读取被激活的socket,便可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须经过多线程的方式才能达到这个目的。服务器
IO多路复用方式容许单线程内处理多个IO请求,可是每一个IO请求的过程仍是阻塞的(在select函数上阻塞),平均时间甚至比同步阻塞IO模型还要长。若是用户线程只注册本身感兴趣的socket或者IO请求,而后去作本身的事情,等到数据到来时再进行处理,则能够提升CPU的利用率。
因为select函数是阻塞的,所以多路IO复用模型也被称为异步阻塞IO模型。注意,这里的所说的阻塞是指select函数执行时线程被阻塞,而不是指socket。通常在使用IO多路复用模型时,socket都是设置为NONBLOCK的,不过这并不会产生影响,由于用户发起IO请求时,数据已经到达了,用户线程必定不会被阻塞。
IO多路复用是最常使用的IO模型,可是其异步程度还不够“完全”,由于它使用了会阻塞线程的select系统调用。所以IO多路复用只能称为异步阻塞IO,而非真正的异步IO。网络
展现了非阻塞IO如何让你使用一个selector区处理多个链接.多线程
Linux支持IO多路复用的系统调用有select、poll、epoll,这些调用都是内核级别的。但select、poll、epoll本质上都是同步I/O,先是block住等待就绪的socket,再是block住将数据从内核拷贝到用户内存。
在这两种模式下的事件多路分离器反馈给程序的信息是不同的:
1.Reactor模式下说明你能够进行读写(收发)操做了。
2.Proactor模式下说明已经完成读写(收发)操做了,具体内容在给定缓冲区中,能够对这些内容进行其余操做了。
Reactor关注的是I/O操做的就绪事件,而Proactor关注的是I/O操做的完成事件
通常地,I/O多路复用机制都依赖于一个事件多路分离器(Event Demultiplexer)。分离器对象可未来自事件源的I/O事件分离出来,并分发到对应的read/write事件处理器(Event Handler)。
Reactor模式采用同步IO,而Proactor采用异步IO。
在Reactor中,事件分离器负责等待文件描述符或socket为读写操做准备就绪,而后将就绪事件传递给对应的处理器,最后由处理器负责完成实际的读写工做。
而在Proactor模式中,处理器或者兼任处理器的事件分离器,只负责发起异步读写操做。IO操做自己由操做系统来完成。传递给操做系统的参数须要包括用户定义的数据缓冲区地址和数据大小,操做系统才能从中获得写出操做所需数据,或写入从socket读到的数据。事件分离器捕获IO操做完成事件,而后将事件传递给对应处理器。好比,在windows上,处理器发起一个异步IO操做,再由事件分离器等待IOCompletion事件。典型的异步模式实现,都创建在操做系统支持异步API的基础之上,咱们将这种实现称为“系统级”异步或“真”异步,由于应用程序彻底依赖操做系统执行真正的IO工做。
Reactor和Proactor模式的主要区别就是真正的读取和写入操做是有谁来完成的,Reactor中须要应用程序本身读取或者写入数据,而Proactor模式中,应用程序不须要进行实际的读写过程,它只须要从缓存区读取或者写入便可,操做系统会读取缓存区或者写入缓存区到真正的IO设备.
NIO,有人称之为New I/O,由于它相对于以前的I/O类库是新增的,因此被称为New I/O。可是,因为以前老的 I/O 类库是阻塞 I/O,New I/O类库的目标就是要让Java支持非阻塞 I/O,因此,更多的人喜欢称之为非阻塞 I/ O(Non-block I/O)。
注意,select是阻塞的,不管是经过操做系统的通知(epoll)仍是不停的轮询(select,poll),这个函数是阻塞的。因此你能够放心大胆地在一个while(true)里面调用这个函数而不用担忧CPU空转。
NIO采用Reactor模式,一个Reactor线程聚合一个多路复用器Selector,它能够同时注册、监听和轮询成百上千个Channel,一个IO线程能够同时并发处理N个客户端链接,线程模型优化为1:N(N < 进程可用的最大句柄数)或者M : N (M一般为CPU核数 + 1, N < 进程可用的最大句柄数)。
JAVA NIO 不是同步非阻塞I/O吗,为何说JAVA NIO提供了基于Selector的异步网络I/O?
java nio的io模型是同步非阻塞,这里的同步异步指的是真正io操做(数据内核态用户态的拷贝)是否须要进程参与。
而说java nio提供了异步处理,这个异步应该是指编程模型上的异步。基于reactor模式的事件驱动,事件处理器的注册和处理器的执行是异步的。
AIO(Async I/O)里面会更进一步:不但等待就绪是非阻塞的,就连数据从网卡到内存的过程也是异步的。
换句话说,BIO里用户最关心“我要读”,NIO里用户最关心"我能够读了",在AIO模型里用户更须要关注的是“读完了”。
NIO一个重要的特色是:socket主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的I/O操做是同步的(消耗CPU但性能很是高)。
BIO模型,之因此须要多线程,是由于在进行I/O操做的时候,一是没有办法知道到底能不能写、能不能读,只能"傻等",即便经过各类估算,算出来操做系统没有能力进行读写,也无法在socket.read()和socket.write()函数中返回,这两个函数没法进行有效的中断。因此除了多开线程另起炉灶,没有好的办法利用CPU。
NIO的读写函数能够马上返回,这就给了咱们不开线程利用CPU的最好机会:若是一个链接不能读写(socket.read()返回0或者socket.write()返回0),咱们能够把这件事记下来,记录的方式一般是在Selector上注册标记位,而后切换到其它就绪的链接(channel)继续进行读写。
咱们大概能够总结出NIO是怎么解决掉线程的瓶颈并处理海量链接的:
NIO由原来的阻塞读写(占用线程)变成了单线程轮询事件,找到能够进行读写的网络描述符进行读写。除了事件的轮询是阻塞的(没有可干的事情必需要阻塞),剩余的I/O操做都是纯CPU操做,没有必要开启多线程。
而且因为线程的节约,链接数大的时候由于线程切换带来的问题也随之解决,进而为处理海量链接提供了可能。
不少人喜欢将JDK1.4提供的NIO框架称为异步非阻塞I/O,可是,若是严格按照UNIX网络编程模型和JDK的实现进行区分,实际上它只能被称为非阻塞I/O,不能叫异步非阻塞I/O。在早期的JDK1.4和1.5 update10版本以前,JDK的Selector基于select/poll模型实现,它是基于I/O复用技术的非阻塞I/O,不是异步I/O。在JDK1.5 update10和Linux core2.6以上版本,Sun优化了Selctor的实现,它在底层使用epoll替换了select/poll,上层的API并无变化,能够认为是JDK NIO的一次性能优化,可是它仍旧没有改变I/O的模型。
由JDK1.7提供的NIO2.0,新增了异步的套接字通道,它是真正的异步I/O,在异步I/O操做的时候能够传递信号变量,当操做完成以后会回调相关的方法,异步I/O也被称为AIO。
NIO类库支持非阻塞读和写操做,相比于以前的同步阻塞读和写,它是异步的,所以不少人习惯于称NIO为异步非阻塞I/O,包括不少介绍NIO编程的书籍也沿用了这个说法。为了符合你们的习惯,咱们也将NIO称为异步非阻塞I/O或者非阻塞I/O。
基本上,全部的 IO 在NIO 中都从一个Channel 开始。Channel 有点象流。 数据能够从Channel读到Buffer中,也能够从Buffer 写到Channel中。这里有个图示:
Selector容许单线程处理多个Channel。若是你的应用打开了多个链接(通道),但每一个链接的流量都很低,使用Selector就会很方便。例如,在一个聊天服务器中。
这是在一个单线程中使用一个Selector处理3个Channel的图示:
要使用Selector,得向Selector注册Channel,而后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就能够处理这些事件,事件的例子有如新链接进来,数据接收等。
最后总结一下到底NIO给咱们带来了些什么:
事件驱动模型
避免多线程
单线程处理多任务
非阻塞I/O,I/O读写再也不阻塞,而是返回0
基于block的传输,一般比基于流的传输更高效
更高级的IO函数,zero-copy
IO多路复用大大提升了Java网络应用的可伸缩性和实用性
BIO | NIO | AIO 以Java的角度,理解以下:
在JDK1.4以前,用Java编写网络请求,都是创建一个ServerSocket,而后,客户端创建Socket时就会询问是否有线程能够处理,若是没有,要么等待,要么被拒绝。即:一个链接,要求Server对应一个处理线程。
在Java里的由来,在JDK1.4及之后版本中提供了一套API来专门操做非阻塞I/O,咱们能够在java.nio包及其子包中找到相关的类和接口。因为这套API是JDK新提供的I/O API,所以,也叫New I/O,这就是包名nio的由来。这套API由三个主要的部分组成:缓冲区(Buffers)、通道(Channels)和非阻塞I/O的核心类组成。在理解NIO的时候,须要区分,说的是New I/O仍是非阻塞IO,New I/O是Java的包,NIO是非阻塞IO概念。这里讲的是后面一种。
NIO自己是基于事件驱动思想来完成的,其主要想解决的是BIO的大并发问题: 在使用同步I/O的网络应用中,若是要同时处理多个客户端请求,或是在客户端要同时和多个服务器进行通信,就必须使用多线程来处理。也就是说,将每个客户端请求分配给一个线程来单独处理。这样作虽然能够达到咱们的要求,但同时又会带来另一个问题。因为每建立一个线程,就要为这个线程分配必定的内存空间(也叫工做存储器),并且操做系统自己也对线程的总数有必定的限制。若是客户端的请求过多,服务端程序可能会由于不堪重负而拒绝客户端的请求,甚至服务器可能会所以而瘫痪。
NIO基于Reactor,当socket有流可读或可写入socket时,操做系统会相应的通知引用程序进行处理,应用再将流读取到缓冲区或写入操做系统。
也就是说,这个时候,已经不是一个链接就要对应一个处理线程了,而是有效的请求,对应一个线程,当链接没有数据时,是没有工做线程来处理的。
与NIO不一样,操做系统负责处理内核区/用户区的内存数据迁移和真正的IO操做,应用程序只须直接调用API的read或write方法便可。这两种方法均为异步的,对于读操做而言,当有流可读取时,操做系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操做而言,当操做系统将write方法传递的流写入完毕时,操做系统主动通知应用程序。
便可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。
在JDK1.7中,这部份内容被称做NIO.2,主要在java.nio.channels包下增长了下面四个异步通道:
其中的read/write方法,会返回一个带回调函数的对象,当执行完读取/写入操做后,直接调用回调函数。
说道实现原理,还要从操做系统的IO模型上了解
按照《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。
能够理解的说明是:在Linux 2.6之后,java NIO的实现,是经过epoll来实现的,这点能够经过jdk的源代码发现。而AIO,在windows上是经过IOCP实现的,在linux上经过新的API来实现。