「阅读笔记」零拷贝

原文链接:zhuanlan.zhihu.com/p/258513662

演进历程

1. IO 中断

整个数据的传输过程,都需要 CPU 亲自参与搬运数据的过程,而且这个过程,CPU 是不能做其他事情的。

2. DMA 直接内存访问

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

3. 传统 IO

4 次上下文切换+4 次数据拷贝

内核缓冲区=磁盘高速缓存(PageCache

  1. 缓存最近被访问的数据:通过 DMA 把磁盘里的数据搬运到内存里,这样就可以用读内存替换读磁盘;
  2. 预读功能:

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() 系统调用");
    }
}
相关推荐
涡能增压发动积38 分钟前
Browser-Use Agent使用初体验
人工智能·后端·python
探索java1 小时前
Spring lookup-method实现原理深度解析
java·后端·spring
lxsy1 小时前
spring-ai-alibaba 之 graph 槽点
java·后端·spring·吐槽·ai-alibaba
码事漫谈1 小时前
深入解析线程同步中WaitForSingleObject的超时问题
后端
码事漫谈1 小时前
C++多线程同步:深入理解互斥量与事件机制
后端
少女孤岛鹿2 小时前
微服务注册中心详解:Eureka vs Nacos,原理与实践 | 一站式掌握服务注册、发现与负载均衡
后端
CodeSaku2 小时前
是设计模式,我们有救了!!!(四、原型模式)
后端
二闹2 小时前
什么?你的 SQL 索引可能白加了!?
后端·mysql·性能优化
lichenyang4532 小时前
基于Express+Ejs实现带登录认证的多模块增删改查后台管理系统
后端