原文链接:zhuanlan.zhihu.com/p/258513662
演进历程
1. IO 中断
整个数据的传输过程,都需要 CPU 亲自参与搬运数据的过程,而且这个过程,CPU 是不能做其他事情的。

2. DMA 直接内存访问
在进行 I/O 设备和内存的数据传输的时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务。

3. 传统 IO
4 次上下文切换+4 次数据拷贝

内核缓冲区=磁盘高速缓存(PageCache):
- 缓存最近被访问的数据:通过 DMA 把磁盘里的数据搬运到内存里,这样就可以用读内存替换读磁盘;
- 预读功能:
4. 零拷贝
mmap + write:4 次上下文切换+3 次数据拷贝 sendfile:2 次上下文切换+3 次数据拷贝

Demo
mmap
mmap是在页表中建立虚拟地址到文件的映射关系,不是将地址存储到物理内存中。
mmap
/**
* 零拷贝读取文件内容 - 使用mmap内存映射
*
* 底层原理:
* 1. Java的FileChannel.map()方法底层调用操作系统的mmap()系统调用
* 2. mmap将文件直接映射到进程的虚拟内存地址空间
* 3. 文件内容通过虚拟内存管理系统按需加载(延迟加载)
* 4. 访问映射内存时触发页面错误(page fault),内核自动加载对应的文件页面
*
* 内存映射过程:
* 1. mmap()创建虚拟内存映射区域
* 2. 建立虚拟地址到文件偏移的映射关系
* 3. 首次访问时触发缺页中断
* 4. 内核将文件页面加载到物理内存
* 5. 更新页表,建立虚拟地址到物理地址的映射
*
* 与传统read()对比:
* - 传统方式:read() -> 内核缓冲区 -> 用户缓冲区 (数据拷贝)
* - mmap方式:直接访问映射内存 -> 页面错误 -> 内核加载页面 (无数据拷贝)
*
*
* 适用场景:
* - 大文件随机访问
* - 文件内容需要多次访问
* - 内存充足的环境
*/
private static void zeroCopyReadFile() throws IOException {
String fileName = "source.txt";
long startTime = System.currentTimeMillis();
try (FileChannel channel = FileChannel.open(Paths.get(fileName), StandardOpenOption.READ)) {
long fileSize = channel.size();
// 使用mmap内存映射进行零拷贝读取
// 底层调用mmap(addr, length, prot, flags, fd, offset)系统调用
// 参数说明:
// - MapMode.READ_ONLY: 只读映射 (PROT_READ)
// - 0: 映射起始偏移量
// - fileSize: 映射长度
// 返回MappedByteBuffer,代表映射的内存区域
ByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, fileSize);
// 从映射内存读取文件内容
// 这里的读取操作实际上是内存访问,可能触发页面错误
// 内核会自动将对应的文件页面加载到物理内存中
byte[] data = new byte[(int) fileSize];
buffer.get(data); // 内存拷贝操作,从映射内存到Java堆内存
String content = new String(data, "UTF-8");
long endTime = System.currentTimeMillis();
System.out.println("\n零拷贝读取文件内容 (mmap内存映射):");
System.out.println(" 文件名: " + fileName);
System.out.println(" 文件大小: " + fileSize + " bytes");
System.out.println(" 耗时: " + (endTime - startTime) + "ms");
System.out.println(" 底层实现: mmap() 系统调用");
System.out.println(" 映射类型: 只读内存映射 (PROT_READ)");
// 注意:虽然叫零拷贝,但buffer.get(data)这一步仍然涉及内存拷贝
// 真正的零拷贝是文件到映射内存的过程,通过页面错误机制实现
System.out.println(" 说明: 文件到映射内存为零拷贝,映射内存到Java数组仍需拷贝");
// System.out.println(" 内容预览:");
// System.out.println(" " + content.replace("\n", "\n "));
}
}
sendfile
sendfile
/**
* 零拷贝文件传输 - 使用sendfile系统调用
*
* 底层原理:
* 1. Java的FileChannel.transferTo()方法底层调用操作系统的sendfile()系统调用
* 2. sendfile()直接在内核空间完成文件到文件的数据传输
* 3. 数据流向:磁盘 -> 内核缓冲区 -> 目标文件,完全绕过用户空间
* 4. 避免了传统方式的4次数据拷贝:
* 传统方式:磁盘 -> 内核缓冲区 -> 用户缓冲区 -> 内核缓冲区 -> 目标文件
* 零拷贝: 磁盘 -> 内核缓冲区 -> 目标文件
*
* 系统调用对比:
* - 传统方式:read() + write() 多次系统调用
* - 零拷贝: sendfile() 单次系统调用
*
* 性能优势:
* - 减少CPU使用率(无需处理用户空间数据)
* - 降低内存带宽占用
* - 减少上下文切换次数
* - 提高大文件传输效率
*/
private static void zeroCopyFileTransfer() throws IOException {
String sourceFile = "source.txt";
String targetFile = "target_zerocopy.txt";
long startTime = System.currentTimeMillis();
try (FileChannel sourceChannel = FileChannel.open(Paths.get(sourceFile), StandardOpenOption.READ);
FileChannel targetChannel = FileChannel.open(Paths.get(targetFile),
StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
long fileSize = sourceChannel.size();
// 零拷贝传输 - 底层调用sendfile(out_fd, in_fd, offset, count)系统调用
// 参数说明:
// - 0: 起始偏移量
// - fileSize: 传输字节数
// - targetChannel: 目标文件描述符
// 数据直接在内核空间从源文件传输到目标文件,不经过用户空间
long transferred = sourceChannel.transferTo(0, fileSize, targetChannel);
long endTime = System.currentTimeMillis();
System.out.println("零拷贝文件传输完成 (sendfile系统调用):");
System.out.println(" 源文件: " + sourceFile);
System.out.println(" 目标文件: " + targetFile);
System.out.println(" 传输字节数: " + transferred);
System.out.println(" 耗时: " + (endTime - startTime) + "ms");
System.out.println(" 底层实现: sendfile() 系统调用");
}
}