3. DMA-BUF 基础:设计哲学、核心数据结构与生命周期管理
DMA-BUF 是 Linux 内核中用于在设备驱动(如 GPU、VPU、ISP)之间共享 DMA 缓冲区的标准框架。它解决了传统驱动间通过 V4L2、DRM GEM 私有接口进行内存拷贝或句柄传递的低效与耦合问题。本章深入剖析其设计哲学、核心数据结构及严谨的生命周期管理机制。
3.1 设计哲学:零拷贝与所有权分离
DMA-BUF 的核心设计目标是在不产生 CPU 拷贝的前提下,实现多设备对同一物理内存区域的并发访问。其哲学体现在三个关键原则:
- 缓冲区即对象:将一块连续的物理内存(或 SG 表描述的分散内存)抽象为一个内核对象
struct dma_buf,该对象拥有底层内存的所有权。 - 访问即附着:任何希望访问该缓冲区的设备驱动,必须通过
dma_buf_attach()创建一个附着对象struct dma_buf_attachment,代表该设备与缓冲区的绑定关系。 - 映射即协商:附着后,驱动通过
dma_buf_map_attachment()获取针对该设备 IOMMU/MMU 视角的sg_table(分散/聚集表),实现设备虚拟地址与物理页的映射。映射参数(如读写权限、缓存一致性)由附着设备与导出者协商。
核心优势: 生产者(Exporter)与消费者(Importer)无需了解对方的内存管理细节,仅通过文件描述符(fd)传递缓冲区句柄,实现跨驱动、跨进程的零拷贝共享。
3.2 核心数据结构
DMA-BUF 框架由三个核心结构体支撑,它们共同构成了从“内存拥有者”到“内存使用者”的完整链路。
3.2.1 struct dma_buf —— 缓冲区对象
这是整个框架的根对象,代表一块被共享的 DMA 缓冲区。它由导出者(Exporter)创建,并关联一个文件描述符供用户空间或其它驱动使用。
| 成员 | 类型 | 描述 |
|---|---|---|
size | size_t | 缓冲区大小(字节) |
file | struct file * | 关联的匿名文件,用于引用计数和 fd 传递 |
attachments | struct list_head | 挂载所有附着者的链表头 |
ops | const struct dma_buf_ops * | 导出者提供的操作函数表(如 attach, detach, map_dma_buf, unmap_dma_buf, release) |
priv | void * | 导出者的私有数据指针,通常指向其 GEM 对象或 V4L2 缓冲区 |
resv | struct dma_resv * | 预留对象,管理缓冲区的跨设备同步(隐式 fencing) |
3.2.2 struct dma_buf_attachment —— 附着点
每个想要使用该缓冲区的设备驱动都需要创建一个附着点。它记录了设备特定的映射需求和状态。
| 成员 | 类型 | 描述 |
|---|---|---|
dev | struct device * | 附着设备的指针 |
dmabuf | struct dma_buf * | 指向被附着的缓冲区对象 |
priv | void * | 附着设备的私有数据(如 IOMMU domain 信息) |
sgt | struct sg_table * | 当前映射的 SG 表,由 dma_buf_map_attachment() 填充 |
dir | enum dma_data_direction | DMA 方向(DMA_TO_DEVICE, DMA_FROM_DEVICE, DMA_BIDIRECTIONAL) |
3.2.3 struct sg_table —— 分散/聚集表
这是设备可寻址的物理内存描述表。DMA-BUF 不要求底层内存物理连续,SG 表通过 struct scatterlist 链表描述每一段物理连续区域(页块)的基址和长度。
struct sg_table {
struct scatterlist *sgl; // SG 链表头
unsigned int nents; // 实际 DMA 映射的条目数
unsigned int orig_nents; // 原始 SG 条目数
};
关键理解:
dma_buf_map_attachment() 返回的 sg_table 已经过 IOMMU 转换,其中的地址是设备总线地址(Device Bus Address),而非 CPU 物理地址。这是设备 DMA 引擎能够直接使用的地址。
3.3 生命周期管理:从创建到销毁
DMA-BUF 的生命周期严格遵循“引用计数 + 显式附着/分离”的模型,确保在并发环境下不会出现内存泄漏或野指针。
- 创建(Export):导出者调用
dma_buf_export(),传入struct dma_buf_export_info(包含 ops、size、flags、priv 等)。内核创建dma_buf对象并关联一个匿名文件,引用计数初始化为 1。 - 获取 fd:导出者通过
dma_buf_fd()获取文件描述符,传递给用户空间或通过内核内部接口传递给其他驱动。每次fdget()会增加文件引用计数。 - 附着(Attach):使用者驱动调用
dma_buf_attach(),内核创建dma_buf_attachment并加入dmabuf->attachments链表。此时缓冲区引用计数 +1。 - 映射(Map):使用者调用
dma_buf_map_attachment(),导出者的map_dma_buf回调返回sg_table。使用者驱动使用该表编程 DMA 引擎。 - 解映射(Unmap):DMA 传输完成后,使用者调用
dma_buf_unmap_attachment(),释放设备地址映射。 - 分离(Detach):使用者不再需要缓冲区时,调用
dma_buf_detach(),从链表移除附着点,缓冲区引用计数 -1。 - 释放(Release):当所有 fd 被关闭且所有附着者都已分离,引用计数归零,内核调用
dma_buf_ops->release()回调,由导出者释放底层物理内存。
常见陷阱: 使用者必须在 DMA 传输完全停止后才能调用
dma_buf_unmap_attachment(),否则可能导致设备访问已释放的总线地址。此外,dma_buf_attach() 和 dma_buf_detach() 必须成对出现,否则会造成引用泄漏。
3.4 生命周期与数据流 SVG 架构图
下图展示了从 GPU 导出缓冲区到 VPU 附着、映射、使用并最终释放的完整生命周期与数据流。
3.5 关键设计决策:为什么需要 attachment?
直接让每个使用者获取 dma_buf 指针并自行映射不是更简单吗?attachment 层的引入解决了三个核心问题:
- 设备隔离:不同设备的 IOMMU 地址空间不同,attachment 为每个设备保存独立的映射状态和私有数据。
- 引用跟踪:内核通过 attachment 链表精确知道当前有多少设备正在使用该缓冲区,从而安全地管理内存释放时机。
- 缓存一致性协商:附着时,使用者可以声明其缓存一致性需求(如是否需要 CPU 缓存刷新),导出者据此调整映射属性。
性能提示: 在 Vblank 中断与 DMA-BUF 联合调优场景中,频繁的
map/unmap 会引入 IOMMU TLB 刷新开销。对于高帧率场景,建议在驱动初始化时一次性 map,并在会话期间保持映射,仅在帧切换时通过 fence 机制同步,而非反复 map/unmap。