在 Linux 内存操作与硬件交互的学习中,我们常会接触到 malloc、mmap、DMA-BUF 这几种方式,很多人会陷入一个误区:mmap 作为“零拷贝”的代表,理应比传统的 malloc+memcpy 更快。但实际开发中会发现,当 mmap 遇上 DMA-BUF 时,速度会暴跌几十倍——这背后的核心原因,并非 mmap 本身效率低下,而是我们忽略了内存访问的底层逻辑:页表属性与硬件访问规则。今天就用一篇笔记,彻底理清这几者的关系,读懂 mmap 的真正代价。
一、内存操作的认知演进:从简单到复杂
我们对内存操作的认知,往往会经历三个阶段,每个阶段的核心差异的是“谁在操作内存”和“如何操作内存”:
1. malloc + memcpy:CPU 主导的“直接操作”
这是最基础、最常见的内存操作方式。malloc 向系统申请一块内存,返回用户态可直接访问的指针,CPU 通过这个指针读写内存数据;如果需要在内存与设备(如文件、外设)之间传递数据,会通过 memcpy 完成数据拷贝——全程由 CPU 主导,逻辑简单,操作直接。
这种方式的优势是开发成本低,CPU 可直接利用高速缓存(Cache)加速访问,在纯 CPU 处理数据的场景下,效率很高。
2. mmap:内核与用户态的“共享缓冲”
mmap 的核心设计目标是“零拷贝”——它将内核空间的内存区域,直接映射到用户态进程的地址空间,让用户态进程可以直接访问内核内存,无需通过 memcpy 拷贝数据(避免了内核态与用户态之间的两次数据搬运:内核读数据→拷贝到用户态,或用户态写数据→拷贝到内核态)。
此时很多人会默认:mmap 一定比 malloc+memcpy 快。但这个结论并不绝对——mmap 的速度,取决于它所映射的内存“属性”,而非 mmap 本身。
3. DMA-BUF:硬件模块间的“零拷贝传递”
DMA-BUF 是为解决“硬件间数据传递”而设计的内存机制,核心是实现“硬件级零拷贝”。它本质上是一块被内核管理的共享内存,允许 GPU、NPU、编码器、显示器等硬件模块直接访问,无需 CPU 介入数据搬运。
很多人对 DMA-BUF 有一个误解:认为 CPU 不能碰 DMA-BUF。实际上,CPU 可以通过 mmap 映射 DMA-BUF 并访问,但一旦这么做,速度会急剧下降——这正是我们要重点拆解的“mmap 的真正代价”。
二、读懂底层关键:两个决定“正确性”与“速度”的核心概念
要理解 mmap 与 DMA-BUF 结合后的速度暴跌,必须先搞懂两个底层概念:硬件 I/O 一致性和页表属性。这两个概念,分别决定了内存访问的“正确性”和“速度”。
1. 硬件 I/O 一致性:保证“数据正确”,无需手动同步
在早期的嵌入式系统中,CPU 有自己的高速缓存(Cache),而 DMA 设备(如 GPU)会直接访问物理内存(DRAM),两者之间没有自动同步机制——如果 CPU 写的数据还停留在 Cache 中,未刷回 DRAM,DMA 设备读到的就是旧数据,会导致数据错误(如视频花屏、AI 推理出错);反之,DMA 设备写入的数据,CPU 也可能因为 Cache 未更新而读不到最新值。
为了解决这个问题,早期开发中需要手动调用 ioctl(SYNC) 等接口,强制同步 Cache 与 DRAM 的数据。但现代不少 SoC(如各类 ARM 架构 SoC)都集成了专门的互联模块(如 ARM CCI/CMN),实现了“硬件 I/O 一致性”——硬件会自动同步 CPU Cache 与各类硬件设备(GPU/NPU 等)访问的数据,开发者无需手动调用同步接口,既保证了数据正确性,也简化了开发。
但要注意:硬件 I/O 一致性只解决“数据正确”的问题,不解决“访问速度”的问题。
2. 页表属性:决定“访问速度”,mmap 无权修改
我们访问的每一块内存,都会在页表中记录其属性,其中最关键的属性之一,就是“是否可缓存(Cacheable/Non-Cacheable)”。这个属性,直接决定了 CPU 访问内存的速度,而它的设置者,并不是 mmap,而是内存的“导出者”(如 DMA-BUF 的驱动)。
两种页表属性的差异,直接决定访问速度:
- Cacheable(可缓存) :普通内存(malloc 申请)、普通文件 mmap 映射的内存,默认都是这个属性。CPU 访问时,会优先从高速缓存(L1/L2/L3)中读取数据,缓存命中时延迟极低,吞吐量极高——这也是普通 mmap 速度快的核心原因。
- Non-Cacheable(不可缓存) :DMA-BUF 映射的内存,被驱动强制设置为这个属性。CPU 访问时,会完全绕过高速缓存,直接访问物理内存(DRAM)——DRAM 的访问延迟远高于 Cache,吞吐量会暴跌几十倍。
这里的关键误区的是:很多人认为“mmap 本身慢”,但实际上,mmap 只是一种“映射机制”,它的作用是将物理内存映射到用户态地址空间,无权修改内存的页表属性。页表属性由内存的导出者(如 DMA-BUF 驱动)写死,mmap 只能被动接受——这才是 DMA-BUF 的 mmap 速度慢的根源。
三、关键澄清:mmap 的“零拷贝”,不等于“快”
我们之所以会对 mmap 的速度产生误解,核心是混淆了“零拷贝”和“快”这两个概念——两者没有必然联系,零拷贝解决的是“数据搬运”问题,而“快”解决的是“访问效率”问题。
1. 零拷贝的本质:不复制数据,而非加速访问
mmap 的“零拷贝”,是指避免了内核态与用户态之间的数据拷贝,数据始终在同一块物理内存中,只是地址空间被映射到了不同的层面(内核态/用户态)。这种设计的核心价值,是减少 CPU 的数据搬运开销,尤其在大数据量传输场景(如视频、音频)中,能显著降低 CPU 占用率。
但零拷贝不代表“访问速度快”——如果被映射的内存是 Non-Cacheable 属性,CPU 访问时依然会很慢,只是减少了“拷贝”这个步骤的开销,而非“访问”本身的开销。
2. 为什么 DMA-BUF 必须设置为 Non-Cacheable?
这不是驱动故意“限速”,而是硬件安全和数据一致性的必然要求:
- DMA 设备(GPU/NPU/编码器等)访问内存时,会直接访问物理内存(DRAM),不经过 CPU Cache;
- 如果 DMA-BUF 的页表属性是 Cacheable,CPU 写入的数据可能会停留在 Cache 中,未及时刷回 DRAM,此时 DMA 设备读到的就是旧数据,导致数据错误;
- 为了保证 DMA 设备与 CPU 访问的数据一致,驱动必须将 DMA-BUF 的页表属性设置为 Non-Cacheable,强制 CPU 直接访问 DRAM,与 DMA 设备的访问路径保持一致——这是牺牲“CPU 访问速度”,换取“数据正确性”。
四、正确用法:避开 mmap 的“坑”,发挥零拷贝价值
理解了页表属性的核心作用后,我们就能明确:DMA-BUF 的设计初衷,是给硬件模块之间传递数据用的,而非给 CPU 访问用的。想要发挥零拷贝的价值,同时避免速度暴跌,关键在于“让 CPU 只调度,不碰数据”。
正确用法:传递 fd,让硬件直接交互
DMA-BUF 的核心优势,是支持“文件描述符(fd)传递”——不同硬件模块之间,无需拷贝数据,只需传递 DMA-BUF 的 fd,就能直接访问同一块物理内存。此时 CPU 只负责调度(如传递 fd、触发硬件操作),不参与数据读写,既实现了零拷贝,又保证了访问速度(硬件直接访问 DRAM,无需 CPU 介入)。
示例逻辑(伪代码):
// 正确:fd 在硬件间流转,CPU 不碰数据
int dma_fd = create_dma_buf(size); // 创建 DMA-BUF,获取 fd
send_to_display(dma_fd); // 传递 fd 给显示模块,硬件直接访问
send_to_encoder(dma_fd); // 传递 fd 给编码器,硬件直接访问
错误用法:mmap DMA-BUF 给 CPU 访问
如果通过 mmap 映射 DMA-BUF,让 CPU 读写数据,虽然依然是零拷贝(数据未被复制),但 CPU 必须绕过 Cache 直接访问 DRAM,速度会暴跌几十倍,不仅浪费了 DMA-BUF 的设计价值,还会增加 CPU 开销,甚至破坏硬件流水线。
示例逻辑(伪代码,不推荐):
// 错误:mmap DMA-BUF 给 CPU 处理,速度暴跌
int dma_fd = create_dma_buf(size);
void *p = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, dma_fd, 0);
memcpy(p, data, size); // CPU 读写数据,极慢
五、核心结论
- mmap 本身不慢,慢的是它所映射的内存的“页表属性”——普通 mmap 是 Cacheable,所以快;DMA-BUF 的 mmap 是 Non-Cacheable,所以慢。
- 硬件 I/O 一致性保证“数据正确”,但管不了“访问速度”;页表属性决定“访问速度”,由 DMA-BUF 驱动写死,mmap 无权修改。
- 零拷贝 ≠ 快:零拷贝解决“数据搬运”问题,页表属性解决“访问速度”问题,两者独立无关。
- DMA-BUF 的正确用法:只传递 fd,让硬件模块直接交互;CPU 只做调度,不碰数据——这才是发挥零拷贝价值的唯一方式。
- CPU 不是“不能碰”DMA-BUF,而是“碰了就慢”,且违背 DMA-BUF 的设计初衷,实际开发中应坚决避免。
六、杂谈
很多开发者在调试时,会发现“mmap 速度时快时慢”(本人就是做dma_buf fd零拷贝时中间想要cpu直接画框,使用mmap发i西安),本质就是忽略了页表属性的差异:当映射普通文件、普通内存时,速度飞快;当映射 DMA-BUF、设备内存时,速度暴跌。理解了这一点,就能避开很多内存操作的坑。
对于嵌入式开发、多媒体开发(视频编解码)、AI 推理(NPU 与 GPU 数据交互)等场景,DMA-BUF 是核心工具,掌握它的底层逻辑(页表属性、硬件交互),才能真正发挥其零拷贝优势,写出高效、稳定的代码。