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)创建,并关联一个文件描述符供用户空间或其它驱动使用。

成员类型描述
sizesize_t缓冲区大小(字节)
filestruct file *关联的匿名文件,用于引用计数和 fd 传递
attachmentsstruct list_head挂载所有附着者的链表头
opsconst struct dma_buf_ops *导出者提供的操作函数表(如 attach, detach, map_dma_buf, unmap_dma_buf, release)
privvoid *导出者的私有数据指针,通常指向其 GEM 对象或 V4L2 缓冲区
resvstruct dma_resv *预留对象,管理缓冲区的跨设备同步(隐式 fencing)

3.2.2 struct dma_buf_attachment —— 附着点

每个想要使用该缓冲区的设备驱动都需要创建一个附着点。它记录了设备特定的映射需求和状态。

成员类型描述
devstruct device *附着设备的指针
dmabufstruct dma_buf *指向被附着的缓冲区对象
privvoid *附着设备的私有数据(如 IOMMU domain 信息)
sgtstruct sg_table *当前映射的 SG 表,由 dma_buf_map_attachment() 填充
direnum dma_data_directionDMA 方向(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 的生命周期严格遵循“引用计数 + 显式附着/分离”的模型,确保在并发环境下不会出现内存泄漏或野指针。

  1. 创建(Export):导出者调用 dma_buf_export(),传入 struct dma_buf_export_info(包含 ops、size、flags、priv 等)。内核创建 dma_buf 对象并关联一个匿名文件,引用计数初始化为 1。
  2. 获取 fd:导出者通过 dma_buf_fd() 获取文件描述符,传递给用户空间或通过内核内部接口传递给其他驱动。每次 fdget() 会增加文件引用计数。
  3. 附着(Attach):使用者驱动调用 dma_buf_attach(),内核创建 dma_buf_attachment 并加入 dmabuf->attachments 链表。此时缓冲区引用计数 +1。
  4. 映射(Map):使用者调用 dma_buf_map_attachment(),导出者的 map_dma_buf 回调返回 sg_table。使用者驱动使用该表编程 DMA 引擎。
  5. 解映射(Unmap):DMA 传输完成后,使用者调用 dma_buf_unmap_attachment(),释放设备地址映射。
  6. 分离(Detach):使用者不再需要缓冲区时,调用 dma_buf_detach(),从链表移除附着点,缓冲区引用计数 -1。
  7. 释放(Release):当所有 fd 被关闭且所有附着者都已分离,引用计数归零,内核调用 dma_buf_ops->release() 回调,由导出者释放底层物理内存。
常见陷阱: 使用者必须在 DMA 传输完全停止后才能调用 dma_buf_unmap_attachment(),否则可能导致设备访问已释放的总线地址。此外,dma_buf_attach() 和 dma_buf_detach() 必须成对出现,否则会造成引用泄漏。

3.4 生命周期与数据流 SVG 架构图

下图展示了从 GPU 导出缓冲区到 VPU 附着、映射、使用并最终释放的完整生命周期与数据流。

DMA-BUF 生命周期与数据流架构 GPU (Exporter) dma_buf_export() struct dma_buf size, file, ops, resv VPU (Importer) dma_buf_attach() dma_buf_attachment dev, dir, sgt sg_table 设备总线地址列表 物理内存页 (可能不连续) 创建 fd 传递 attach() map_attachment() DMA 引擎访问 release() 释放 关键生命周期规则 • attach/detach 必须成对 • map/unmap 必须成对 • 引用归零前不会释放物理内存

3.5 关键设计决策:为什么需要 attachment?

直接让每个使用者获取 dma_buf 指针并自行映射不是更简单吗?attachment 层的引入解决了三个核心问题:

  • 设备隔离:不同设备的 IOMMU 地址空间不同,attachment 为每个设备保存独立的映射状态和私有数据。
  • 引用跟踪:内核通过 attachment 链表精确知道当前有多少设备正在使用该缓冲区,从而安全地管理内存释放时机。
  • 缓存一致性协商:附着时,使用者可以声明其缓存一致性需求(如是否需要 CPU 缓存刷新),导出者据此调整映射属性。
性能提示: 在 Vblank 中断与 DMA-BUF 联合调优场景中,频繁的 map/unmap 会引入 IOMMU TLB 刷新开销。对于高帧率场景,建议在驱动初始化时一次性 map,并在会话期间保持映射,仅在帧切换时通过 fence 机制同步,而非反复 map/unmap。