java|图解四种 IO 模型

最近越来越认为,在讲解技术相关问题时,大白话固然很重要,通俗易懂,让人有想读下去的欲望。但几乎所有的事,都有两面性,在看到其带来好处时,不妨想想是否也引入了不好的地方。
例如在博客中,过于大白话的语言的确会让你阅读起来更加顺畅,也更容易理解。但这都是其他人理解,已经咀嚼过了的,人家是已经完全理解了,你从这些信息中大概可能会观察不到全貌。所以,适当的白话是很好的,但这个度得控制一下。
接下来切入正文。
相信大家经常看到这个问题:

BIO、NIO 和 AIO 有什么区别?
看到这个问题,可能你脑海中就会浮现以下这些字眼。比如 BIO 就是如果从内核获取数据会一直阻塞,直到数据准备完毕返回。再比如 NIO,内核在数据没有准备好时不会阻塞住,调用程序会一直询问内核数据是否 Ready。
虽然是正确的,字数也很少。但是这样一来,你看这些概念就不是理解,而是背诵了。其实 BIO 和 NIO 这类的名词还有一个共同的名字叫——IO模型,总共有:
java|图解四种 IO 模型
文章图片

IO 模型
由于信号驱动 IO 在实际中不常用,我们主要讲以下四种模型:
  1. 同步阻塞
  2. 同步非阻塞
  3. IO 多路复用
  4. 异步 IO
这里还是通过例子来理解这 4 种 IO 模型:
假设此时客户端正在发送一些数据到服务器,并且数据已经通过客户端的协议栈、网卡,陆陆续续的到达了服务器这边的内核态 Buffer 中了。
不清楚用户态和内核态区别的可以看看《简单聊聊用户态和内核态的区别》
对数据在网络中是如何传输的细节感兴趣的,可以去看看我之前写的文章 《请求数据包从发送到接收,都经历了什么?》。
同步阻塞 BIO
我们需要知道,内核在处理数据的时候其实是分成了两个阶段:
  • 数据准备
  • 数据复制
在网络 IO 中,数据准备可能是客户端还有部分数据还没有发送、或者正在发送的途中,当前内核 Buffer 中的数据并不完整;而数据复制则是将内核态 Buffer 中的数据复制到用户态的 Buffer 中去。
【java|图解四种 IO 模型】当调用线程发起 read 系统调用时,如果此时内核数据还没有 Ready,调用线程会阻塞住,等待内核 Buffer 的数据。内核数据准备就绪之后,会将内核态 Buffer 的数据复制到用户态 Buffer 中,这个过程中调用线程仍然是阻塞的,直到数据复制完成,整个流程用图来表示就张这样:
java|图解四种 IO 模型
文章图片

同步非阻塞 NIO
相信大家知道 Java 中有个包叫 nio,但那跟我们现在正在讨论的 NIO 不是同一个概念。
现在正在讨论的是 Non-Blocking IO,代表同步非阻塞,是一种基础的 IO 模型。而 nio 包则是 New IO,里面的 IO 模型实际上是 IO多路复用,大家不要搞混淆了。
有了 BIO 的基础,这次我们直接来看图:
java|图解四种 IO 模型
文章图片

NIO
还是分为两个阶段来讨论。
数据准备阶段。此时用户线程发起 read 系统调用,此时内核会立即返回一个错误,告诉用户态数据还没有 Read,然后用户线程不停地发起请求,询问内核当前数据的状态。
数据复制阶段。此时用户线程还在不断的发起请求,但是当数据 Ready 之后,用户线程就会陷入阻塞,直到数据从内核态复制到用户态。
稍微总结一下,如果内核态的数据没有 Ready,用户线程不会阻塞;但是如果内核态数据 Ready 了,即使当前的 IO 模型是同步非阻塞,用户线程仍然会进入阻塞状态,直到数据复制完成,并不是绝对的非阻塞。
那 NIO 的好处是啥呢?显而易见,实时性好,内核态数据没有 Ready 会立即返回。但是事情的两面性就来了,频繁的轮询内核,会占用大量的 CPU 资源,降低效率。
IO 多路复用
IO 多路复用实际上就解决了 NIO 中的频繁轮询 CPU 的问题。在之前的 BIO 和 NIO 中只涉及到一种系统调用——read,在 IO 多路复用中要引入新的系统调用——select
read 用于读取内核态 Buffer 中的数据,而 select 你可以理解成 MySQL 中的同名关键字,用于查询 IO 的就绪状态。
在 NIO 中,内核态数据没有 Ready 会导致用户线程不停的轮询,从而拉满 CPU。而在 IO 多路复用中调用了 select 之后,只要数据没有准备好,用户线程就会阻塞住,避免了频繁的轮询当前的 IO 状态,用图来表示的话是这样:
java|图解四种 IO 模型
文章图片

IO 多路复用
异步 AIO
该模型的实现就如其名,是异步的。用户线程发起 read 系统调用之后,无论内核 Buffer 数据是否 Ready,都不会阻塞,而是立即返回。
内核在收到请求之后,会开始准备数据,准备好了&复制完成之后会由内核发送一个 Signal 给用户线程,或者回调用户线程注册的接口进行通知。用户线程收到通知之后就可以去读取用户态 Buffer 的数据了。
java|图解四种 IO 模型
文章图片

AIO
由于这种实现方式,异步 IO 有时也被叫做信号驱动 IO。相信你也发现了,这种方式最重要的是需要 OS 的支持,如果 OS 不支持就直接完蛋。
Linux 系统在 2.6 版本的时候才引入了异步IO,不过那个时候并不算真正的异步 IO,因为内核并不支持,底层其实是通过 IO 多路复用实现的。而到了 Linux 5.1 时,才通过 io_uring 实现了真 AIO。

    推荐阅读