BIO、NIO、AIO、Selector、Poll、EPoll、Reactor、Proactor这些名词如果仔细去研究,是比较容易混淆的,相关联但有所区别,或者是在不同的领域或者层级,很多文章没有做明确的区分,做些梳理希望对理解有帮助(如有错请纠偏)。
一、概念铺垫
1.用户空间与内核空间
操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。
2.应用到操作系统内核
把java应用和操作系统的文件(一切皆文件)调用,拆开看,大概的操作如下:
【JAVA应用】-->【本地方法】-->【操作系统内核方法】-->【文件】
3.IO模式两阶段
1. 等待数据准备 (Waiting for the data to be ready)---内核
2. 将数据从内核拷贝到进程中 (Copying the data from the kernelto the process)--用户空间
(ps:模式是对于普遍问题的普遍解决方案。)_
4.什么是IO
unix/linux的世界里,不管Socket、管道、终端,一切都是文件,一切都是二进制流。
I/O操作(input and output),指在信息交换的过程中,对这些流进行数据的收发操作。
内核(kernel)利用文件描述符FD(file descriptor)来访问文件。
二、BIO、NIO、AIO
1.系统层面IO模式
linux系统提供了下面五种网络模式的方案。
- 阻塞 I/O(blocking IO)
- 非阻塞 I/O(nonblocking IO)
- I/O 多路复用( IO multiplexing)
- 信号驱动 I/O( signal driven IO)---不常用
- 异步 I/O(asynchronous IO)---2.6 版本内核的一个标准特性
2.阻塞 I/O
阻塞 IO 分为两个步骤:
步骤 1. 等待数据准备,拷贝到 OS 内核缓存区 (该过程中应用程序进程都会被阻塞)
3.非阻塞 IO
对比BIO的步骤 1 等待数据的过程中,应用程序进程不会被阻塞,而是不断询问 OS 内核数据有没有准备好:
如果数据没有准备好,OS 内核返回一个 error,应用程序进程过一段时间再次询问(该过程中应用程序进程不会被阻塞)
PS:Java NIO 中可以设置 SocketChannel:channel.configureBlocking(false);
3.多路复用IO
多路复用IO就是我们说的select,poll,epoll,通过一种机制,让一个进程process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
说说区别:
1.支持一个进程所能打开的最大连接数
select
单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是32*32,同理64位机器上FD_SETSIZE为32*64),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。
poll
poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的
epoll
虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接
2.FD剧增后带来的IO效率问题
select
因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。
poll
同上
epoll
因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。
3.消息传递方式
select
内核需要将消息传递到用户空间,都需要内核拷贝动作
poll
同上
epoll
epoll通过内核和用户空间共享一块内存来实现的,利用mmap()文件映射内存加速与内核空间的消息传递;减少复制开销。
总结:
综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。
1.表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
2.select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善。
4.异步 IO Asynchronous IO
异步IO又叫做事件驱动IO,异步IO操作是需要操作系统底层支持。
1.用户进程发起异步 read 操作后,OS 内核立即返回,用户进程不会阻塞,而是去做其他事情。
这两个阶段都是由内核自动完成的,然后发送一个信号告知用户线程操作已经完成。
三、总结
1.5种IO模型
1.同步与异步
数据从“内核缓存区-用户内存”这个过程是否需要用户进程等待,实际IO读写是否阻塞请求进程。
是否等待IO执行的结果,使用异步IO来编写程序性能会远远高于同步IO,但异步IO的缺点是编程模型复杂。
2.阻塞与非阻塞
阻塞与非阻塞关注的是调用者在等待结果返回之前所处的状态
阻塞与非阻塞描述的是用户线程调用内核IO操作的方式
阻塞blocking是指IO操作需要彻底完成后才返回到用户空间,调用结果返回之前调用者被挂起。
非阻塞noblocking是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。
阻塞与非阻塞是函数或方法的实现方式,在数据就绪之前是立即返回还是等待,发起IO请求是否会被阻塞。
3.概念总结
Selector、Poll、EPoll是lunix操作系统的内核多路复用IO的概念。
BIO(同步阻塞)、NIO(同步非阻塞)、AIO(异步非阻塞)是更多的是JAVA的概念,JAVA基于操作系统的IO模式,实现IO,提供IO的基础包。
很多JAVA框架基于线程模式,如Proactor、Reactor更多的可用理解为JAVA多线程的实现用的设计模式。这个有点类似操作系统实现的Selector、Poll、EPoll,但是可用基于操作系统这些模式提供的接口,再应用上层使用。
这些也可以统称IO模式,理解为软件上实现IO功能的一种设计,一种解决方案,模式是对于普遍问题的普遍解决方案。
4.JAVA IO包
BIO 就是传统的java.io包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用时可靠的线性顺序。它的优点就是代码比较简单、直观;缺点就是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。
NIO 是 Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。
AIO 是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
IO 大致可以分为4种类型:
InputStream、OutputStream 基于字节操作的 IO
Writer、Reader 基于字符操作的 IO
File 基于磁盘操作的 IO
Socket 基于网络操作的 IO
java.net下提供的 Scoket 很多时候人们也把它归为 同步阻塞 IO ,因为网络通讯同样是 IO 行为。
java.io下的类和接口很多,但大体都是 InputStream、OutputStream、Writer、Reader 的子集,所有掌握这4个类和File的使用,是用好 IO 的关键。
5.高性能IO设计模式
Reactor
在Reactor模式中会先对每个客户端注册感兴趣的事件,然后有一个线程专门去轮询每个客户端是否有事件发生,当有事件发生时便顺序处理每个事件,当所有事件都处理完毕后便再转去继续轮询。
Proactor
在Proactor模式中当检测到有事件发生时会新起一个异步操作,然后交由内核线程去处理,当内核线程完成IO操作之后,发送一个通知告知操作已经完成。IO模型中的异步IO模型采用的就是Proactor模式。
6.参考
Linux IO模式及 select/poll/epoll详解:https://segmentfault.com/a/1190000003063859
Linux IO 模型:https://www.jianshu.com/p/fb153133abef
IO模型:https://www.jianshu.com/p/39e2281d9864