零拷贝
含义
零拷贝(Zero-Copy) 的核心含义是:
在数据从存储介质(如磁盘)→ 内核缓冲区 → 用户空间 → 内核socket缓冲区 → 网卡的传输路径中,尽量避免或完全消除由CPU执行的内存拷贝(memcpy)操作,把数据搬运的任务尽可能交给DMA(直接内存访问)硬件来完成,从而大幅降低CPU开销、上下文切换次数和内存带宽占用。
零拷贝 不等于 完全没有发生任何拷贝,其实更准确的说法是“零CPU拷贝” 或 “避免不必要的用户态↔内核态拷贝”。
方式
传统 read + write 方式(4次拷贝 + 4次上下文切换)
1
2
3
4
磁盘 → DMA → 内核Page Cache (拷贝1:DMA)
内核Page Cache → 用户缓冲区 (拷贝2:CPU)
用户缓冲区 → 内核socket缓冲区 (拷贝3:CPU)
内核socket缓冲区 → 网卡 (拷贝4:DMA)
上下文切换:read → write → read → write(共4次)
目前Linux上最常用的几种“零拷贝”方式对比
| 方式 | 系统调用 | CPU拷贝次数 | DMA拷贝次数 | 上下文切换次数 | 典型适用场景 | 备注 |
|---|---|---|---|---|---|---|
| 传统 read+write | read + write | 2 | 2 | 4 | 通用,但低效 | 基准对比 |
| mmap + write | mmap + write | 1 | 2 | 4 | 需要对文件内容做少量修改的场景 | 内存映射,适合随机读写 |
| sendfile(普通) | sendfile | 1 | 2 | 2 | 静态文件服务(Nginx经典) | Linux 2.2+ |
| sendfile + SG-DMA(收集式) | sendfile | 0 | 2 | 2 | 最理想的静态文件传输 | 需要网卡支持DMA Scatter-Gather,主流网卡基本都支持 |
| splice / tee | splice / vmsplice | 0 | 2(管道间) | 2 | 管道间零拷贝、代理转发 | 内核2.6.17+,不能跨主机 |
| MSG_ZEROCOPY | setsockopt + send(MSG_ZEROCOPY) | 0 | — | 减少 | 用户态缓冲区直接发网络(大包) | 内核4.14+ TCP,5.0+ UDP,适合10KB+大包 |
| io_uring + 注册缓冲区 | io_uring + IORING_OP_提供缓冲区 | 0 | — | 大幅减少 | 高性能网络服务器、数据库 | 目前最激进的异步零拷贝方案 |
最常用的三种零拷贝方式简要原理
- mmap + write(内存映射方式)
1
2
3
4
5
磁盘 → DMA → Page Cache
↑
mmap 映射(共享同一物理页)
↓
用户进程直接读写这块内存 → write(内核直接从Page Cache取) → socket缓冲区 → DMA → 网卡
- 省掉了一次用户缓冲区 ←→ 内核缓冲区的CPU拷贝
- 但仍然有一次内核 → socket缓冲区的CPU拷贝
- sendfile(Linux静态文件发送神器)
普通sendfile(2.4以前或没开启SG-DMA):
磁盘 → DMA → Page Cache → CPU拷贝 → socket缓冲区 → DMA → 网卡
sendfile + DMA Scatter-Gather(现代主流):
磁盘 → DMA → Page Cache 内核只把文件偏移 + 长度信息传给网卡驱动 网卡驱动通过DMA Scatter-Gather直接从Page Cache读取数据发出去
→ 真正全程零CPU拷贝,只有2次DMA
- splice / vmsplice(管道零拷贝)
用于两个文件描述符之间(或用户缓冲区与管道)零拷贝移动数据,常用于:
- nginx proxy
- redis AOF rewrite
- 数据中转不需解析内容的场景