前言
在Java中,有三种IO模型: BIO,NIO,AIO。介绍这三种IO模型之前,需要介绍一下同步,异步与阻塞,非阻塞的概念,然后再从Java和Linux OS的角度去分析BIO,NIO和AIO
同步与异步
同步
同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。
通俗的例子描述同步就像:
你打电话问书店老板有没有《葵花宝典》这本书的时候,如果是同步机制,书店老板会说,你稍等,”我查一下”,然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。
异步
异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。
通俗的例子描述异步就像:
而异步机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调
此处参照知乎上关于此问题的回答:https://www.zhihu.com/question/19732473
再次总结一下同步与异步:
同步与异步最大的区别就是被调用方的执行方式和返回时机,同步指的是被调用方做完事情之后再返回,异步指的是被调用方先返回,然后再做事情,做完之后再想办法通知调用方
阻塞与非阻塞
阻塞
阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
非阻塞
非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。
还是上面买书的例子:
你打电话问书店老板有没有《葵花宝典》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果。
同步、异步和阻塞、非阻塞的区别
阻塞和同步不是一回事,同步,异步与阻塞,非阻塞针对的对象是不一样的,阻塞,非阻塞是说的调用者,同步,异步说的是被调用者
BIO、NIO、AIO概览
BIO(Blocking I/O):BIO也就是传统的同步阻塞IO模型,对应Java.io包,它提供了很多IO功能,比如输入输出流,对文件进行操作。在网络编程(Socket通信)中也同样进行IO操作。
NIO(New I/O): NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象
AIO: AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型
Linux的5种 I/O模型
上面简单介绍了Java中的三种IO模型,三种模型提供的与IO有关的API,在文件处理时,底层实际上是依赖操作系统层面的IO操作实现的,比如在Linux 2.6以后,Java中的NIO和AIO都是通过 epoll来实现的,关于epoll等概念后面也会阐述。
而实际上在Linux(Unix)操作系统中,共有五种 IO模型,分别是:阻塞IO模型、非阻塞IO模型、IO复用模型、信号驱动IO模型以及异步IO模型,而4种都是同步的,只有最后一种是异步的。下面的分析主要参考了《UNIX网络编程 卷1:套接字联网API(第3版)中的介绍。
阻塞IO模型 - BIO
一个输入操作通常包括两个不同的阶段:
- 等待数据准备好
- 从内核向进程复制数据
对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达,当所等待分组到达时,它被复制到内核中的某个缓冲区,第二步就是把数据从内核缓冲区复制到应用进程缓冲区。
从上图可以看出,应用进程通过 系统调用 recvfrom
去接收数据,而由于内核数据没有准备好,应用进程就会阻塞,直到内核准备好数据并将其从内核复制到应用进程的缓冲区中或者发生错误才返回。最常见的错误就是系统调用被信号中断。进程从调用recvfrom开始到它返回的整段时间内是被阻塞的。
Linux下的阻塞式I/O模型就对应了Java下的BIO模型,BIO的底层实现是调用操作系统的API去执行的,也就是调用操作系统的Socket套接字。
非阻塞式I/O模型 - NIO
应用进程通过系统调用 recvfrom
不断的去和内核交互,直到内核数据报准备好,而如果内核无数据准备好,转而立即返回一个 EWOULDBLOCK
的错误,过一段时间再次发送 recvfrom
请求,在此期间进程可以做其他事情,不用一直等待,这就是非阻塞。
当一个应用进程循环调用 recvfrom
时,我们称之为轮询(polling),应用进程持续轮询内核,以查看某个操作是否就绪。Java的NIO映射到Linux操作系统就是如上图所示的非阻塞I/O模型
I/O复用模型
IO多路复用使用select/poll/epoll
函数,多个进程的IO都可以注册在同一个 select
上,当用户进程调用该 select
时,select
去监听所有注册好的IO,如果所有被监听的IO需要的数据都没有准备好,那么 select
调用进程会被阻塞,只要任意一个IO的数据报套接字变为可读,即数据报已经准备好,select
就返回套接字可读这一条件,然后调用 recvfrom
把所读数据报复制到应用进程缓冲区。
强调一点就是,IO多路复用模型并没有涉及到非阻塞,进程在发出select
后,要一直阻塞等待其监听的所有IO操作至少有一个数据准备好才返回,强调阻塞状态,不存在非阻塞。
而在 Java NIO中也可以实现多路复用,主要是利用多路复用器 Selector,与这里的 select
函数类型,Selector会不断轮询注册在其上的通道Channel,如果有某一个Channel上面发生读或写事件,这个Channel处于就绪状态,就会被Selector轮询出来。关于Java NIO实现多路复用更多的介绍请查询相关文章。
I/O多路复用的应用场景
I/O 多路复用的主要应用场景如下:
- 服务器需要同时处理多个处于监听状态或者多个连接状态的套接字
- 服务器需要同时处理多种网络协议的套接字
I/O多路复用的系统调用函数
目前支持I/O 多路复用的系统调用函数有 select,pselect,poll,epoll
。在Linux 网络编程中,很长一段时间都使用select
做轮询和网络事件通知。然而因为select的一些固有缺陷导致它的应用受到了很大的限制,比如select 单个进程打开的最大句柄数是有限的。最终在 Linux 2.6 选择epoll 替代了select,Java NIO和AIO底层就是用epoll。更多关于这些系统调用的介绍与使用,请参阅 《UNIX网络编程 卷1:套接字联网API(第3版)
信号驱动式I/O模型
应用进程预先向内核安装一个信号处理函数,然后立即返回,进程继续工作,不阻塞,当数据报准备好读取时,内核就为该进程产生一个信号通知进程,然后进程再调用recvfrom读取数据报。
信号驱动式IO不是异步的
信号驱动式IO在数据准备阶段是异步的,当内核中有数据报准备后再通知进程,但是在调用 recvfrom操作进行数据拷贝时是同步的,所以总体来说,整个IO过程不能是异步的。
异步I/O模型 - AIO
应用进程调用aio_read
函数,给内核传递描述符,缓存区指针,缓存区大小和文件偏移,并告诉内核当整个操作完成时如何通知进程,然后该系统调用立即返回,而且在等待I/O完成期间,我们的进程不被阻塞,进程可以去干其他事情,然后内核开始等待数据准备,数据准备好以后再拷贝数据到进程缓冲区,最后通知整个IO操作已完成。
Java的AIO提供了异步通道API,其操作系统底层实现就是这个异步I/O模型
与信号驱动式I/O的区别
主要区别在于: 信号驱动式I/O是由内核通知我们何时去启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。
5种I/O模型的比较
由上图可以再次看出,IO操作主要分为两个阶段:
- 等待数据报准备阶段
- 数据拷贝阶段
前4种IO模型都是同步IO模型,为什么说都是同步的,因为它们在第二步数据拷贝阶段都是阻塞的,这会导致整个请求进程存在阻塞的情况,所以是同步的,而异步IO模型不会导致请求进程阻塞。
小结
上面简要阐述了BIO,NIO,AIO以及Linux下的5种IO模型,对于IO模型更加详细的介绍参考《UNIX网络编程 卷1:套接字联网API(第3版)