目录
- [Linux DMA-BUF:跨设备缓冲区共享与同步](#Linux DMA-BUF:跨设备缓冲区共享与同步)
-
- [1. DMA-BUF 解决什么问题](#1. DMA-BUF 解决什么问题)
- [2. DMA-BUF 不负责什么](#2. DMA-BUF 不负责什么)
- [3. 三个核心原语](#3. 三个核心原语)
-
- [3.1 dma_buf:共享缓冲区对象](#3.1 dma_buf:共享缓冲区对象)
- [3.2 dma_fence:异步操作完成信号](#3.2 dma_fence:异步操作完成信号)
- [3.3 dma_resv:与缓冲区关联的 fence 容器](#3.3 dma_resv:与缓冲区关联的 fence 容器)
- [4. 参与者:exporter、importer 与用户空间](#4. 参与者:exporter、importer 与用户空间)
-
- [4.1 Exporter](#4.1 Exporter)
- [4.2 Importer](#4.2 Importer)
- [4.3 用户空间](#4.3 用户空间)
- [5. 四种地址不要混淆](#5. 四种地址不要混淆)
- [6. sg_table 与 scatter-gather](#6. sg_table 与 scatter-gather)
- [7. Exporter:创建与导出 DMA-BUF](#7. Exporter:创建与导出 DMA-BUF)
-
- [7.1 dma_buf_ops 的职责](#7.1 dma_buf_ops 的职责)
- [8. Importer:设备 DMA 访问流程](#8. Importer:设备 DMA 访问流程)
-
- [8.1 获取与 attach](#8.1 获取与 attach)
- [8.2 Map、提交与完成](#8.2 Map、提交与完成)
- [8.3 DMA 方向](#8.3 DMA 方向)
- [8.4 正确的释放顺序](#8.4 正确的释放顺序)
- [9. 引用计数与生命周期](#9. 引用计数与生命周期)
- [10. CPU 访问与缓存一致性](#10. CPU 访问与缓存一致性)
-
- [10.1 内核空间访问](#10.1 内核空间访问)
- [10.2 用户空间 mmap](#10.2 用户空间 mmap)
- [11. 缓存一致性与执行同步是两件事](#11. 缓存一致性与执行同步是两件事)
- [12. 隐式同步与显式同步](#12. 隐式同步与显式同步)
-
- [12.1 隐式同步](#12.1 隐式同步)
- [12.2 显式同步](#12.2 显式同步)
- [13. DMA-Heap:从用户空间分配 DMA-BUF](#13. DMA-Heap:从用户空间分配 DMA-BUF)
- [14. 图像 buffer 还需要布局元数据](#14. 图像 buffer 还需要布局元数据)
- [15. 动态映射与内存迁移](#15. 动态映射与内存迁移)
- [16. 锁与错误处理](#16. 锁与错误处理)
- [17. 调试、统计与安全](#17. 调试、统计与安全)
-
- [17.1 查看 fd 与 DMA-BUF 信息](#17.1 查看 fd 与 DMA-BUF 信息)
- [17.2 常见安全检查](#17.2 常见安全检查)
- [18. 原文中需要纠正的几个结论](#18. 原文中需要纠正的几个结论)
- [19. 建议的验证实验](#19. 建议的验证实验)
-
- [实验一:验证 fd 传递与引用生命周期](#实验一:验证 fd 传递与引用生命周期)
- [实验二:记录 importer 的资源状态](#实验二:记录 importer 的资源状态)
- [实验三:区分 fence 等待与缓存同步](#实验三:区分 fence 等待与缓存同步)
- [20. 小结](#20. 小结)
- 参考资料
Linux DMA-BUF:跨设备缓冲区共享与同步
本文面向 Linux 设备驱动开发者,接口语义以当前上游内核文档为参考。DMA-BUF 是内核内部 API,版本之间会演进;实际开发必须以目标内核的
include/linux/dma-buf.h、include/linux/dma-resv.h和对应驱动实现为准。
1. DMA-BUF 解决什么问题
摄像头、视频编解码器、GPU、NPU 和显示控制器经常需要依次访问同一块图像或张量数据。如果每经过一个设备都复制一次数据,会增加内存带宽、延迟和功耗。
DMA-BUF 提供了一套跨驱动、跨子系统、跨进程共享缓冲区的框架:
- 内核使用
struct dma_buf表示一个共享缓冲区对象; - 用户空间通常使用普通文件描述符(fd)持有和传递这个对象;
- 导入驱动通过
struct dma_buf_attachment建立缓冲区与设备的关系; - 设备访问时,导入驱动取得针对该设备完成 DMA 映射的
struct sg_table; dma_fence和dma_resv用于协调异步硬件访问;- CPU 访问前后需要执行相应的缓存一致性协议。
一个典型多媒体链路可以表示为:
text
Camera / ISP -> Video Codec -> GPU -> Display
\___________ 同一个 DMA-BUF ___________/
控制面:fd、尺寸、格式、stride、offset、modifier、fence
数据面:由 exporter 管理的实际 backing storage
Android 图形栈中,Gralloc/Allocator、SurfaceFlinger、GPU 和显示驱动经常通过包含 DMA-BUF fd 的 native handle 共享图形缓冲区。具体由哪个进程分配、使用几个 fd,以及元数据怎样传递取决于 Android 和厂商实现,因此不应把某一种 Android 流程当成 DMA-BUF 本身的定义。
2. DMA-BUF 不负责什么
理解边界比记忆接口更重要:
- DMA-BUF 本身不是通用内存分配器。 backing storage 由 exporter 管理;DMA-Heap、DRM GEM、V4L2 或厂商分配器可以提供 DMA-BUF。
- fd 不是物理地址。 fd 是进程文件描述符表中的句柄,内核通过它找到
struct file和struct dma_buf。 - DMA-BUF 不保证物理连续。 缓冲区可能由离散页、CMA 连续内存、设备本地内存或其他存储组成。
- DMA-BUF 不描述像素格式。 宽高、stride、plane、offset、format 和 modifier 必须由上层 API 一起传递。
- 共享同一缓冲区不等于可以无序并发访问。 调用方仍需要处理硬件执行顺序、CPU 缓存一致性和业务层互斥。
- "零拷贝"不保证没有任何复制。 exporter 可能因设备可达性、内存迁移或 CPU 访问而移动或复制 backing storage。
3. 三个核心原语
当前 DMA-BUF 文档把体系拆成三个相互配合的原语。
3.1 dma_buf:共享缓冲区对象
struct dma_buf 表示共享缓冲区及其生命周期。用户空间看到的是 fd,内核驱动看到的是 struct dma_buf *。
几个重要概念字段是:
size:缓冲区字节数,在该 DMA-BUF 生命周期中保持不变;file:用于 fd 表示和引用计数的struct file;ops:exporter 提供的回调;attachments:已经附着的设备关系;priv:exporter 的私有缓冲区对象;resv:与缓冲区关联的struct dma_resv。
3.2 dma_fence:异步操作完成信号
struct dma_fence 表示一个异步 DMA 操作的完成点,例如:
- GPU 渲染完成;
- 视频解码完成;
- 显示控制器不再读取某个 buffer;
- DMA engine 拷贝完成。
fence 从未完成状态变为 signaled 状态。它表示"操作完成",不负责缓存刷新,也不是内存锁。
3.3 dma_resv:与缓冲区关联的 fence 容器
struct dma_resv 管理与一个资源关联的一组 fence,并为隐式同步提供基础。当前接口用 usage 区分读、写等访问语义。
粗略理解依赖关系:
text
后续读:通常需要等待先前写完成
后续写:通常需要等待先前读和写完成
具体驱动必须遵循 enum dma_resv_usage 和所属子系统的同步规则,不能只凭"一个共享 fence、一个独占 fence"的旧模型实现新代码。
4. 参与者:exporter、importer 与用户空间
4.1 Exporter
Exporter 是拥有并管理 backing storage 的驱动或子系统,主要负责:
- 分配、回收或迁移实际存储;
- 实现
struct dma_buf_ops; - 检查导入设备能否访问该存储;
- 为每个 attachment 建立合适的 DMA 映射;
- 配合 CPU 访问完成缓存一致性处理;
- 在最后一个引用释放时销毁 exporter 私有对象。
exporter 描述内存对象的所有者,并不等同于数据流中的"生产者"。一个 importer 也可能通过设备向 buffer 写入数据。
4.2 Importer
Importer 是希望让某个设备访问 DMA-BUF 的驱动,主要负责:
- 从 fd 获得
struct dma_buf引用; - 将自己的
struct deviceattach 到缓冲区; - 按正确方向取得设备 DMA 映射;
- 等待必要 fence 后向硬件提交任务;
- 在任务完成后 signal 自己的 fence;
- 按相反顺序解除 mapping、attachment 和引用。
struct dma_buf_attachment 表示"一个 DMA-BUF 与一个设备之间的附着关系",不是一个用户进程,也不等同于 fd。
4.3 用户空间
用户空间通常把 DMA-BUF fd 当成 opaque handle,通过以下方式传递:
- 某个设备 UAPI 的 ioctl;
- Unix domain socket 的
SCM_RIGHTS; - Binder/native handle;
- Wayland、DRM、V4L2、EGL、Vulkan 等上层协议或 API。
不同进程的 fd 数字没有全局意义。把整数 7 通过普通消息告诉另一个进程,并不能让对方访问同一个对象;必须通过能传递 struct file 引用的机制发送 fd。
5. 四种地址不要混淆
共享 DMA 缓冲区时会同时出现多种地址空间:
| 地址 | 使用者 | 含义 |
|---|---|---|
| 用户虚拟地址 | 用户进程 | mmap() 返回的进程地址 |
| 内核虚拟地址 | CPU 内核代码 | dma_buf_vmap() 得到的映射 |
| 物理地址/页 | 内存管理与 exporter | backing storage 的物理组成 |
| DMA 地址或 IOVA | 具体设备 | 设备发起 DMA 时使用的地址 |
存在 IOMMU 时,设备使用的 IOVA 与 CPU 物理地址可能完全不同。即使物理页离散,IOMMU 也可能提供适合设备访问的地址视图。
dma_buf_map_attachment() 返回的 sg_table 已针对 attachment->dev 完成 DMA 映射。驱动给硬件编程时应使用 DMA API 提供的 DMA 地址和长度,不能把 page_to_phys()、sg_phys() 或 CPU 虚拟地址直接当成设备地址。
同一个 DMA-BUF 对不同设备可能得到不同 DMA 地址,因此不能把设备 A 的 sg_table 缓存后交给设备 B 使用。
6. sg_table 与 scatter-gather
struct sg_table 管理 scatterlist,描述缓冲区的若干内存段。它并不简单等于"按页排列的物理连续块列表"。需要区分:
- exporter 的 backing store 可能由多段物理页组成;
- DMA API 可以合并相邻段;
orig_nents与 DMA 映射后的nents可能不同;- DMA 地址受设备 DMA mask、IOMMU、segment size 和边界约束影响;
- 映射结果只在对应 attachment 和 mapping 生命周期内有效。
遍历已映射的 scatterlist 时,应采用目标内核为 DMA 映射提供的遍历宏,并读取 DMA address/length 字段。具体宏和语义应依据目标内核版本核对。
7. Exporter:创建与导出 DMA-BUF
Exporter 先创建自己的私有 buffer 对象,然后用 DMA-BUF 包装它。简化骨架如下:
c
#include <linux/dma-buf.h>
#include <linux/err.h>
#include <linux/fcntl.h>
static int my_export_buffer(struct my_buffer *buffer)
{
DEFINE_DMA_BUF_EXPORT_INFO(exp_info);
struct dma_buf *dmabuf;
int fd;
exp_info.ops = &my_dma_buf_ops;
exp_info.size = buffer->size;
exp_info.flags = O_RDWR;
exp_info.priv = buffer;
dmabuf = dma_buf_export(&exp_info);
if (IS_ERR(dmabuf))
return PTR_ERR(dmabuf);
fd = dma_buf_fd(dmabuf, O_CLOEXEC);
if (fd < 0) {
dma_buf_put(dmabuf);
return fd;
}
/* 成功后,fd 持有这份引用;不要再对同一引用调用 dma_buf_put()。 */
return fd;
}
DEFINE_DMA_BUF_EXPORT_INFO() 会初始化结构并设置 exporter/module 信息。dma_buf_export() 创建 struct dma_buf 和关联的匿名文件对象,但是否已经分配实际 backing storage 由 exporter 决定。有些 exporter 可以延迟到 attach 或 map 时再选择内存位置。
dma_buf_fd() 把 DMA-BUF 安装到当前进程的 fd 表。创建 fd 时必须原子设置 O_CLOEXEC,避免多线程进程在 exec 竞态中把敏感 buffer fd 泄漏给新程序。
7.1 dma_buf_ops 的职责
当前内核中需要重点理解的回调包括:
| 回调 | 必选性 | 主要职责 |
|---|---|---|
attach / detach |
可选 | 检查设备约束并管理每个 attachment 的私有状态 |
map_dma_buf / unmap_dma_buf |
必选 | 创建和释放面向导入设备的 DMA 映射 |
release |
必选 | 最后一个 DMA-BUF 引用消失后释放 exporter 私有对象 |
begin_cpu_access / end_cpu_access |
可选 | 准备和结束 CPU 访问,处理缓存或存储迁移 |
mmap |
可选 | 支持用户空间映射 |
vmap / vunmap |
可选 | 支持整个 buffer 的内核虚拟映射 |
pin / unpin |
动态映射相关 | 控制 backing store 是否允许迁移 |
Exporter 的 map_dma_buf() 通常需要完成以下工作:
- 确保 backing storage 已分配且满足设备约束;
- 必要时固定或迁移存储;
- 为当前 attachment 构造独立的
sg_table; - 通过 DMA API 映射到
attachment->dev的 DMA 地址空间; - 在失败时按逆序释放已经取得的资源。
每个 attachment 应获得自己可释放的映射对象。Exporter 不能随意把同一份已 DMA-map 的 sg_table 返回给不同设备。
8. Importer:设备 DMA 访问流程
8.1 获取与 attach
Importer 从用户传入的 fd 取得引用:
c
struct dma_buf *dmabuf;
struct dma_buf_attachment *attach;
dmabuf = dma_buf_get(fd);
if (IS_ERR(dmabuf))
return PTR_ERR(dmabuf);
attach = dma_buf_attach(dmabuf, dev);
if (IS_ERR(attach)) {
int ret = PTR_ERR(attach);
dma_buf_put(dmabuf);
return ret;
}
dma_buf_get() 使用底层 struct file 的引用计数增加一份内核引用。这个引用计数包含 fd 和其他内核持有者,不等于"消费者数量"。
Attach 阶段 exporter 可以检查:
- 设备 DMA mask;
- 最大 segment 大小和边界;
- backing storage 是否能被该设备访问;
- 是否需要移动到 system memory;
- peer-to-peer 访问能力。
因此 dma_buf_attach() 可以失败,importer 必须处理错误。
8.2 Map、提交与完成
当前内核同时提供普通 helper 和带 _unlocked 后缀的变体。按照上游 locking convention,普通 map/unmap helper 要求 importer 已经持有 DMA-BUF 的 reservation 锁;_unlocked 变体要求调用时未持有该锁,并由 helper 处理需要的加锁。不能只按后缀的字面含义推断锁条件。未自行管理 reservation 锁的 importer 可采用下面的简化流程:
text
dir = DMA_FROM_DEVICE
sgt = dma_buf_map_attachment_unlocked(attach, dir)
if sgt 返回错误:
dma_buf_detach(dmabuf, attach)
dma_buf_put(dmabuf)
返回错误
等待本次访问依赖的 fence
使用 sgt 中面向 attach->dev 的 DMA 地址配置硬件
创建并发布代表本次任务的 dma_fence
提交硬件任务
硬件中断或完成 worker:
确认设备不再访问该映射
signal fence
dma_buf_unmap_attachment_unlocked(attach, sgt, dir)
dma_buf_detach(dmabuf, attach)
dma_buf_put(dmabuf)
这段代码只展示资源顺序,不是完整的异步驱动实现。真实驱动不能在提交硬件后立即执行 unmap;mapping 和 attachment 必须至少存活到硬件任务完成。
8.3 DMA 方向
DMA 方向从内存与设备之间的数据流理解:
| 方向 | 含义 | 示例 |
|---|---|---|
DMA_TO_DEVICE |
内存中的数据被设备读取 | 显示扫描、编码器读取输入 |
DMA_FROM_DEVICE |
设备向内存写入数据 | 摄像头采集、解码器写输出 |
DMA_BIDIRECTIONAL |
设备可能读写 | 确实无法使用更精确方向时 |
方向必须与 map、硬件访问和 unmap 保持一致。错误方向在 DMA-coherent 平台上可能暂时看不出问题,但在 non-coherent 平台上可能产生陈旧数据或额外同步开销。
8.4 正确的释放顺序
text
确认硬件访问完成
-> signal/处理 fence
-> dma_buf_unmap_attachment_unlocked()
-> dma_buf_detach()
-> dma_buf_put()
dma_buf_unmap_attachment() 的含义是释放 DMA mapping,并不负责等待硬件完成,也不等价于"通知 exporter DMA 已完成"。硬件完成应由驱动中断、完成队列和 fence 等机制确认。
9. 引用计数与生命周期
DMA-BUF 的生命期主要由关联 struct file 的引用计数管理:
dma_buf_get(fd)获得一份引用,必须由dma_buf_put()配对;get_dma_buf(dmabuf)增加已有对象的额外内核引用,也必须配对dma_buf_put();dup()、fork()、SCM_RIGHTS和 Binder fd 传递都可能增加或共享文件引用;- 用户空间
close(fd)只释放该 fd 持有的引用; - 所有 fd 与内核引用都释放后,才会调用 exporter 的
release()。
fd、attachment 和 mapping 是三个不同生命周期:
text
fd / dma_buf reference:保证 dma_buf 对象存在
attachment:保证设备与 buffer 的关系存在
mapping:保证当前设备 DMA 地址映射有效,通常也约束 backing store 迁移
Importer 从 fd 调用 dma_buf_get() 后,用户进程关闭原 fd 不会立刻使 importer 手中的指针失效,因为 importer 已有独立引用。
10. CPU 访问与缓存一致性
CPU 访问 DMA-BUF 有两种常见情况:内核通过 vmap 访问,或用户空间通过 mmap 访问。二者都必须遵循 begin/end 协议。
10.1 内核空间访问
当前接口使用 struct iosys_map 表示映射,因为 backing storage 可能是普通系统内存,也可能是 I/O memory:
c
#include <linux/dma-buf.h>
#include <linux/errno.h>
#include <linux/iosys-map.h>
static int process_buffer_on_cpu(struct dma_buf *dmabuf)
{
struct iosys_map map = { };
int ret;
ret = dma_buf_begin_cpu_access(dmabuf, DMA_BIDIRECTIONAL);
if (ret)
return ret;
ret = dma_buf_vmap_unlocked(dmabuf, &map);
if (ret)
goto out_end_access;
/* 使用 iosys_map helper 访问内容,不要假定 map 一定是普通 void *。 */
dma_buf_vunmap_unlocked(dmabuf, &map);
out_end_access:
if (dma_buf_end_cpu_access(dmabuf, DMA_BIDIRECTIONAL) && !ret)
ret = -EIO;
return ret;
}
需要注意:
begin_cpu_access()可以失败;vmap()也可以因 exporter 不支持或虚拟地址空间不足而失败;begin_cpu_access()成功后,即使后续处理失败也必须调用end_cpu_access();vmap/vunmap与begin/end_cpu_access解决不同问题,不能互相替代;- 大 buffer 的长期
vmap会消耗内核虚拟地址空间。
旧文中的 dma_buf_kmap()、dma_buf_kmap_atomic() 已不适合作为当前通用接口讲解。高端内存和局部页访问应根据目标内核提供的现代内存映射 API 重新设计。
10.2 用户空间 mmap
如果 exporter 支持 mmap,用户空间可以对 DMA-BUF fd 调用普通 mmap()。每一次 CPU 读写都应由 DMA_BUF_IOCTL_SYNC 包围:
c
#include <errno.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/dma-buf.h>
static int dmabuf_sync(int fd, uint64_t flags)
{
struct dma_buf_sync sync = { .flags = flags };
int ret;
do {
ret = ioctl(fd, DMA_BUF_IOCTL_SYNC, &sync);
} while (ret < 0 && (errno == EINTR || errno == EAGAIN));
return ret;
}
static int write_dmabuf(int fd, size_t size)
{
void *addr;
int ret = -1;
addr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (addr == MAP_FAILED)
return -1;
if (dmabuf_sync(fd, DMA_BUF_SYNC_START | DMA_BUF_SYNC_WRITE) < 0)
goto out_unmap;
/* 在 [addr, addr + size) 范围内写入数据。 */
if (dmabuf_sync(fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_WRITE) < 0)
goto out_unmap;
ret = 0;
out_unmap:
munmap(addr, size);
return ret;
}
生产代码必须保证 START 成功后,即使 CPU 处理失败也尽力执行匹配的 END;上面为突出主线省略了该状态管理。
DMA_BUF_IOCTL_SYNC 只提供 CPU 映射所需的缓存一致性机会,并不阻止其他进程或设备同时访问。CPU 开始访问前必须先等待 GPU/其他设备的相关工作完成,END 之后才能提交依赖 CPU 写入结果的新设备任务。
11. 缓存一致性与执行同步是两件事
这是 DMA-BUF 最容易混淆的部分:
text
执行同步:某个设备是否已经完成读写?
工具:dma_fence、dma_resv、sync_file、poll、子系统同步对象
缓存一致性:CPU 与设备看到的数据是否一致?
工具:begin/end_cpu_access、DMA_BUF_IOCTL_SYNC、DMA API
即使 fence 已经 signaled,CPU 在 non-coherent 平台上仍可能需要 invalidate cache 才能看到设备写入。反过来,即使做了 cache flush,也不表示仍在运行的 GPU 已停止访问内存。
CPU 访问的一般顺序是:
text
等待先前设备 fence
-> begin CPU access / SYNC_START
-> CPU 读写
-> end CPU access / SYNC_END
-> 提交后续设备任务并建立 fence
12. 隐式同步与显式同步
12.1 隐式同步
隐式同步把 fence 关联到 DMA-BUF 的 dma_resv。支持该模型的驱动在提交任务时:
- 根据读写方向取得并等待所需依赖 fence;
- 创建代表本次异步任务的 fence;
- 以正确 usage 加入 reservation object;
- 在硬件真正完成后 signal fence。
用户空间可以对 DMA-BUF fd 使用 poll()/epoll() 查询隐式 fence:
POLLIN/EPOLLIN用于判断读取所需的先前写是否完成;POLLOUT/EPOLLOUT用于判断写入所需的先前读写是否完成。
poll() 就绪只表示相应 fence 完成;CPU 访问前仍需执行 DMA_BUF_IOCTL_SYNC。
12.2 显式同步
显式同步把 dma_fence 包装为独立的 sync_file fd,由用户空间明确随任务传递。它常用于 Vulkan 等显式同步 API,也用于跨子系统交换完成点。
DMA-BUF UAPI 提供:
DMA_BUF_IOCTL_EXPORT_SYNC_FILE:把 DMA-BUF 当前相关 fence 的快照导出为sync_file;DMA_BUF_IOCTL_IMPORT_SYNC_FILE:把一个sync_filefence 加入 DMA-BUF 的隐式同步状态。
这两个 ioctl 可以桥接显式同步和隐式同步,但"导出依赖→提交任务→导入完成 fence"不是原子操作。多个线程或上下文同时操作时,用户空间还需要额外锁定或协议保证这组步骤之间没有竞态。
13. DMA-Heap:从用户空间分配 DMA-BUF
DMA-Heap 为用户空间提供分配 DMA-BUF 的统一 UAPI。每个 /dev/dma_heap/<name> 节点表示一种分配策略或内存池。例如上游内核常见的 system heap 提供 cacheable、虚拟连续的 buffer;配置 CMA 后还可能出现连续内存 heap。实际节点由内核配置和平台决定。
简化的用户空间分配代码:
c
#include <fcntl.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <linux/dma-heap.h>
static int alloc_from_system_heap(uint64_t size)
{
struct dma_heap_allocation_data data = {
.len = size,
.fd_flags = O_RDWR | O_CLOEXEC,
.heap_flags = 0,
};
int heap_fd;
int ret;
heap_fd = open("/dev/dma_heap/system", O_RDWR | O_CLOEXEC);
if (heap_fd < 0)
return -1;
ret = ioctl(heap_fd, DMA_HEAP_IOCTL_ALLOC, &data);
close(heap_fd);
if (ret < 0)
return -1;
return data.fd;
}
DMA-Heap 只解决"从哪个 heap 分配并取得 DMA-BUF fd"。它不提供图像宽高、格式、stride,也不替调用方完成设备任务同步。
14. 图像 buffer 还需要布局元数据
一个 fd 和一个 size 不足以解释图像内容。跨 DRM、V4L2、EGL、Wayland 或 Vulkan 交换图像时,通常还需要:
- width、height;
- pixel format;
- plane 数量;
- 每个 plane 的 fd、offset 和 stride;
- format modifier;
- color space、range、transfer function 等颜色信息;
- 与读写完成相关的 fence。
多 plane 图像可能所有 plane 共用一个 DMA-BUF,也可能每个 plane 使用独立 fd。Importer 必须验证 offset、stride 和尺寸计算不会越过 buffer 边界;不能仅信任用户空间元数据。
Modifier 描述线性、tiling、压缩等实际内存布局。两个设备都认识同一个四字符格式,不代表它们一定能以相同 modifier 访问该 buffer。
15. 动态映射与内存迁移
简单 importer 通常在 attachment 或 mapping 存活期间固定 backing storage。对于 GPU VRAM 等需要迁移的内存,这会限制内存管理器回收和移动资源。
动态 importer 可以使用 dma_buf_dynamic_attach() 和 importer attach ops,在 exporter 准备移动 backing storage 时接收 mapping 失效通知,然后销毁旧 mapping,并在下一次设备访问前重新 map。
动态路径需要严格遵守:
dma_resv锁规则;- fence 等待与发布规则;
- mapping 失效回调的时限;
pin/unpin的适用范围;- peer-to-peer memory 是否有
struct page。
这不是普通 importer 的必选优化。除非驱动确实需要可迁移内存并理解所有同步约束,否则先实现静态、生命周期清晰的 attachment/mapping 更容易保证正确性。
16. 锁与错误处理
DMA-BUF、DMA reservation 和各子系统锁会形成跨驱动锁依赖。实现时必须阅读目标内核文档中的 DMA-BUF locking convention。
关键原则:
- 不要在 core 已持有
dma_resv锁调用的 exporter callback 中再次获取同一锁; - 普通
map/unmap、vmap/vunmaphelper 要求 importer 已持有 reservation 锁;对应的_unlockedhelper 要求调用时未持锁,并由 helper 处理需要的加锁; attach、map、begin_cpu_access、vmap和用户态 ioctl 都可能失败;dma_buf_get()、dma_buf_attach()和dma_buf_map_attachment()返回错误指针时使用IS_ERR()/PTR_ERR();- 清理路径必须按获取资源的逆序执行;
- 不能在原子上下文调用可能 sleep 的回调;
map_dma_buf()可能因分配或迁移而睡眠。
推荐把 importer 状态明确拆成:
text
EMPTY -> GOT_DMABUF -> ATTACHED -> MAPPED -> IN_FLIGHT
^ | | | |
+----------+------------+----------+-----------+
每次错误都按逆序回退
不要用一个布尔变量同时表示"已经 attach""已经 map"和"硬件已经完成"。
17. 调试、统计与安全
17.1 查看 fd 与 DMA-BUF 信息
对于持有 DMA-BUF fd 的进程,可以先检查:
bash
ls -l /proc/<pid>/fd
cat /proc/<pid>/fdinfo/<fd>
DMA-BUF 的 fdinfo 通常包含 size、引用相关计数和 exp_name 等字段,具体内容随内核和 exporter 变化。
启用 debugfs 的开发系统还可能提供:
bash
cat /sys/kernel/debug/dma_buf/bufinfo
debugfs 不应作为稳定生产 ABI,也不应仅为采集数据就在生产环境放宽挂载权限。某些内核在启用相应配置时还提供 DMA-BUF sysfs statistics;路径和字段应以目标内核的 ABI 文档为准。
Exporter 和用户空间可以为 DMA-BUF 设置有意义的名称,便于定位泄漏和统计,但名称只用于调试,不能作为权限或身份判断依据。
17.2 常见安全检查
- 创建或接收 fd 时使用
O_CLOEXEC/MSG_CMSG_CLOEXEC; - 把 DMA-BUF fd 当作访问内存的能力句柄,只传给确实需要的进程;
- 分配给用户空间或其他安全域前清除旧内容,防止信息泄漏;
- 验证 buffer size、plane offset、stride、长度加法和整数溢出;
- 验证设备 DMA mask、IOMMU domain 和访问权限;
- 不要信任用户提供的 fd 一定是 DMA-BUF,
dma_buf_get()失败必须正常返回; - 确保异常、进程退出和设备 reset 路径最终都能释放引用并 signal 或终止相关 fence。
18. 原文中需要纠正的几个结论
| 原表述 | 更准确的理解 |
|---|---|
| DMA-BUF 在物理内存分配后导出 | backing storage 由 exporter 管理,可以提前分配,也可能延迟分配、迁移或位于设备内存 |
dma_buf 绑定文件描述符 |
dma_buf 关联 struct file;fd 是每个进程 fd 表中的句柄,可以有多个 |
| 引用计数表示消费者数量 | 引用计数统计文件和内核引用,不等于 attachment 或业务消费者数量 |
| attachment 是消费者对象 | attachment 是 DMA-BUF 与某个 struct device 的关系 |
| scatterlist 是按页的连续物理块 | map 后的 sg_table 是面向具体设备的 DMA 段描述,可能受 IOMMU 和 DMA API 合并影响 |
| unmap 通知 DMA 结束 | 驱动必须先确认硬件结束;unmap 只释放 mapping |
| begin/end 负责所有同步 | begin/end 主要解决 CPU 可见性与缓存一致性,硬件执行顺序仍需 fence 或子系统同步机制 |
kmap/kmap_atomic 是当前通用访问接口 |
当前 DMA-BUF 整体内核映射使用 vmap/vunmap 和 iosys_map;旧接口不应直接套用 |
19. 建议的验证实验
实验一:验证 fd 传递与引用生命周期
使用 DMA-Heap 分配一块 DMA-BUF,进程 A 通过 SCM_RIGHTS 发送给进程 B:
- A 分配并设置 DMA-BUF 名称;
- A 通过
mmap + DMA_BUF_IOCTL_SYNC写入固定模式; - A 用 Unix domain socket 发送 fd;
- B 映射并校验内容;
- A 关闭自己的 fd,B 再次读取;
- 分别观察两个进程的
/proc/<pid>/fdinfo/<fd>。
预期结果:B 收到的 fd 数字不一定与 A 相同;A 关闭 fd 后,只要 B 仍持有引用,buffer 就不会释放。
实验二:记录 importer 的资源状态
在一个可控的测试驱动中为 get -> attach -> map -> submit -> complete -> unmap -> detach -> put 每一步记录 tracepoint 或动态调试日志,并注入 attach 失败、map 失败和设备超时。
检查每个失败点是否都按逆序清理,重点确认:
- map 失败时没有调用 unmap;
- 已 map 但未提交时仍会 unmap;
- 已提交任务必须等硬件停止访问后才能 unmap;
- 任意路径最终都恰好执行一次 detach 和 put。
实验三:区分 fence 等待与缓存同步
在具有真实 non-coherent DMA 设备的平台上,分别记录:
- 设备任务 fence 何时 signaled;
- CPU 的 begin/end access 何时执行;
- 数据校验何时进行。
正确顺序应是先等待设备完成,再开始 CPU access。不要在生产设备上故意省略缓存同步来"证明会出错",因为 coherent 平台可能掩盖问题,non-coherent 平台上的结果也可能是非确定的数据损坏。
20. 小结
DMA-BUF 的主线可以归纳为:exporter 管理存储并导出对象,用户空间传递 fd,importer 为具体设备建立 attachment 和 DMA mapping,fence/reservation 管理异步访问顺序,begin/end 协议管理 CPU 缓存一致性,最后所有资源按相反顺序释放。
排查问题时,应始终分别回答四个问题:
- 谁持有 buffer 的引用,生命周期是否正确?
- 当前地址属于 CPU 虚拟、物理还是设备 DMA/IOVA 空间?
- 上一次异步读写是否已经通过 fence 确认完成?
- CPU 与设备之间是否完成了正确方向的缓存同步?
把这四点分开,DMA-BUF 中大多数"偶尔花屏""某平台正常、另一平台错误""buffer 无法释放"和 IOMMU fault 问题都会更容易定位。