会吃番茄耶
26-06-17 23:08 微博认证:超话粉丝大咖(赵梦超话)

# 【嵌入式八股每日一题】DMA、DMA-BUF、buffer

> **DMA 是硬件搬运工**:CPU 配置源地址、目标地址、长度、方向,DMA 控制器把外设数据搬到内存,完成后通过中断通知 CPU。资料里也把 DMA 总结为“CPU 只配置源地址、目标地址、长度,搬运过程由 DMA 控制器完成,完成后中断通知 CPU”。

但 **DMA-BUF 不是 DMA 硬件**。它们名字像,但不是一层东西。

---

# 1. 先一句话区分 DMA 和 DMA-BUF

```text
DMA:
硬件机制,负责搬数据。

DMA-BUF:
Linux 内核中的 buffer 共享机制,负责让不同模块共享同一块内存。
```

你可以这样理解:

```text
DMA = 搬运工
Buffer = 仓库
DMA-BUF = 仓库的共享钥匙/通行证机制
buffer fd = 这把钥匙在用户态的编号
```

---

# 2. buffer 是什么?

**buffer 本质就是一块内存。**

它用来临时存放数据。

比如一帧图像:

```text
1920 × 1080 × 3 Byte ≈ 6 MB
```

这 6 MB 图像数据必须放在某块内存里,这块内存就叫:

```text
image buffer
```

在RK3588 项目里,可能有这些 buffer:

```text
USB 摄像头采集到的图像 buffer
RGA 输入 buffer
RGA 输出 buffer
NPU 输入 buffer
CPU/OpenCV 处理用的 buffer
显示模块使用的 buffer
```

如果没有共享机制,数据流可能是:

```text
摄像头 buffer
↓ memcpy
OpenCV buffer
↓ memcpy
RGA buffer
↓ memcpy
NPU buffer
```

这样每经过一个模块都复制一遍图像,CPU 和内存带宽都会被浪费。

---

# 3. DMA 是什么?你原来的理解再补完整一点

DMA 是 **Direct Memory Access,直接内存访问**。

它解决的问题是:

> 外设和内存之间有大量数据要搬,不希望 CPU 一个字节一个字节搬。

传统 CPU 搬运:

```text
外设寄存器 -> CPU 读取 -> CPU 写入内存
```

DMA 搬运:

```text
外设 -> DMA 控制器 -> 内存
```

CPU 只做配置:

```text
源地址
目标地址
传输长度
传输方向
传输完成中断
```

已知DMA 的配置要素:源地址、目标地址、传输长度、传输方向、触发方式等。

比如摄像头采集:

```text
摄像头 / USB / CSI / ISP

DMA

DDR 中的图像 buffer

CPU / RGA / NPU 后续处理
```

注意:

> DMA 只负责“搬到内存”。
> DMA 不负责“让 RGA、NPU、Display 都认识这块内存”。
> 这就是 DMA-BUF 要解决的问题。

---

# 4. DMA-BUF 是什么?

**DMA-BUF 是 Linux 内核提供的一套 buffer 共享框架。**

它解决的问题是:

> 一块内存已经被摄像头写好了,能不能不复制,直接交给 RGA、NPU、显示模块继续用?

DMA-BUF 的核心思想:

```text
某个模块申请/拥有一块 buffer

把这块 buffer 导出成一个 fd

其他模块拿着这个 fd 导入

大家访问的是同一块底层内存
```

所以 DMA-BUF 不是搬数据的硬件,它是:

```text
Linux 内核里管理“共享 buffer”的机制
```

它主要用于这种场景:

```text
Camera -> RGA -> NPU -> Display
```

目标是:

```text
不要每一层都 memcpy 一整张图像
```

操作系统资料里把零拷贝总结为减少内核缓冲区和用户缓冲区之间的数据拷贝,DMA 传输也属于减少 CPU 搬运的思想。 DMA-BUF 更进一步,是在多媒体/AI pipeline 里让不同设备共享同一块 buffer。

---

# 5. buffer fd 是什么?

先理解 fd。

在 Linux 里,fd 是 **file descriptor,文件描述符**。

平时:

```c
int fd = open("/dev/video0", O_RDWR);
```

这个 `fd` 是用户态拿到的一个整数。

它代表内核里的一个对象。

对于普通文件:

```text
fd -> 文件对象
```

对于设备:

```text
fd -> 设备对象
```

对于 DMA-BUF:

```text
buffer fd -> 一块共享 buffer 对象
```

所以 **buffer fd 不是图像数据本身**。

它只是一个“句柄”:

```text
buffer fd = 用户态拿到的一张票 / 一个钥匙编号
```

真正的图像数据还在内存里。

你可以把它想成:

```text
真正的 buffer:仓库
buffer fd:仓库钥匙编号
```

用户态把这个 fd 传给 RGA 或 NPU,意思是:

> 你不要我复制图像给你了,你拿这个 fd,自己去内核里找到那块共享 buffer。

---

# 6. DMA 和 DMA-BUF 的关系

它们经常一起出现,但职责不同。

## DMA 负责搬数据

比如摄像头采集:

```text
摄像头

DMA

内存中的 buffer
```

## DMA-BUF 负责共享这块 buffer

```text
这个 buffer
↓ 导出成 fd
buffer fd
↓ 传给 RGA
RGA 使用同一块 buffer
↓ 传给 NPU
NPU 使用同一块 buffer
```

组合起来就是:

```text
摄像头
↓ DMA 写入
共享 buffer
↓ DMA-BUF fd
RGA / NPU / Display 共享使用
```

一句话:

> **DMA 把数据搬进 buffer;DMA-BUF 让多个模块共享这个 buffer。**

---

# 7. 不用 DMA-BUF 的数据流

假设你做图像预处理:

```text
USB Camera 获取图像

用户态 OpenCV Mat
↓ memcpy
RGA 输入 buffer
↓ RGA resize
RGA 输出 buffer
↓ memcpy
NPU 输入 buffer
↓ NPU 推理
```

问题:

```text
图像很大;
每次 memcpy 都耗 CPU;
还占内存带宽;
延迟变大。
```

例如一张 1920×1080 RGB 图像约 6MB,如果每帧复制几次,30fps 下就是几百 MB/s 甚至更高的数据移动。

---

# 8. 使用 DMA-BUF 的理想数据流

更理想的是:

```text
Camera / V4L2 buffer
↓ 导出 dma-buf fd
RGA 导入 fd 做 resize / crop / format convert
↓ 输出另一个 dma-buf fd
NPU 导入 fd 做推理

CPU 只做调度和后处理
```

图示:

```text
同一块物理 buffer

┌───────────┼───────────┐
│ │ │
Camera RGA NPU
│ │ │
写入 处理 推理
│ │ │
└────── buffer fd ──────┘
```

这里传递的不是整张图,而是:

```text
buffer fd
```

也就是:

```text
一个整数句柄
```

---

# 9. “共享同一块物理 buffer”到底怎么实现?

从用户视角看,是这样:

```text
1. A 模块申请一块 buffer
2. A 模块把 buffer 导出成 dma-buf fd
3. 用户态拿到 fd
4. 用户态把 fd 传给 B 模块
5. B 模块 import 这个 fd
6. B 模块把它映射成自己能访问的 DMA 地址
7. B 模块开始处理同一块 buffer
```

更具体一点:

```text
Camera 驱动:
申请图像 buffer
DMA 把图像写进去
导出 dma-buf fd

用户程序:
拿到 fd
把 fd 传给 RGA

RGA 驱动:
import fd
attach 到自己的设备
map 成 RGA 可访问的地址
执行 resize/crop/format convert

NPU 驱动:
import fd 或拿 RGA 输出 fd
map 成 NPU 可访问的地址
执行推理
```

注意一个关键点:

> 不同硬件模块不一定直接看到“同一个物理地址”。
> 在有 IOMMU 的系统里,每个设备可能拿到自己的 IOVA / DMA 地址。
> 但它们背后对应的是同一组物理页 / 同一块共享 buffer。

所以更严谨地说:

```text
共享的是同一个 dma-buf 对象背后的内存;
每个设备通过自己的 DMA 映射方式访问它。
```

---

# 10. 为什么 fd 可以代表一块 buffer?

因为在 Linux 里,“一切皆文件”思想很强。

内核可以把很多对象包装成 fd:

```text
普通文件 -> fd
socket -> fd
设备节点 -> fd
epoll 对象 -> fd
dma-buf 对象 -> fd
```

Linux 驱动资料里也展示了用户空间通过 `open/read/write/ioctl` 进入内核,最终由 VFS 和驱动的 `file_operations` 处理;这就是为什么很多内核对象可以用 fd 暴露给用户空间。

DMA-BUF 也是这样:

```text
内核中有一个 dma_buf 对象

导出给用户态

用户态拿到一个 fd
```

所以:

```text
buffer fd 不是地址;
buffer fd 不是指针;
buffer fd 是内核对象的引用编号。
```

不能这样用:

```c
uint8_t *p = (uint8_t *)buffer_fd; // 错
```

因为 fd 只是整数编号,不是内存地址。

如果 CPU 想访问这块 buffer,需要:

```text
mmap(buffer_fd)
```

把它映射到用户空间,才能得到用户态虚拟地址。

---

# 11. buffer fd、虚拟地址、物理地址、DMA 地址的区别

这是面试最容易混的地方。

| 名称 | 谁用 | 是什么 | 能不能直接读写 |
| ------------- | ----------- | --------------- | -------- |
| buffer fd | 用户程序 / 驱动接口 | 一块 dma-buf 的句柄 | 不能直接读写 |
| 用户虚拟地址 | CPU 用户态 | mmap 后得到的地址 | 用户程序可读写 |
| 内核虚拟地址 | CPU 内核态 | 内核访问 buffer 的地址 | 驱动可读写 |
| 物理地址 | 内存硬件 | 真正 DDR 地址 | 用户态一般拿不到 |
| DMA 地址 / IOVA | 外设/RGA/NPU | 设备访问内存用的地址 | 设备使用 |

关系图:

```text
用户态:
buffer_fd = 12

│ mmap

user_addr = 0x7fxxxxxx CPU 用它访问

内核态:
dma_buf 对象

├── kernel virtual addr
├── physical pages
└── device dma addr / IOVA

硬件:
RGA / NPU / DMA 使用 device dma addr 访问
```

你可以记住:

```text
fd 是“找 buffer 的编号”
地址是“访问 buffer 的门牌号”
```

---

# 12. DMA-BUF 和 mmap 的区别

这两个经常一起问。

## mmap

mmap 的作用是:

> 把内核里的某块 buffer 映射到用户进程地址空间,让 CPU 用户态能访问。

```text
kernel buffer
↓ mmap
user virtual address
```

## DMA-BUF

DMA-BUF 的作用是:

> 让不同设备/驱动共享同一块 buffer。

```text
Camera buffer fd

RGA import

NPU import
```

区别:

```text
mmap:解决 CPU 用户态怎么访问 buffer
DMA-BUF:解决多个硬件模块怎么共享 buffer
```

它们可以配合:

```text
DMA-BUF fd
↓ mmap
CPU 访问这块共享 buffer
```

但它们不是一回事。

---

# 13. DMA-BUF 是不是等于零拷贝?

不一定。

DMA-BUF 是实现零拷贝的一种重要机制,但不是用了 DMA-BUF 就一定 0 拷贝。

要真正减少拷贝,需要满足:

```text
1. 上游模块能导出 dma-buf fd
2. 下游模块能导入 dma-buf fd
3. 格式兼容
4. stride / 对齐兼容
5. 不需要中间 CPU memcpy
6. cache 同步正确
```

如果 Camera 输出 NV12,但是你的 NPU 只接受 RGB,并且 RGA 没有接上,那中间还是可能发生格式转换和拷贝。

所以面试标准说法:

> DMA-BUF 提供了跨模块共享 buffer 的能力,但是否真正零拷贝,取决于整条 pipeline 的格式、对齐、硬件支持和同步机制。

---

# 14. Cache 一致性问题仍然存在

你之前学过:

```text
CPU 看 Cache
DMA / RGA / NPU 看 DDR
```

所以共享 buffer 时也会有问题。

## CPU 写,RGA/NPU 读

```text
CPU 把数据写到 cache
DDR 里还是旧数据
RGA/NPU 从 DDR 读
读到旧数据
```

需要:

```text
clean / flush cache
```

## RGA/NPU 写,CPU 读

```text
RGA/NPU 已经把结果写到 DDR
CPU cache 里还是旧数据
CPU 读到旧图
```

需要:

```text
invalidate cache
```

驱动资料里 DMA buffer 也强调了 Cache 一致性:驱动可以用 `dma_alloc_coherent` 分配一致性 DMA 缓冲区,或者使用 DMA 映射/同步机制。

所以 DMA-BUF 共享时也要考虑同步:

```text
谁写完了?
谁接下来要读?
CPU 有没有参与?
cache 是否同步?
```

---

# 15. 用 RK3588 项目理解

项目现在是:

```text
USB 双目相机

RK3588 采集图像

OpenCV / 双目算法

深度图 / 点云
```

如果后续引入 RGA/NPU,理想链路是:

```text
USB 双目图像 buffer

RGA:resize / crop / format convert

NPU:ROI 检测 / 模型推理

CPU:变焦决策 / 控制 / 后处理
```

如果没有 DMA-BUF,可能每一步都要复制图像。

如果有 DMA-BUF,希望变成:

```text
图像 buffer fd

RGA import fd

RGA 输出 buffer fd

NPU import fd

CPU 只拿结果
```

这样 CPU 主要做:

```text
调度
控制
后处理
错误处理
```

而不是反复 memcpy 图像。

---

# 16. 一个完整例子:Camera -> RGA -> NPU

假设 Camera 输出一帧图像。

## 第一步:Camera 产生 buffer

```text
Camera 驱动申请 buffer
DMA 把图像写入 buffer
驱动导出 dma-buf fd
```

用户态拿到:

```c
int camera_fd = 10; // /dev/video0
int buf_fd = 23; // 这一帧图像对应的 dma-buf fd
```

注意:

```text
buf_fd = 23 不是图像数据
它只是这块图像 buffer 的句柄
```

## 第二步:RGA 使用这个 fd

```text
RGA import buf_fd
RGA 得到自己可访问的 DMA 地址
RGA 做 resize / crop
RGA 输出到另一个 buffer
```

可能得到:

```text
rga_out_fd = 24
```

## 第三步:NPU 使用 RGA 输出 fd

```text
NPU import rga_out_fd
NPU 把它当作 input
NPU 推理
CPU 获取 output tensor
```

这就是:

```text
Camera -> RGA -> NPU
```

中间传的是 fd,不是整张图像。

---

# 17. 最容易混的几个点

## 误区 1:DMA-BUF 是 DMA 的一种?

不是。

```text
DMA 是硬件搬运机制。
DMA-BUF 是 Linux buffer 共享框架。
```

## 误区 2:buffer fd 是地址?

不是。

```text
fd 是句柄;
地址才是 CPU 或设备访问内存用的东西。
```

## 误区 3:共享同一块 buffer 就一定没有拷贝?

不一定。

格式不兼容、模块不支持 import、CPU 处理、cache 同步不对,都可能导致拷贝或错误。

## 误区 4:DMA-BUF 可以替代 DMA?

不能。

DMA-BUF 不搬数据。

```text
DMA 搬数据;
DMA-BUF 管共享。
```

## 误区 5:DMA-BUF 只给 CPU 用?

不是。

DMA-BUF 主要是给多个设备/驱动共享 buffer 用。CPU 要访问时才 mmap。

---

# 18. 标准回答

> DMA 和 DMA-BUF 不是同一层概念。DMA 是硬件机制,用来在外设和内存之间直接搬运数据,CPU 只负责配置源地址、目标地址、长度和方向,搬运完成后 DMA 通过中断通知 CPU。
>
> DMA-BUF 是 Linux 内核中的共享 buffer 框架,它不负责搬数据,而是负责让不同驱动或硬件模块共享同一块内存。比如 camera 采集到一帧图像后,可以把这块图像 buffer 导出成一个 dma-buf fd,用户态再把这个 fd 传给 RGA 或 NPU,RGA/NPU 在内核中 import 这个 fd,并映射成自己可以访问的 DMA 地址,从而直接处理同一块底层 buffer,减少 memcpy。
>
> buffer 本质上是一块内存,用来临时存放数据,比如一帧图像。buffer fd 是这块 dma-buf 对象在用户态的文件描述符,它不是地址,也不是指针,而是一个句柄。CPU 如果想访问这块 buffer,需要通过 mmap 把它映射成用户态虚拟地址;硬件模块访问时,则由驱动通过 dma-buf attach/map 得到设备可访问的 DMA 地址或 IOVA。
>
> 所以 DMA-BUF 常用于 camera -> RGA -> NPU -> display 这类多媒体 pipeline 中。DMA 负责把数据搬到内存,DMA-BUF 负责把这块内存共享给后续模块。需要注意的是,DMA-BUF 不等于天然零拷贝,是否真正减少拷贝还取决于上下游模块是否支持导入导出、格式和 stride 是否兼容,以及 cache 同步是否正确。

---

# 19. 用一张图记住

```text
没有 DMA-BUF:

Camera
↓ DMA
Camera buffer
↓ memcpy
RGA buffer
↓ memcpy
NPU buffer

NPU 推理

有 DMA-BUF:

Camera
↓ DMA
共享 buffer
↓ 导出 buffer fd
RGA import fd

RGA 输出 buffer fd

NPU import fd

NPU 推理
```

最核心一句:

```text
DMA 是搬运数据的硬件;
DMA-BUF 是共享内存的 Linux 机制;
buffer 是真正存数据的内存;
buffer fd 是这块共享 buffer 在用户态的句柄。
```

发布于 日本