在 Linux 后端面试中,“零拷贝”几乎是一道必问题。很多候选人能背出 sendfile、mmap、splice 这几个名词,但一旦被追问“它们到底零在哪”“谁真正减少了拷贝次数”“为什么 Kafka 用 mmap 而 Nginx 用 sendfile”,回答就开始模糊。这篇文章帮你把零拷贝的技术脉络和面试表达一次性理清楚。
一、先搞清楚:传统 read/write 到底拷贝了几次
面试时不要一上来就背 API,先画出传统文件发送的流程。假设要把一个磁盘文件通过 socket 发给客户端,典型代码是:
read(fd, buf, len);
write(sockfd, buf, len);
这短短两行,背后发生了 4 次上下文切换 + 4 次数据拷贝:
- DMA 拷贝:磁盘数据 → 内核页缓存(page cache)
- CPU 拷贝:内核页缓存 → 用户缓冲区(buf)
- CPU 拷贝:用户缓冲区 → socket 发送缓冲区
- DMA 拷贝:socket 缓冲区 → 网卡
其中第 2、3 次是 CPU 参与的拷贝,也是零拷贝技术要消灭的主要目标。理解这一点,后面所有优化都是在回答同一个问题:能不能让数据不经过用户态,直接在内核里流转?
二、mmap + write:减少一次 CPU 拷贝
mmap 的思路是把内核页缓存直接映射到用户空间,用户程序拿到的是同一块物理内存的虚拟地址,于是 read 的那次拷贝被省掉了:
void *buf = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, 0);
write(sockfd, buf, len);
拷贝次数变为 3 次:
- DMA:磁盘 → 页缓存
CPU:页缓存 → 用户缓冲区(被 mmap 消除)- CPU:用户缓冲区(实为页缓存映射)→ socket 缓冲区
- DMA:socket 缓冲区 → 网卡
面试要点:mmap 严格来说不算“零拷贝”,它只是把 4 次降到 3 次,仍然有一次 CPU 拷贝。它的优势在于用户态可以随机访问文件内容,所以适合需要对数据做解析、修改的场景,比如 Kafka 的索引文件读取、数据库的 B+ 树页访问。
另外要主动提一句风险:mmap 映射大文件时,如果发生缺页或文件被截断,会触发 SIGBUS 信号;而且映射区域的回收受页缓存管理影响,不适合小文件高频操作。
三、sendfile:真正的零拷贝主力
sendfile 是 Linux 2.2 引入的系统调用,专门为“文件 → socket”这个场景设计:
sendfile(out_fd, in_fd, &offset, count);
数据完全不经过用户态,拷贝次数降到 3 次(早期版本),如果网卡支持 SG-DMA(Scatter-Gather DMA),还能进一步降到 2 次:
- DMA:磁盘 → 页缓存
- CPU:页缓存 → socket 缓冲区(携带文件描述符和偏移量)
- DMA:socket 缓冲区 → 网卡
当网卡支持 SG-DMA 时,第 2 步的 CPU 拷贝也可以省掉,内核只把“数据在哪、多长”的描述符传给网卡,由网卡直接从页缓存读取。这才是教科书意义上的零拷贝——CPU 完全不碰数据。
面试加分点:
sendfile只能用于文件到 socket,不能反过来,也不能对数据做修改。- Linux 2.6.33 之后增加了
sendfile的变体,配合TCP_CORK可以更好地拼包。 - Nginx 的静态文件服务默认开启
sendfile on,这是它高并发下吞吐量大的关键原因之一。
四、splice:管道作为中转的通用方案
sendfile 的局限是“in_fd 必须是文件”。如果想在任意两个 fd 之间零拷贝传输,比如 socket → socket、socket → 文件,就要用 splice:
splice(fd_in, NULL, pipe_fd[1], NULL, len, SPLICE_F_MOVE);
splice(pipe_fd[0], NULL, fd_out, NULL, len, SPLICE_F_MOVE);
splice 要求至少一端是管道,所以通常配合一个 pipe 做中转。它通过内核的 pipe buffer 传递页的引用,避免了数据拷贝,本质上移动的是内存页的指针。
面试要点:
splice是 Linux 2.6.17 引入的,比sendfile更通用,但使用更复杂。- 它同样可以实现 2 次 DMA 拷贝 + 0 次 CPU 拷贝。
- 适合代理、转发类场景,比如 HAProxy 在某些路径下会用到。
- 注意
splice的 fd 之一必须是 pipe,这是它不如sendfile直观的原因。
五、三者对比:一张表说清楚
| 特性 | mmap + write | sendfile | splice |
|---|---|---|---|
| 引入版本 | 很早 | 2.2 | 2.6.17 |
| CPU 拷贝次数 | 1 次 | 0~1 次 | 0 次 |
| 数据可否修改 | 可以 | 不可以 | 不可以 |
| 适用方向 | 文件 → 任意 | 文件 → socket | 任意 fd(一端需 pipe) |
| 典型使用者 | Kafka、数据库 | Nginx、Apache | 代理、转发程序 |
| 是否真正零拷贝 | 否 | 是(SG-DMA 下) | 是 |
六、面试怎么答才出彩
如果面试官问“讲讲零拷贝”,建议按这个结构回答:
- 先定义问题:从传统 read/write 的 4 次拷贝讲起,指出 CPU 拷贝是瓶颈。
- 再给方案:mmap 消除用户态拷贝,sendfile 消除用户态参与,splice 提供通用性。
- 最后做取舍:需要改数据用 mmap,纯转发用 sendfile,任意 fd 用 splice。
- 补充实践:Kafka 用 mmap 读索引、用 sendfile 发消息;Nginx 用 sendfile 发静态文件;这些都是真实工程中的选择,能体现你不只是背概念。
一个容易被忽略的追问是:“零拷贝真的零吗?”标准答案是:DMA 拷贝无法避免,零拷贝指的是消除 CPU 参与的数据拷贝。能把这句话说清楚,基本就超过大多数候选人了。
总结
零拷贝不是某一个 API,而是一类优化思想:让数据尽量留在内核态,减少 CPU 在内存之间的搬运。mmap 是折中方案,sendfile 是文件到 socket 的最优解,splice 是更通用的补充。面试时把拷贝次数、适用场景和真实案例串起来讲,比单纯罗列 API 有说服力得多。
未经允许不得转载:任鹏个人博客 » Linux 面试中如何讲清楚零拷贝:sendfile、mmap 与 splice 的对比

