起因

最近在学习分布式课程的时候 , 涉及到ZK实现可扩展锁的章节 , 提到了 事件驱动多线程 , 很自然的联想到了NIO有关的知识。

想起来之前在纠结的一个问题 :

NIO以为这 No Blocking I/O , 也就是我们当前的线程不会继续等待本次的I/O事件 , 那么究竟是谁在执行这里的I/O? 这里I/O是否会占用线程/进程的时间片?

首先我们假设,NIO不会占用线程/进程的时间片, 并且这里的I/O操作是由于OS中的组件完成的,

在此之前简单回顾NIO 与 OS 相关的知识。

NIO基础

更详细的内容请参考 : https://blog.dhx.icu/2023/05/netty/Netty网络编程(1)JavaNIO-1[Netty]/

Java NIO(New I/O)是从Java 1.4版本开始引入的一个新的I/O API,可以用于替代标准的Java I/O API

NIO与原来的I/O有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的I/O操作

NIO I/O
面向流(Stream Oriented) 面向缓冲区(Buffer Oriented)
阻塞I/O(Blocking I/O) 非阻塞I/O(Non Blocking I/O)
选择器(Selectors

NIO的设计还是为了数据传输。对于channel,如果说之前的文件流类似于水流,那么channel就类似于铁路。对于铁路本身,他是不能直接传输数据的,需要我们配备上火车车厢,而channel的作用就是连接初始地点以及目标地点。因此注意通道本身不能传输数据,要想传输数据必须要有缓冲区。如果你想要把数据写入到文件中,那么就可以先把部分的数据写入到缓冲区中, 通过通道把这部分数据运输到目标的文件,反之亦然。因此说现在的面向缓冲区的传输方式是双向的。

Talk is cheap , show me the code

下面是Java的NIO API简单示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(String[] args) throws I/OExceptI/On {
// 访问文件, mode为 read & write
RandomAccessFile raf = new RandomAccessFile("test", "rw");
FileChannel channel = raf.getChannel();
// 申请buf , 如果是 ByteBuffer.allocateDirect(cap) 则是申请主存而不是JVM内存。
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 写文件
StringBuilder w = new StringBuilder();
for (int i = 0; i < 1000; i++) {
buffer.put("test".getBytes(StandardCharsets.UTF_8));
channel.write(buffer);
w.append("test");
buffer.clear();
}
// 刷新数据到磁盘, true表示强制更新文件metaData
channel.force(true);
// 读文件
int len = -1;
StringBuilder r = new StringBuilder();
while((len=channel.read(buffer))!=-1){
r.append(new String(buffer.array(),0,len));
}
assert w.toString().equals(r.toString());
}

OS的I/O控制方式

程序直接控制方式

  • 在程序直接控制方式中,CPU会直接处理 I/O 操作,数据传输等都由 CPU 负责。在这种方式下,CPU 必须不断地轮询 I/O 设备状态,这会导致大量的 CPU 时间用于轮询。

程序直接控制方式虽然简单且易于实现,但其缺点也显而易见,由于CPU 和I/0 设备只能串行工作,导致CPU 的利用率相当低

中断驱动方式

中断驱动方式通过 CPU 的中断机制来实现。当 I/O 设备完成一个操作时,会发送一个中断信号给 CPU,中断处理程序会被调用来处理这个事件。

这种方式避免了程序不断地轮询设备状态,提高了 CPU 的利用率。

中断驱动方式比程序直接控制方式有效,但由于数据中的每个字在存储器与I/0 控制器之间的传输都必须经过CPU, 这就导致了中断驱动方式仍然会消耗较多的CPU 时间。

DMA

Direct Memory Access : 直接内存访问

在 DMA 方式中,数据传输无需 CPU 的直接干预,而是由专门的 DMA 控制器来完成,这意味着 CPU 可以继续执行其他任务

DMA 控制器可以直接和设备和内存进行数据传输,减轻了 CPU 的负担,提高了系统的效率。

通道控制方式

通道控制方式是在主控制器和 I/O 设备之间引入了专门的通道控制器,它可以独立地执行 I/O 操作,从而将 I/O 操作和 CPU 的执行分开,提高了系统的 I/O 处理能力。这种方式多用于一些高速、高性能的设备上。

I/O通道方式是DMA方式的发展,它可以进一步 减少CPU的干预,即把对一个数据块的读(或写)为单位的干预,减少为对一组数据块的读(或写)及有关控制和管理为单位的干预。同时,又可以实现CPU、通道和I/0 设备三者的并行操作,从而更有效地提高整个系统的资源利用率。

两者的关系

结合上面的OS的四种I/O控制方式

方式 是否需要CPU干预
程序直接控制 需要
中断驱动控制 需要
DMA 较少
I/O通道 极少

Java 的 NIO 主要基于非阻塞 I/O 和 I/O 多路复用机制来实现,同于与操作系统的 I/O 控制方式存在一些联系:

  1. DMA 方式:Java 的 NIO 技术使用了直接内存访问(Direct Memory Access,DMA)的方式来进行数据传输。通过 ByteBuffer 类的 allocateDirect() 方法可以直接在内存中分配直接缓冲区,这种缓冲区的数据不受垃圾回收器的管理,可以直接通过 DMA 传输数据,这提供了在非阻塞 I/O 操作中更快速的数据传输。
  2. 通道控制方式:NIO 中的 Channel 可以看作是这些 I/O 控制方式的一种抽象。Channel 的引入使得可以在一个线程中管理多个 I/O 操作,并且 Selector 实现了通道控制(Channel control)方式,能够通过单一的线程处理多个通道(Channel)的 I/O 操作,实现了 I/O 多路复用。

到这里, 通过NIO来提高CPU利用率的原理也就显而易见了, 至于AI/O , I/O多路复用等技术 , 实际上也是依赖于DMA和通道控制方式。

对于最开始的问题, 我们也可以做出基本的解答: 执行I/O操作的是 DMA 等组件 , 并且不会占用进程/线程的时间片(实际上时间片是CPU占用时间 , 这里基本上没有使用CPU, 也不存在占用时间片的说法了)

时刻记住计算机的分层:

reference