在学习之前我相信大家肯定都会和我一样对IO模型有很多的疑惑,想要迫切地知道一切,比如IO模型到底是什么,它有什么作用等等。但我们要明白IO 模型这一块涉及到了许多计算机底层的知识,想要彻底搞定他我们就必须从底层一步步的去理解。
首先IO是什么?
IO 即 Input/Output ,表示输入/输出。在计算机中,我们知道有许多的IO外设,像是键盘、鼠标、硬盘等等。输入设备负责向计算中输入数据,而输出设备则负责接收计算机中输出的数据。我们操作外设完成与计算机系统间的交互。内部实现则是交由外设与计算机系统之间的通信完成,而这正是我们需要了解的IO过程。
那么外设是怎么与计算机系统之间进行通信的呢?
这里涉及到计算机系统内部的实现原理。
我们都知道每台计算机要使用都得有一个操作系统(OS)。操作系统实际是封装的一层软件,用以实现文件管理、存储管理、进程管理、设备管理等等功能。那么操作系统又是怎么实现的呢?这就不得不提到内核了。我们可以把内核看做一个软件,同样是做了一层封装。现代计算机操作系统为了保证稳定和安全性,大多采用了“机制与策略分离”的原理来划分OS结构(这里的机制指的是为实现某一特定功能的具体执行机构,策略则是指在机制的基础上借助某些参数或算法实现的优化)。
内核作为软件一开始存储在磁盘中,当我们打开电脑时,内核便被加载到了内存中开始运行。同时将整个内存划分为了 内核空间(内核内)和用户空间(内核外) 这两块区域。内核空间可以使用一些实现基础功能的指令如进程调度、地址转换等等,而用户空间不能直接调用内核空间指令。
用户进程执行一开始都在用户空间,那么问题来了,如果它想要使用内核空间指令该咋办呢?此时就必须进行系统调用,实现CPU的上下文切换(用户态——>内核态)。
了解了这些我们在想一下外设与计算机系统间的通信过程。通信过程我们总离不开存储过程,进程通信吧,那我们的用户进程也不得不发起一个系统调用请求。然后将IO的执行交由内核完成。
那么内核又是怎么实现的呢?注意这个时候才终于到了本篇的核心:IO模型。IO模型决定了内核对IO请求采取的实现策略。但总体不外乎就两个步骤:
- 内核等待 I/O 设备准备好数据
- 内核将数据从内核空间拷贝到用户空间。
IO模型
BIO(Blocking IO)
顾名思义它是一种阻塞式的IO模型。那么是怎么个阻塞法呢?
当应用程序发起一个IO请求时,例如说我想从磁盘文件中读取数据,到了计算机底层就是发起了一个read系统调用指令。内核执行这个read指令实现数据读取(这里指令执行细节不做叙述,具体可参考计算机操作系统),这个指令执行需要一点时间,我们不妨把它分为三个步骤:准备数据--->数据就绪--->拷贝数据。注意:此时应用程序会陷入阻塞状态直到接收到拷贝完成后的数据。
由于它必须经历等待数据准备就绪的过程,所有称它为阻塞同步模型。这种模型的缺陷很明显,特别是在网络通信中,服务端不可能为了一个连接请求或是数据传输就陷入阻塞,为此服务端不得不创建多个线程来实现多个连接请求。它的优点也很明显,实现十分简单。
NIO(Non-Blocking/New IO)
同样的我们首先知道他是同步非阻塞的。既然是非阻塞那就意味着它不会和BIO一样阻塞等待着接收内核数据直到拷贝完成。既然他不会阻塞等待,那它又是怎么知道内核准备好数据的时机呢?
一种方法是不断调用系统指令,直到数据准备完成,这么做虽然不会使进程陷入阻塞,但会极大地消耗CPU资源,显然不是一种可行的方案。
这个时候IO多路复用模型就登场了!多路指的是多个连接
简单来说就是它不再使用“重量级”的系统调用指令来查询数据是否准备完成,而是通过select/poll/epoll调用指令来实现。它们支持一次查询多个系统调用以检查数据准备状态。
到这里我们明白了,NIO 采用同步非阻塞的方式,它使用IO多路复用模型来减少无效的系统调用以减少对CPU资源的消耗。
在Java中我们可以通过Selector,Channel,Buffer等抽象来深入理解NIO。
AIO(Asynchronous IO)
它采用的是异步模型,基于事件和回调机制来实现。它实现非阻塞的原理就是通过一个事件回调,由于线程同样需要监听该回调事件,因此它在性能上的提升并不大。