一句话概括:零拷贝技术让数据在 磁盘 → 内核缓冲区 → 网卡 之间直接传输,避免用户态与内核态之间的冗余数据拷贝,核心是消除 CPU 参与的数据搬移。
传统 IO 路径(4 次拷贝,2 次 CPU 参与)
| |
- ① DMA 读磁盘到 page cache
- ② CPU 把 page cache 拷到用户态 buffer(read)
- ③ CPU 把用户态 buffer 拷到 socket buffer(send)
- ④ DMA 从 socket buffer 发送到网卡
零拷贝的目标:去掉 ② 和 ③ 这两步 CPU 拷贝。
1. sendfile(最常用,静态文件服务)
| |
路径:
| |
底层: Page Cache 中的页面直接通过 scatter-gather DMA 发送到网卡。内核只需要把 page 指针链入 skb 的 frag 数组,DMA 引擎直接读取。CPU 不碰数据,只传元数据(指针、长度)。
限制:
- 数据必须从文件到 socket(不支持 socket→socket 或文件→文件)
- out_fd 必须支持 DMA 发送(通常只有 TCP socket)
- in_fd 必须是支持 splice_read 的文件(普通文件、块设备)
2. splice(通用 pipe 搬运,任意 fd 之间)
| |
路径:
| |
底层: splice 将 fd_in 的 page 引用直接挂到 pipe 的 struct pipe_buffer 上,再从 pipe 挂到 fd_out 的 skb 上。数据始终在内核 page cache 里不动,只移动了引用计数。
sendfile vs splice:
- sendfile 是 splice 的特化(splice 内部实现就是 pipe + 两个 splice 调用)
- splice 可以连接任意两个 fd(文件 → socket、socket → socket、pipe → 任何),而 sendfile 只支持文件 → socket
3. mmap + write(用户态直接操作 page cache)
| |
路径:
| |
底层: mmap 将 page cache 的物理页直接映射到用户地址空间,用户 write 时内核直接从映射页构造 skb->frags,无需 copy_from_user。
问题:
- 多线程并发写同一个 mmap 区域需加锁
- 文件长度变化、截断时的 SIGBUS 处理复杂
- 脏页回写时机不可控
4. SO_ZEROCOPY(Linux 4.14+,用户态直接发零拷贝)
| |
路径:
| |
底层:
- 内核调用
pin_user_pages固定用户缓冲区物理页(防止被 swap 出去) - 将用户页地址填入 skb frags,DMA 引擎直接读取用户态内存
- 发送完成通过
recvmsg返回SO_EE_ORIGIN_ZEROCOPY通知用户释放
去掉了一次内核态到用户态的拷贝(copy_from_user),但需要 pin/unpin 开销。
适用场景: 大块数据发送(>10KB),小包反而更慢(pin/unpin 开销 > 直接拷贝)
5. io_uring + 注册缓冲区(Linux 5.1+)
| |
路径:
| |
底层: io_uring_register_buf_ring 一次 pin_user_pages 后返回 buffer index,后续所有 IO 请求直接通过 index 引用 buffer,无需再 pin/unpin。避免了每次 IO 的页锁定开销,同时去掉 CPU 拷贝。
6. RDMA(InfiniBand / RoCE / iWARP,硬件级零拷贝)
| |
路径:
| |
底层: 网卡通过 ibv_reg_mr 获取用户虚拟地址 → 物理页的映射表,远端网卡直接写入本端物理页,旁路内核,零 CPU。本端应用直接看到数据,不经过内核 socket buffer。
代价:
- 需要 RDMA 硬件(InfiniBand 交换机/网卡或支持 RoCE 的以太网卡)
- 内存注册(
ibv_reg_mr)开销大(页表遍历 + pin pages),需池化复用
7. DPDK(用户态驱动,完全绕过内核)
| |
网卡直接把数据 DMA 到用户态预先分配的 hugepage 里。没有系统调用,没有上下文切换,没有内核数据结构。
但代价是应用层必须实现 TCP 协议栈(如 mTCP、F-Stack)。
各场景推荐
| 场景 | 推荐技术 |
|---|---|
| Web 服务器静态文件 | sendfile |
| 反向代理/网关转发 | splice |
| 大块数据发送(视频服务) | SO_ZEROCOPY |
| 高性能存储 + 网络 | io_uring + registered buffers |
| 超低延迟(< 10us) | RDMA |
| 纯包转发(> 100Gbps) | DPDK |
注意: 零拷贝不是银弹。小包场景下,CPU 拷贝的延迟远小于 pin/unpin 或上下文切换的开销,传统路径反而更快。Nginx 对 < 16KB 的文件直接用 read + write,超过才用 sendfile——这也是一种工程权衡。