Theory · OS · Zero Copy
从 4 次拷贝到 2 次:mmap+write · sendfile · SG-DMA · splice —— Kafka 与 Nginx 高吞吐的内核基石
文件发网卡的默认路径要 4 次拷贝 4 次切换——其中 2 次 CPU 拷贝纯属"数据路过用户态交学费"
mmap+write 省一次 · sendfile 省用户态往返 · SG-DMA 连 socket 缓冲都跳过 · splice 用管道中转
"零"的是 CPU 拷贝与用户态中转——数据要改(加密/压缩)就得回用户态,零拷贝立刻失效
Baseline · 4 Copies 4 Switches
Step 1 · 3 Copies 4 Switches
mmap 把文件映射进用户地址空间——用户指针与页缓存是同一块物理内存(虚拟内存 deck 的机制复用)。于是"用户缓冲区"不再存在独立副本:磁盘 → 页缓存(DMA)→ 套接字缓冲(CPU)→ 网卡(DMA),3 次拷贝、4 次切换。
省掉"页缓存 → 用户缓冲"的那次 CPU 拷贝(数据不再进用户独占内存)。但仍有一次系统调用往返 ×2(mmap 不是零成本),且 write 的 CPU 拷贝还在。
适合"要读取文件内容做处理再发"的场景(比 read 省一次拷贝);代价:mmap 大量小文件映射开销、映射的写缺页、以及 munmap 后的 TLB 抖动。Ceph/Kafka 早期索引等场景用它。
// C 版骨架 buf = mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 0); write(sockfd, buf, len); // CPU 拷 1 次 munmap(buf, len); // 剩余拷贝:DMA 磁盘→页缓存 ① // CPU 页缓存→socket ② // DMA socket→网卡 ③
Step 2/3 · 2 Switches
sendfile(out_fd, in_fd, …) 在内核内把页缓存数据直接拷到套接字缓冲:磁盘 → 页缓存(DMA)→ 套接字缓冲(CPU)→ 网卡(DMA)。用户态完全不碰数据——切换从 4 次降到 2 次(一次 sendfile 进出)。
网卡支持scatter-gather DMA 时:套接字缓冲不再放数据,只放描述符(内存指针 + 长度),DMA 直接从页缓存"聚拢"数据到网卡——CPU 拷贝归零,只剩 2 次 DMA。这才是完整的"零拷贝"。
① 数据对应用不可见——要改内容(加密/压缩/改 header 之外的 body)就不能用;② 早期 out_fd 必须是 socket(文件→网卡的方向);③ TLS 加密后失效(数据必须过用户态加密)。
// 一条调用替代 read+write #include <sys/sendfile.h> // out: socket, in: 文件 sendfile(sockfd, filefd, &offset, count); // Nginx 配置开关: // sendfile on; // tcp_nopush on; <-- 配合整包发送 // Java NIO 的同一机制: // FileChannel.transferTo()
sendfile on 与 Kafka 消费路径的理论收益来源。
splice · pipe as Relay
splice(fd_in, off, fd_out, off, len, flags) 借助一个内核管道(pipe)做中转:数据在内核缓冲之间"移动"(实际是页引用转移),不经过用户态——任意两个支持管道操作的 fd 之间都能用(socket↔socket、socket↔文件、文件↔文件)。
典型用法两次调用:fd_in → pipe,pipe → fd_out。代价是多一次中转的簿记;收益是方向不受限(sendfile 只能文件→socket),还能和 tee 配合做"一处读多处分发"(管道内容复制引用)。
Nginx 的 tcp_proxy/stream 模块、一些代理与中继用 splice 做纯转发;但绝大多数场景 sendfile 已够用——splice 是"socket 到 socket 纯转发"场景的首选。
| 方案 | 拷贝(DMA/CPU) | 切换 | 方向限制 |
|---|---|---|---|
| read+write | 2 DMA + 2 CPU | 4 | 任意 |
| mmap+write | 2 DMA + 1 CPU | 4 | 文件→任意 |
| sendfile | 2 DMA + 1 CPU | 2 | 文件→socket |
| sendfile+SG | 2 DMA + 0 CPU | 2 | 文件→socket(需网卡支持) |
| splice | 2 DMA + 0 CPU | 2+(两次调用) | 任意(经 pipe 中转) |
copy_file_range(文件→文件零拷贝,跨文件系统视内核支持)与 tee(管道内容引用复制)——零拷贝家族在 Linux 里覆盖了所有方向的组合。
When Zero-Copy Doesn't Apply
纯转发(静态文件、消息原样投递、日志搬运)→ 零拷贝有效;要加密(TLS)、压缩、改写、解析 → 数据必须进用户态被 CPU 处理 → 零拷贝失效。TLS 是最常见杀手:HTTPS 服务器的 sendfile 收益被加密抹平。
consumer 拉取路径默认用 sendfile——但开启压缩或 SSL 后自动退回普通路径(数据要压缩/加密)。面试答"Kafka 用零拷贝"时要补上这个条件分叉,才是完整答案。
几 KB 的小文件:mmap 建映射 / sendfile 的固定开销可能超过省下的拷贝;且小块数据 DMA/CPU 拷贝本来就快。零拷贝是大块数据(MB 级)的最优解,不是万金油。
零拷贝方案都仍走 Page Cache(sendfile 从页缓存取数据)——热数据命中缓存才零拷贝,冷数据首次读仍有磁盘 I/O。它优化的是"搬运",不是"读取"。
| 场景 | 能不能零拷贝 | 方案 |
|---|---|---|
| 静态文件 HTTP | 能 | sendfile (+SG-DMA) |
| Kafka 明文消费 | 能 | sendfile |
| Kafka + TLS/压缩 | 不能 | 退回普通读写 |
| HTTPS 静态站 | 不能(加密) | 内核 TLS(kTLS)可恢复 |
| 纯转发代理 | 能 | splice |
| 改写/压缩中转 | 不能 | mmap 或 read+write |
Kafka · Nginx in Production
三大内核红利之一:顺序写(日志段文件追加, Page Cache 预读友好)+ Page Cache 复用(不自己管缓存,热数据天然在页缓存)+ sendfile(consumer 拉取:页缓存 → socket 一条调用)。三者叠加 = 磁盘速度接近网络速度。
fetch 明文 + 无压缩转换时走 sendfile;开启 SSL 或压缩(broker 端解压)后退回普通路径。运维判断:看 broker 是否终结 TLS、compression.type 是否在 broker 端转换。
sendfile on(零拷贝)+ tcp_nopush on(攒满整包再发,配合文件大块传输)+ directio(超大文件绕 Page Cache 防污染,与第 10 篇 O_DIRECT 呼应)——三层开关各管一个文件大小段。
io_uring · The Future
Linux 传统 AIO 名存实亡(仅 O_DIRECT、语义残缺);epoll 是同步语义(数据搬运还要自己 read);每个请求至少一次系统调用(提交 I/O 本身的开销)。想要真异步 + 低调用税——io_uring(5.1+,2019)应运而生。
内核与用户态共享内存:提交队列 SQ(用户放请求)与完成队列 CQ(内核放结果)。日常操作零系统调用:写 SQ ring → 内核后台消费 → 结果进 CQ → 用户收割;只在高水位时用 io_uring_enter 催一下。
覆盖文件/socket/定时器/带缓冲读写(IORING_OP_PROVIDE_BUFFERS 演化出的 buffer pool);配套零拷贝类操作(send zerocopy 6.0+)逐步落地。已在存储引擎(如新的 LSM 引擎)、高性能代理中生产使用——epoll + Reactor 的长期挑战者,但生态与调试成熟度仍需时间。
| 对比 | epoll + read/write | io_uring |
|---|---|---|
| 等待就绪 | epoll_wait 阻塞 | CQ 通知(可等可轮询) |
| 数据搬运 | 用户自己 read/write(同步) | 内核完成(真异步) |
| 每请求 syscall | ≥ 2 次 | 可 0 次(SQ/CQ 共享) |
| 生态 | 成熟(Redis/Netty/Go) | 快速成长中 |
Go × Zero Copy
io.Copy(dst, src) 不是傻瓜循环:dst 实现 ReaderFrom / src 实现 WriterTo 就走专用路径——TCPConn.ReadFrom 会检测 src 是否 *os.File,是则内核 sendfile(Linux),file→file 走 copy_file_range。写对接口 = 白拿零拷贝。
① src 被 bufio/compress 包装(类型断言失败,退回用户态循环);② src 是 bytes.Buffer/内存(本来就没有零拷贝可言);③ 中途 io.TeeReader 加日志——数据必须过 CPU,回到基线路径。
静态文件下载服务:io.Copy(w, f) 直通 sendfile(配合 http.ServeContent 底层同理);TLS 连接上自动失效(tls.Conn 没有 ReaderFrom 直通)——与第 6 页的边界完全一致。strace 看 sendfile 调用即可验证。
// 白拿零拷贝的正确姿势 f, _ := os.Open("video.mp4") defer f.Close() // TCP 明文:内部走 sendfile io.Copy(conn, f) // 文件 → 文件:copy_file_range dst, _ := os.Create("copy.mp4") io.Copy(dst, f) // 错过优化的写法: // io.Copy(conn, bufio.NewReader(f)) // ↑ 包装后类型断言失败
Interview QA
4 次拷贝:DMA 磁盘→页缓存、CPU 页缓存→用户、CPU 用户→socket 缓冲、DMA socket→网卡;4 次切换:read 进出 + write 进出。其中 2 次 CPU 拷贝对应数据"路过用户态"的纯开销。
DMA 是设备与内存直接搬运(不占 CPU 时钟);CPU 拷贝要占 CPU、消耗内存带宽。零拷贝省的是 CPU 拷贝与用户态中转——"零"指 CPU 手里不再过数据,不是没有任何拷贝。
mmap+write:省 1 次 CPU 拷贝(用户缓冲与页缓存合一),4 切换不变;sendfile:把搬运收进内核,切换降到 2 次,仍有 1 次 CPU 拷贝;加 SG-DMA 后 CPU 拷贝归零(2 次全 DMA)。
TLS 要对明文加密——数据必须进用户态被 CPU 处理,零拷贝的前提(CPU 不碰数据)被破坏。解法:kTLS 把对称加密下沉内核,数据在内核内加密后 DMA 直出,重新零拷贝。
明文且 broker 端不做压缩转换时走 sendfile;开启 SSL 或 broker 端压缩后数据必须过用户态处理,自动退回普通路径。答 Kafka 零拷贝要带上这两个前提条件。
文件→socket 的单向场景用 sendfile(一次调用、路径最短);socket↔socket 或任意方向用 splice(借 pipe 中转、两次调用)。纯转发代理(不解析内容)是 splice 的典型主场。
epoll 只批量化"等待"(同步语义,搬运还得自己 read);io_uring 用共享 SQ/CQ 环把"提交 I/O"也省掉(可零系统调用)且搬运由内核完成(真异步)。关系:epoll 是当下主流,io_uring 是两条优化线的合流与长期方向。
TCPConn.ReadFrom 检测到 src 是 *os.File 就走 sendfile,file→file 走 copy_file_range。失效:src 被 bufio/compress 包装、tls.Conn 中转、或中途 TeeReader——类型断言失败退回用户态循环。
OS Series · The Big Picture
Related & References
I/O 模型与 epoll →(本篇的"等待"前传)
文件系统与 Page Cache →(数据从哪来)
虚拟内存 →(mmap 方案的机制地基)
操作系统总览 →(系统调用切换税的出处)
Kafka 底层原理 →(零拷贝 + 顺序写的完整故事)
Redis 线程模型 →(小对象场景为什么不用零拷贝)
参考来源(本 deck 结论可溯源至下列一手材料)
| man 2 sendfile / splice / copy_file_range / mmap | 各方案语义与限制(out_fd 方向、pipe 中转)的一手定义 |
| OSTEP ch.36(I/O 设备)+ CSAPP ch.10 | DMA 与拷贝路径的机制基础 |
| kernel Documentation/driver-api(SG-DMA / kTLS) | scatter-gather 与内核 TLS 的支持条件 |
| Apache Kafka 文档(SSL/compression 与传输路径) | 零拷贝触发条件的工程确认 |
| io_uring 官方指南(Axboe)+ kernel 6.x send zerocopy | SQ/CQ 机制与零拷贝发送的最新进展 |
| Go src/internal/poll(sendfile/copy_file_range 分派) | ReadFrom 直通路径的源码依据 |
| xiaolincoding.com《图解系统》zero_copy.html | 四种方案拷贝次数对比的中文叙述 |