11.5 零拷贝

一句话

传统 read + write 要把同一份数据拷四次(磁盘→页缓存→用户缓冲→socket 缓冲→网卡),零拷贝把中间两次用户态拷贝与两次 syscall 省掉——它优化的是 CPU 与内存带宽,不是磁盘。

四次拷贝 vs 两次

路径 拷贝次数 syscall 说明
read + write 4 2 CPU 参与两次用户态拷贝
mmap + write 3 2 省一次拷贝,仍有缺页与映射开销
sendfile 2 1 数据不进用户态,DMA 直接送 socket(可用 SG-DMA 再省一次)
splice(管道) 2 2 适合代理/转发
// 文件服务器:从 fd 直接送到 socket,不经过用户态
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

适用与不适用

场景 用零拷贝? 原因
静态文件服务、视频分发 ✅ 数据不需要修改
消息队列(Kafka 落盘转网络) ✅ 顺序读写 + 发送原样字节
需要压缩/加密/改写内容 ❌ 必须进用户态处理后才能发
小文件(几 KB) ⚠️ 省下的拷贝不值得额外复杂度
需要校验或分片重组 ❌ 逻辑在用户态

IO 模型的分层见 11.3 文件系统与 IO 模型 (阻塞 / 非阻塞 / 异步);事件驱动框架如何用它见 11.4 epoll 与 IO 多路复用、8.10 反向代理与转发策略。

落到数字:四种路径的拷贝

路径 拷贝次数 syscall 用户态 CPU
read + write 4 2 2 次内存拷贝
mmap + write 3 2 1 次 + 缺页
sendfile 2 1 0
sendfile + SG-DMA 1–2(仅描述符) 1 0
# 验证是否走了 sendfile(Nginx 静态文件)
strace -f -e trace=sendfile,read,write -p <nginx-worker-pid> 2>&1 | head -5

反模式

反模式 后果
为了 “零拷贝” 关掉压缩 带宽成本暴涨,CPU 反而更闲
在小对象上追求零拷贝 复杂度上升,收益在噪声里
用 mmap 处理会被截断的文件 SIGBUS 崩溃
忽略 TLS:加密链路必须进用户态 “零拷贝” 在 HTTPS 上通常不成立(kTLS 例外)

三条铁律

🔑 零拷贝省的是 CPU 与内存带宽,不是磁盘 IO。

🔑 数据要改就不能零拷贝;压缩、加密、改写都会强制经过用户态。

🔑 大文件、原样转发才是它的主场。

自检

Q1 为什么 sendfile 能把 4 次拷贝降到 2 次?

▸ 答案:因为它让数据不进用户态:内核从 page cache 直接组装到 socket 缓冲(支持 SG-DMA 时连这一次也省掉,只传描述符)。省掉的是 “内核→用户→内核” 的两次 CPU 拷贝,以及一次 syscall。

Q2 一个图片服务开了 gzip 压缩,还能用零拷贝吗?

▸ 答案:不能(除非在文件生成时就压缩好)。压缩必须把数据读进用户态处理,零拷贝的前提被破坏。可行方案是 “预压缩 + 按 Accept-Encoding 选文件”,让发送阶段仍是原样字节流。

Q3 Kafka 为什么能受益于零拷贝?

▸ 答案:因为它的读写模式是 “顺序追加 + 原样发送”:消息落盘后,消费者拉取时直接从 page cache 送到 socket,不需要解析或改写消息体。这也是 Kafka 高吞吐的原因之一(配合批量与顺序 IO)。

Q4 小文件用零拷贝为什么收益不明显?

▸ 答案:因为小文件的瓶颈在元数据与 syscall 次数,而不是拷贝带宽:几 KB 的拷贝只占微秒级,而路径解析、打开文件、连接建立才是主要成本。合并小文件、减少请求数比零拷贝更有效。

Q5 HTTPS 下零拷贝为什么通常不成立?

▸ 答案:因为 TLS 需要把明文加密成密文,数据必须经过用户态(或内核 TLS,即 kTLS)。没有 kTLS 时,sendfile 只能发送已加密的静态文件,动态加密场景无法使用。这也是 “静态资源用 CDN 卸载 TLS” 的一个理由。

Q6 为什么 sendfile 在需要计算 Content-Length 或做范围请求时不一定最省事?

▸ 答案:因为它把数据直接推给 socket,应用层拿不到内容做分片/校验;Range 请求要由框架先 stat 大小再 sendfile 指定偏移。功能复杂的场景往往退回 mmap 或 read,用可控性换一点 CPU。

关联