Skip to content

零拷贝技术

你有没有想过这个问题:

文件服务器传输一个大文件,传统方式需要几次数据拷贝?

答案是 4 次。而且还有 2 次上下文切换(用户态↔内核态)。

零拷贝技术把拷贝次数减少到 2 次,上下文切换减少到 1 次。

传统拷贝路径

假设要把文件从磁盘传输到网卡(Socket),传统方式需要:

应用进程

    ▼ read()
内核缓冲区 ←─────── 磁盘

    ▼ copy_to_user()
用户缓冲区

    ▼ write()
Socket 缓冲区 ←─── 内核缓冲区


网卡

4 次拷贝 + 2 次上下文切换

  1. 磁盘 → 内核缓冲区(DMA 拷贝)
  2. 内核缓冲区 → 用户缓冲区(CPU 拷贝)
  3. 用户缓冲区 → 内核缓冲区(CPU 拷贝)
  4. 内核缓冲区 → 网卡(DMA 拷贝)
  5. 用户态 → 内核态(read)
  6. 内核态 → 用户态(read 返回)

sendfile 零拷贝

Linux 的 sendfile 系统调用跳过了用户空间,数据直接从磁盘到网卡:

应用进程

    ▼ sendfile()
磁盘 ──────────────────────────── 内核缓冲区
    │                                   │
    │                                   ▼
    └─────────────────────────────→ 网卡

2 次拷贝 + 1 次上下文切换

  1. 磁盘 → 内核缓冲区(DMA 拷贝)
  2. 内核缓冲区 → 网卡(DMA 拷贝)
  3. 用户态 → 内核态(sendfile)

Java 实现:transferTo()

FileChannel.transferTo() 底层调用 Linux 的 sendfile

java
public static void copyFileZeroCopy(String src, String dst) throws IOException {
    try (
        FileChannel in = new FileInputStream(src).getChannel();
        FileChannel out = new FileOutputStream(dst).getChannel()
    ) {
        long size = in.size();
        long transferred = 0;
        while (transferred < size) {
            transferred += in.transferTo(
                transferred,
                size - transferred,
                out
            );
        }
    }
}

transferTo() 会尽可能使用零拷贝。如果目标 Channel 不支持,会 fallback 到普通拷贝。

性能对比

方式拷贝次数上下文切换适用场景
传统 read/write4 次2 次小文件
sendfile 零拷贝2 次1 次大文件传输

测试数据(来自网络综合测试):

  • 1GB 文件传输,传统方式约 10 秒
  • 1GB 文件传输,零拷贝约 2 秒
  • 性能提升约 5 倍

适用场景

  • 文件服务器:静态资源传输
  • 日志收集:日志文件传输到 Kafka
  • 音视频服务:大文件流媒体
┌─────────────────────────────────────────────────────────────────┐
│  零拷贝的本质:跳过用户空间,数据直接在内核空间流动                 │
│                                                                 │
│  适用:大文件传输(文件服务器、日志收集、音视频)                  │
│  不适用:小文件或需要应用层处理的场景                             │
└─────────────────────────────────────────────────────────────────┘

注意事项

  1. transferTo() 会自动 fallback:如果目标 Channel 不支持零拷贝,会用普通方式
  2. 不适合小文件:小文件的系统调用开销占比低,零拷贝优势不明显
  3. Java 层面无法强制零拷贝:取决于底层 Channel 是否支持

基于 VitePress 构建