零拷贝技术
你有没有想过这个问题:
文件服务器传输一个大文件,传统方式需要几次数据拷贝?
答案是 4 次。而且还有 2 次上下文切换(用户态↔内核态)。
零拷贝技术把拷贝次数减少到 2 次,上下文切换减少到 1 次。
传统拷贝路径
假设要把文件从磁盘传输到网卡(Socket),传统方式需要:
应用进程
│
▼ read()
内核缓冲区 ←─────── 磁盘
│
▼ copy_to_user()
用户缓冲区
│
▼ write()
Socket 缓冲区 ←─── 内核缓冲区
│
▼
网卡4 次拷贝 + 2 次上下文切换:
- 磁盘 → 内核缓冲区(DMA 拷贝)
- 内核缓冲区 → 用户缓冲区(CPU 拷贝)
- 用户缓冲区 → 内核缓冲区(CPU 拷贝)
- 内核缓冲区 → 网卡(DMA 拷贝)
- 用户态 → 内核态(read)
- 内核态 → 用户态(read 返回)
sendfile 零拷贝
Linux 的 sendfile 系统调用跳过了用户空间,数据直接从磁盘到网卡:
应用进程
│
▼ sendfile()
磁盘 ──────────────────────────── 内核缓冲区
│ │
│ ▼
└─────────────────────────────→ 网卡2 次拷贝 + 1 次上下文切换:
- 磁盘 → 内核缓冲区(DMA 拷贝)
- 内核缓冲区 → 网卡(DMA 拷贝)
- 用户态 → 内核态(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/write | 4 次 | 2 次 | 小文件 |
| sendfile 零拷贝 | 2 次 | 1 次 | 大文件传输 |
测试数据(来自网络综合测试):
- 1GB 文件传输,传统方式约 10 秒
- 1GB 文件传输,零拷贝约 2 秒
- 性能提升约 5 倍
适用场景
- 文件服务器:静态资源传输
- 日志收集:日志文件传输到 Kafka
- 音视频服务:大文件流媒体
┌─────────────────────────────────────────────────────────────────┐
│ 零拷贝的本质:跳过用户空间,数据直接在内核空间流动 │
│ │
│ 适用:大文件传输(文件服务器、日志收集、音视频) │
│ 不适用:小文件或需要应用层处理的场景 │
└─────────────────────────────────────────────────────────────────┘注意事项
- transferTo() 会自动 fallback:如果目标 Channel 不支持零拷贝,会用普通方式
- 不适合小文件:小文件的系统调用开销占比低,零拷贝优势不明显
- Java 层面无法强制零拷贝:取决于底层 Channel 是否支持
