什么是零拷贝?
零拷贝(zero-copy)指在内核与用户态之间不重复搬运数据,减少 CPU 拷贝次数和上下文切换次数。 严格来说它并不能做到"一次都不拷",只是把拷贝从 CPU 手里交给 DMA(Direct Memory Access), 让写网卡/写磁盘的数据通路直接从页缓存流向设备。
1. 传统 read + write 有多贵
把文件发到 socket,最朴素的写法是:
n = read(fd, buf, len); // 磁盘 → 页缓存(DMA)→ 用户 buf(CPU 拷)
write(sock, buf, len); // 用户 buf → socket 缓冲区(CPU 拷)→ 网卡(DMA)
磁盘 ──DMA──► 页缓存 ──CPU──► 用户缓冲区 ──CPU──► socket 缓冲区 ──DMA──► 网卡
① ② ③ ④
2 次系统调用 = 4 次上下文切换(用户态/内核态来回)
4 次数据拷贝 = 2 次 DMA + 2 次 CPU 拷贝
数据明明只需要"从磁盘到网卡",却被迫在用户态绕了一圈。
2. 三种零拷贝方案
| 方案 | 系统调用 | 数据拷贝 | 适用场景 | 限制 |
|---|---|---|---|---|
mmap + write | 2 次 | 2 次 DMA + 1 次 CPU | 需要对数据做少量处理 | 缺页开销、文件截断信号 |
sendfile | 1 次 | 2 次 DMA + 1 次 CPU | 纯转发(静态文件、代理) | 用户态看不到数据,不能改内容 |
sendfile + SG-DMA | 1 次 | 2 次 DMA,0 次 CPU | 大文件传输 | 需网卡支持 scatter-gather |
splice | 1 次 | 管道作为中转,0 次 CPU | 内核内数据搬运 | 必须有一端是管道 |
sendfile 路径(现代网卡 + SG-DMA):
磁盘 ──DMA──► 页缓存 ────────────► 网卡(带文件描述符,DMA 直接读)
└── 内核只传"偏移+长度",CPU 不碰数据
3. 代码
// Go 的 io.Copy 在源是 *os.File、目标是 *net.TCPConn 时会自动走 sendfile
f, _ := os.Open("/data/big.bin")
defer f.Close()
conn, _ := net.Dial("tcp", "10.0.0.8:9000")
defer conn.Close()
io.Copy(conn, f) // 触发 TCPConn.ReadFrom → sendfile(2)
// Java:FileChannel.transferTo 底层就是 sendfile
try (FileChannel in = FileChannel.open(Path.of("/data/big.bin"));
SocketChannel out = SocketChannel.open(new InetSocketAddress("10.0.0.8", 9000))) {
long pos = 0, size = in.size();
while (pos < size) pos += in.transferTo(pos, size - pos, out);
}
# Nginx 静态文件服务:开启 sendfile 与高效写回
sendfile on;
tcp_nopush on; # 配合 sendfile 攒满一个 MSS 再发
tcp_nodelay on; # 长连接上立即发小包
4. 常见追问
Q:零拷贝真的"零"吗? 不是。一定还有 DMA 拷贝(磁盘/网卡与内存之间),零拷贝的"零"指的是不经过用户态、不做多余的 CPU 拷贝。 SG-DMA 之后 CPU 完全不参与数据搬运,只传元数据。
Q:mmap 和 sendfile 怎么选?
需要在用户态改内容(加密、压缩、加头)就只能 mmap 或普通 read;纯转发用 sendfile 最省。
mmap 还有缺页中断和 SIGBUS 的风险,小文件收益不明显。
Q:Kafka 为什么快?靠零拷贝吗?
一部分是。Kafka 消费时用 sendfile 把日志段直接发给消费者,配合页缓存与顺序写,省掉两次拷贝。
但如果开启了消息压缩或 TLS 加密,数据必须回到用户态处理,零拷贝就失效了。
Q:HTTPS 还能用零拷贝吗?
传统上不能:TLS 加密必须在用户态(或内核 TLS 模块)改写数据。有了 kTLS(kernel TLS)后,
加密下沉到内核,sendfile 仍可继续使用;否则只能退回 read + write。
小结
零拷贝的本质是让数据少绕路:用 sendfile/splice 把"页缓存 → 网卡"这一跳交给 DMA,
省掉两次 CPU 拷贝与两次上下文切换。它适合静态文件、代理转发、消息队列这类"不看内容只搬运"的场景;
一旦要改数据(加密、压缩、组装协议头),就得回到用户态。