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。
关联
- 前置:11.3 文件系统与 IO 模型 (阻塞 / 非阻塞 / 异步)
- 并列:11.4 epoll 与 IO 多路复用、8.10 反向代理与转发策略
- 下游:9.1 边缘缓存原理、43.8 产品 (Kafka / RocketMQ / RabbitMQ / NATS)