UNIX I/O 模型
根据冯.诺依曼结构,计算机结构分为 5 大部分:运算器、控制器、存储器、输入设备、输出设备。
输入设备(比如键盘)和输出设备(比如显示器)都属于外部设备。网卡、硬盘这种既可以属于输入设备,也可以属于输出设备。
输入设备向计算机输入数据,输出设备接收计算机输出的数据。
从计算机结构的视角来看的话, I/O 描述了计算机系统与外部设备之间通信的过程。
为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为 用户空间(User space) 和 内核空间(Kernel space ) 。
平常运行的应用程序都是运行在用户空间,只有内核空间才能进行系统态级别的资源有关的操作,例如文件管理、进程通信、内存管理等。如果要进行 IO 操作,需要依赖内核空间的能力,用户空间的程序不能直接访问内核空间。
当想要执行 IO 操作时,由于没有执行这些操作的权限,只能发起系统调用请求操作系统帮忙完成。
在平常开发过程中接触最多的就是 磁盘 IO(读写文件) 和 网络 IO(网络请求和响应)。
从应用程序的视角来看的话,应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。也就是说,应用程序实际上只是发起了 IO 操作的调用,而具体 IO 的执行是由操作系统的内核来完成的。
当应用程序发起 I/O 调用后,会经历两个步骤:
- 内核等待 I/O 设备准备好数据
- 内核将数据从内核空间拷贝到用户空间。
UNIX 系统下, IO 模型一共有 5 种:同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O和异步 I/O。
Java 常见的 I/O 模型
Java中的 IO(Input/Output)模型是管理计算机与外部设备(如磁盘、网络等)之间数据交换的重要机制。它定义了数据如何在应用程序和操作系统之间流动,以及如何处理这些流动过程中的阻塞、非阻塞、同步和异步等问题。Java中常见的IO模型包括 BIO
(Blocking I/O,阻塞IO)、NIO
(Non-Blocking I/O,非阻塞IO)、AIO
(Asynchronous I/O,异步IO)等。
Blocking I/O (BIO)
BIO
模型是最传统的 IO 模型,它在读写数据过程中会发生阻塞现象。
Blocking IO
是指在执行输入/输出操作时,当用户线程发起 IO 请求后,内核会检查数据是否准备就绪,如果没有就绪,程序会被阻塞(即暂停执行),直到操作完成并返回结果为止。BIO 属于同步阻塞 IO 模型 。
在使用 阻塞I/O 的情况下,当一个 I/O 操作开始执行时,程序将被阻塞,直到 I/O 操作完成并返回结果。这意味着程序无法继续执行其他任务,直到 I/O 操作完成。
比如,当一个程序从磁盘读取文件内容时,如果使用阻塞I/O,该程序将一直等待文件读取完成,然后才能继续执行后续的操作。
BIO
的特点是相对简单易用,但在 I/O 操作执行期间会导致程序的停顿,降低了整体的并发性和吞吐量。
BIO
适用于客户端连接数量不高的情况。但当面对十万甚至百万级连接的时候,传统的 BIO
模型是十分低效的。
New I/O (NIO)
Java 中的 NIO
于 Java 1.4 中引入,对应 java.nio
包,提供了 Channel
, Selector
,Buffer
基于缓冲区、选择器和非阻塞 IO 模型的 IO 处理方式。
NIO
中的 N
可以理解为 Non-blocking,不单纯是 New
。它是支持面向缓冲的,基于通道(Channel)的 I/O 操作方法。 可以实现更高的并发、更低的延迟以及更少的资源消耗,适用于高负载、高并发的(网络)应用。
对于文件等 IO 操作,由于 NIO
需要额外的缓冲区和选择器等机制,可能并不能比传统阻塞 IO 提供更好的性能。
Java 中的 NIO 属于同步非阻塞 IO 模型/多路复用模型。其核心组件为:
-
Buffer(缓冲区):用于存储数据的容器,提供了数据的读写操作。
-
Channel(通道):用于连接
Buffer
和Selector
,可以进行双向读写操作。 -
Selector(选择器):用于监听多个
Channel
的事件,当某个Channel
有数据可读或可写时,Selector
会通知用户线程进行处理。
NIO 模型适用于连接数目多且连接时间较短的场景,如聊天服务器、弹幕系统等。通过 Selector
,NIO
模型可以实现一个线程管理多个连接,从而显著提高系统性能。
同步非阻塞模型
同步非阻塞 IO 模型中,应用程序会一直发起 read
调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。
相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。
但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。
多路复用模型
在 NIO
中,数据通过 buffer 进行传输和处理。Seletor 用于管理多个 Channel,可以通过它实现多路复用,即同时监视多个 channel 的 IO 事件。
通过 Selector
只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。
IO 多路复用模型中,线程首先发起 select
调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read
调用。read
调用的过程(数据从内核空间 -> 用户空间
)还是阻塞的。
-
select 调用:内核提供的系统调用,它支持一次查询多个系统调用的可用状态。几乎所有的操作系统都支持。
-
epoll 调用:
linux 2.6
内核,属于 select 调用的增强版本,优化了 IO 的执行效率。
当任意一个通道准备好进行 IO 操作时,选择器就会通知应用程序进行相应的处理。这种方式避免了线程阻塞和轮询操作的开销,提高了应用程序的并发性能。另外,NIO
还提供了非阻塞和异步的 IO 操作方式。
Asynchronous I/O(AIO)
AIO
就是 NIO 2
。Java 7 中引入了 NIO
的改进版 NIO 2
,它是异步 IO 模型,实现了真正的异步 IO 操作。
提供了一种基于事件和回调机制的异步 IO 处理方式。相对于传统的 BIO
和 NIO
模型,AIO
提供了更高级的 IO 操作和处理能力。
AIO
的特点是在进行 IO 操作时不会阻塞,应用程序可以继续执行其他任务,不需要等待系统调用的结果,也不需要进行数据的轮询操作。而不需要等待 IO 操作的完成。当 IO 操作完成时,操作系统会通知应用程序,并通过回调函数处理 IO 结果。这样可以提高应用程序的并发性能和响应能力,特别适用于处理大量的并发 IO 请求。
AIO
的实现基于操作系统提供的异步 IO 机制,如 Linux 中的 epoll
和Windows 中的"I/O Completion Port
"。通过使用操作系统提供的异步IO 特性,AIO
能够将 IO 操作的完成通知直接传递给应用程序,避免了线程的阻塞和轮询操作,提高了 IO 操作的效率和吞吐量。
在 Java中,AIO
API 主要是通过 AsynchronousChannel
和CompletionHandler
接口来实现的。应用程序可以通过注册事件和回调函数的方式来处理异步 IO 操作的结果,适用于高并发、高吞吐量的网络通信和 IO 操作场景,如高性能的服务器和分布式系统等。