什么是零拷贝?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. I/O 底层原理:面试官想知道你是否清楚一次普通的读写操作,数据在内核态和用户态之间到底经历了什么:DMA 拷贝、CPU 拷贝、上下文切换,这几个概念能不能串成一条线。

  2. 性能优化思维:零拷贝本质是“减少无效搬运”。考察你遇到 I/O 密集型场景时,有没有从操作系统层面去思考优化,而不是只会加缓存加机器。

  3. 技术视野:Kafka 为什么快、Netty 的零拷贝指什么,这些追问都在试探你是“背概念”还是“真理解”。

核心答案

先给结论:零拷贝说的是一类 I/O 优化技术:数据传输过程中,尽量减少甚至完全消除 CPU 参与的数据拷贝(理想情况下一次都不需要),顺便把不必要的内核态与用户态上下文切换也省掉。

注意,零拷贝不是“零次拷贝”,而是“CPU 不需要参与搬运数据”。DMA 拷贝还是免不了的——数据总得从磁盘到内存吧?只要这段搬运不需要 CPU 出力,就算零拷贝。

直接对比一下各种方案:

方案 拷贝次数 上下文切换 CPU 拷贝
传统 read + write 4 次 4 次 2 次
mmap + write 3 次 4 次 1 次
sendfile 3 次 2 次 1 次
sendfile + SG-DMA 2 次 2 次 0 次

零拷贝对比
零拷贝对比

深度解析

一、传统 I/O 有多浪费:4 次拷贝 + 4 次切换

以“读取磁盘文件并通过网卡发送出去”这个经典场景为例(比如你写了一个文件下载服务),传统写法是:

// 传统 I/O:先把文件读进来,再写出去
File file = new File("data.txt");
RandomAccessFile raf = new RandomAccessFile(file, "r");
byte[] buffer = new byte[1024];

// 1. 读:数据从内核缓冲区拷贝到用户缓冲区
raf.read(buffer);

// 2. 写:数据从用户缓冲区拷贝到 socket 缓冲区
socket.getOutputStream().write(buffer);

看起来就一次读一次写,但操作系统在背后干的活儿多得多:

传统 IO 拷贝
传统 IO 拷贝

完整走一遍这条链路:

  • 第一次拷贝(DMA):磁盘上的数据通过 DMA 引擎拷贝到内核的页缓存(Page Cache)里。这一步不占 CPU,DMA 芯片自己搬。
  • 第二次拷贝(CPU)read() 系统调用返回前,CPU 把数据从内核缓冲区拷贝到用户空间的缓冲区。这一步跨过了内核态到用户态的边界。
  • 第三次拷贝(CPU):调用 write(),CPU 又把数据从用户缓冲区拷贝回内核的 Socket 缓冲区。
  • 第四次拷贝(DMA):DMA 引擎把数据从 Socket 缓冲区拷贝到网卡,真正发出去。

同时,read()write() 各对应两次上下文切换(用户态 → 内核态 → 用户态),加起来 4 次拷贝 + 4 次上下文切换

问题在哪?你品品第二、三次拷贝——数据从内核搬到用户空间,又原封不动搬回内核。用户程序压根没修改这个文件的内容,纯粹是“白跑两趟”。文件越大、请求越多,这种无效搬运的浪费就越夸张。

二、优化思路一:mmap + write,砍掉一次 CPU 拷贝

既然用户程序不需要碰数据内容,那能不能让用户空间和内核空间共享同一块内存?这就是 mmap(内存映射)。

// Java NIO 中用 MappedByteBuffer 使用 mmap
FileChannel channel = new RandomAccessFile(file, "r").getChannel();
MappedByteBuffer mapped = channel.map(
    FileChannel.MapMode.READ_ONLY, 0, file.length());

mmap 把内核页缓存直接映射到用户空间,用户程序操作这块内存,实际操作的就是页缓存本身,省掉了“内核 → 用户”的那次 CPU 拷贝。流程变成:

mmap 零拷贝
mmap 零拷贝

  • 第一次(DMA):磁盘 → 页缓存,不变。
  • 第二次(CPU)write() 时 CPU 把数据从共享的页缓存拷贝到 Socket 缓冲区。
  • 第三次(DMA):Socket 缓冲区 → 网卡。

拷贝次数从 4 降到 3,CPU 拷贝从 2 次降到 1 次。不过上下文切换还是 4 次(mmapwrite 也各两次)。

mmap 减少拷贝
mmap 减少拷贝

顺带一提,RocketMQ 就大量使用了 mmap,CommitLog 文件写入走的就是内存映射这条路。

三、优化思路二:sendfile,连用户空间都不用去了

Linux 2.2 内核引入了 sendfile 系统调用,专门为“文件到 Socket”这种传输场景设计,数据全程在内核态流转,根本不经过用户空间:

sendfile 流程
sendfile 流程

对比 mmap + write,拷贝次数一样是 3 次,但因为只有一次系统调用,上下文切换从 4 次降到 2 次。

Java NIO 对应的 API 是 FileChannel.transferTo(),底层在 Linux 上就是调用 sendfile

// 零拷贝发送文件:一行搞定
fileChannel.transferTo(0, fileChannel.size(), socketChannel);

Kafka 高吞吐的功臣之一就是它——Broker 之间副本同步、消费者拉取消息,走的都是 transferTo 这条零拷贝路径,数据从页缓存直接飞到网卡,完全不经过 JVM 堆。这也是为什么 Kafka 的消息吞吐量能那么夸张。

sendfile 零拷贝
sendfile 零拷贝

四、终极形态:sendfile + SG-DMA,CPU 拷贝归零

Linux 2.4 内核之后,如果网卡支持 SG-DMA(Scatter-Gather DMA,分散-聚集 DMA)技术,sendfile 还能再进一步:

SG-DMA 流程
SG-DMA 流程

这时候 CPU 连那次“页缓存 → Socket 缓冲区”的拷贝都不用干了,只往 Socket 缓冲区里塞入描述符信息(数据在内存中的地址、长度),SG-DMA 引擎直接根据描述符把页缓存里的数据 gather 到网卡。

全程 2 次拷贝,全是 DMA 干的,CPU 拷贝次数为 0——这才是字面意义上的“零拷贝”,也是这个技术名字的由来。

SG-DMA 零拷贝
SG-DMA 零拷贝

五、别忘了 Netty 的“零拷贝”是另一回事

面试里特别爱挖这个坑。Netty 官方说自己有零拷贝,但 Netty 的零拷贝是缓冲区层面的零拷贝,跟操作系统那个不是一个概念:

对比 OS 层零拷贝 Netty 零拷贝
层面 内核态与用户态之间 ByteBuf 对象之间
目的 减少 CPU 拷贝和上下文切换 减少堆内内存在各组件间的复制
典型实现 sendfilemmap CompositeByteBufslice()duplicate()

Netty 的几个典型操作:

  • CompositeByteBuf:把多个 ByteBuf 逻辑上拼成一个整体,避免内存合并时的真实拷贝;
  • slice() / duplicate():切分出的新 ByteBuf 和原 Buf 共享同一块底层内存;
  • Unpooled.wrappedBuffer():把 byte 数组包装成 ByteBuf,不发生数组复制。

当然,Netty 底层传输数据时也会用到 OS 层的零拷贝(比如 FileRegion 封装了 transferTo),两层零拷贝它都有,但面试时一定要分清楚层次,不然容易被打脸。

Netty 零拷贝区别
Netty 零拷贝区别

面试高频追问

  1. 追问一:零拷贝是不是万能的?什么场景不适合?

    不是。零拷贝的前提是用户程序不需要处理数据内容(纯转发)。如果你拿到数据后还要做加密、压缩、校验,数据必须进用户空间,零拷贝就玩不转了。另外超大文件传输场景,页缓存反而可能被大文件“冲刷”掉影响其他热点数据,这种场景会考虑直接 I/O(绕过页缓存)配合异步 I/O。

  2. 追问二:Kafka 和 RocketMQ 为什么一个选 sendfile,一个选 mmap?

    使用场景不同。Kafka 的核心路径是“消费者拉取消息 → 网络发送”,数据不需要在 Broker 端加工,sendfile 最合适;RocketMQ 的 CommitLog 写入是核心瓶颈,写入用 mmap 让磁盘和内存映射打通,读写都受益。各自的业务形态决定了技术选型。

  3. 追问三:mmap 一定比传统 read 快吗?

    不一定。小文件、随机读、跨页访问场景下 mmap 缺页中断(page fault)的代价可能更高,映射本身也有建立和销毁的开销。没有银弹,都得看场景。

常见面试变体

  • mmapsendfile 的区别是什么?”
  • “Kafka 为什么快?”(零拷贝是必答点之一,配合顺序写、页缓存一起说)
  • “Netty 的零拷贝体现在哪里?”
  • transferTo 方法了解吗?底层怎么实现的?”

记忆口诀

演进路线记一条线:传统四次拷贝太浪费 → mmap 省一次(共享内存)→ sendfile 少切换(一次调用)→ SG-DMA 零 CPU(网卡直取)。

数字对应记法:4 → 3 → 3 → 2(拷贝次数),4 → 4 → 2 → 2(切换次数),最后一步 CPU 拷贝归零,故称“零拷贝”。

总结

零拷贝的核心就一句话:数据在内核态流转时,CPU 别当搬运工。从传统 I/O 的 4 次拷贝 4 次切换,到 mmap 砍一次拷贝,再到 sendfile 砍两次切换,最后 SG-DMA 让 CPU 拷贝彻底归零——把这条演进线讲清楚,再带上 Kafka、RocketMQ、Netty 的落地例子,这道题你就答出区分度了。