当调用一次 channel**.read** 或 stream**.read** 后,会由用户态切换至操作系统内核态来完成真正数据读取,而读取又分为两个阶段,分别为:
根据 UNIX 网络编程 - 卷 I,IO 模型主要有以下几种
用户线程
在一个循环中一直调用 read 方法,若内核空间中还没有数据可读,立即返回
用户线程发现内核空间中有数据后,等待内核空间执行复制数据,待复制结束后返回结果
Java 中通过 Selector 实现多路复用
多路复用与阻塞IO的区别
零拷贝指的是数据无需拷贝到 JVM 内存中,同时具有以下三个优点
传统 IO 问题
传统的 IO 将一个文件通过 socket 写出
File f = new File("helloword/data.txt");
RandomAccessFile file = new RandomAccessFile(file, "r");
byte[] buf = new byte[(int)f.length()];
file.read(buf);
Socket socket = ...;
socket.getOutputStream().write(buf);
内部工作流如下
Java 本身并不具备 IO 读写能力,因此 read 方法调用后,要从 Java 程序的用户态切换至内核态,去调用操作系统(Kernel)的读能力,将数据读入内核缓冲区。这期间用户线程阻塞,操作系统使用 DMA(Direct Memory Access)来实现文件读,其间也不会使用 CPU
DMA 也可以理解为硬件单元,用来解放 cpu 完成文件 IO
从内核态切换回用户态,将数据从内核缓冲区读入用户缓冲区(即 byte [] buf),这期间 CPU 会参与拷贝,无法利用 DMA
调用 write 方法,这时将数据从**用户缓冲区(byte [] buf)**写入 socket 缓冲区,CPU 会参与拷贝
接下来要向网卡写数据,这项能力 Java 又不具备,因此又得从用户态切换至内核态,调用操作系统的写能力,使用 DMA 将 socket 缓冲区的数据写入网卡,不会使用 CPU
可以看到中间环节较多,java 的 IO 实际不是物理设备级别的读写,而是缓存的复制,底层的真正读写是操作系统来完成的
通过 DirectByteBuf
大部分步骤与优化前相同,唯有一点**:Java 可以使用 DirectByteBuffer 将堆外内存映射到 JVM 内存中来直接访问使用**
以下两种方式都是零拷贝,即无需将数据拷贝到用户缓冲区中(JVM 内存中)
底层采用了 linux 2.1 后提供的 sendFile 方法,Java 中对应着两个 channel 调用 transferTo/transferFrom 方法拷贝数据
这种方法下
linux 2.4 对上述方法再次进行了优化
整个过程仅只发生了 1 次用户态与内核态的切换,数据拷贝了 2 次
AIO 用来解决数据复制阶段的阻塞问题
异步模型需要底层操作系统(Kernel)提供支持
- Windows 系统通过 IOCP 实现了真正的异步 IO
- Linux 系统异步 IO 在 2.6 版本引入,但其底层实现还是用多路复用模拟了异步 IO,性能没有优势
本文由
传智教育博学谷
教研团队发布。如果本文对您有帮助,欢迎
关注
和点赞
;如果您有任何建议也可留言评论
或私信
,您的支持是我坚持创作的动力。转载请注明出处!
|