什么是零拷贝?

#计算机网络#操作系统#面试八股

什么是零拷贝?

零拷贝(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 + write2 次2 次 DMA + 1 次 CPU需要对数据做少量处理缺页开销、文件截断信号
sendfile1 次2 次 DMA + 1 次 CPU纯转发(静态文件、代理)用户态看不到数据,不能改内容
sendfile + SG-DMA1 次2 次 DMA,0 次 CPU大文件传输需网卡支持 scatter-gather
splice1 次管道作为中转,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 拷贝与两次上下文切换。它适合静态文件、代理转发、消息队列这类"不看内容只搬运"的场景; 一旦要改数据(加密、压缩、组装协议头),就得回到用户态。

笔记加载中…