零拷贝并非不拷贝,而是避免CPU参与数据搬运,仅由DMA完成最多2次拷贝,上下文切换降至1–2次;核心是绕过用户空间、复用PageCache、依赖硬件DMA支持。

Linux socket 零拷贝不是“不拷贝”,而是让 CPU 躲开数据搬运——只留 DMA 硬件干活,把原本 4 次拷贝压到最多 2 次,上下文切换从 4 次降到 1–2 次。真正起效的关键,是绕过用户空间、复用内核页缓存、依赖硬件 DMA 支持。
零拷贝为什么能快?看懂传统路径的浪费
普通 read() + write() 发文件时,数据实际走的是:
- 磁盘 → 内核页缓存(DMA,不耗 CPU)
- 内核页缓存 → 用户缓冲区(CPU 拷贝,1 次)
- 用户缓冲区 → socket 缓冲区(CPU 拷贝,2 次)
- socket 缓冲区 → 网卡(DMA)
两次 CPU 拷贝 + 四次上下文切换,100MB 文件可能多花 50ms、多占 20%+ CPU。零拷贝的目标就是砍掉那两段 CPU 参与的搬运。
sendfile:最常用、最直接的 socket 零拷贝方案
适用于“原样发文件”场景(如静态资源服务),全程在内核态完成:
- 调用
sendfile(out_fd, in_fd, &offset, count),一个系统调用搞定 - 数据从文件页缓存直送 socket 缓冲区,不经过用户内存
- 基础版:2 次拷贝(磁盘→页缓存→网卡)、2 次上下文切换、0 次 CPU 拷贝
- 若网卡支持 DMA gather,可跳过 socket 缓冲区,实现真正 0 CPU 拷贝
注意:in_fd 必须是普通文件(支持 mmap),out_fd 必须是 socket;不支持加密、压缩等用户态处理。
mmap + write:适合需要简单加工的场景
当你得读文件、改点内容(比如加 header)、再发出去,又不想白拷一次到用户空间:
- 用
mmap()把文件映射进用户虚拟地址,底层仍指向页缓存,无数据拷贝 - 在映射内存里直接修改(如填充 HTTP 头),再调
write()推给 socket - 相比
read(),省掉第 1 次 CPU 拷贝;但write()仍要拷一次到 socket 缓冲区 - 大文件映射需防内存溢出,且文件被截断时可能触发 SIGBUS
splice 和 vmsplice:更底层、更灵活的管道式零拷贝
基于 pipe(内存管道)机制,适合中间有逻辑但不想落地用户空间的链路:
-
splice()可在两个内核 fd(如 file → pipe、pipe → socket)间搬数据,0 拷贝、0 用户态参与 -
vmsplice()把用户空间内存页“借给” pipe,配合splice()实现用户态预处理 + 零拷贝发送 - 要求 fd 支持 splice(多数常规文件和 socket 支持),pipe 容量有限(默认 64KB),需分段操作
比 sendfile 更通用,但编程复杂度略高,Nginx、Kafka 等内部大量使用。
不复杂但容易忽略:零拷贝效果高度依赖硬件(DMA 支持)、内核版本(2.6.17+ 才有 gather DMA)、以及是否真能避开用户态——只要有一次 read() 或 malloc + memcpy,就退回传统路径。



















