4. DMA-BUF 核心概念

在 DRM 与 V4L2 的跨设备协作中,DMA-BUF 是连接“显存”和“摄像头/编解码器”的桥梁。它解决的核心问题是:一块物理内存如何在不复制数据的前提下,被多个设备驱动同时访问。理解 DMA-BUF,必须抓住四个关键词:缓冲区共享、exporter/importer、scatterlist、dma_addr。它们分别回答了“共享什么”“谁提供谁使用”“内存长什么样”“设备看到什么地址”这四个问题。

一、缓冲区共享:从“拷贝”到“引用”

传统做法中,GPU 渲染完一帧图像,若要让视频编码器读取,往往需要把数据从 GPU 缓冲区拷贝到 CPU 可访问内存,再拷贝到编码器缓冲区。两次拷贝不仅消耗带宽,还增加延迟。DMA-BUF 的思路是:内存只分配一次,各方通过文件描述符(fd)引用同一块缓冲区。

DMA-BUF 对外暴露为一个匿名文件描述符。任何持有该 fd 的进程或驱动,都可以通过 dma_buf_get() 获取 struct dma_buf,进而调用 attach、map 等操作。共享的是“缓冲区对象”,而不是“数据副本”。因此,DMA-BUF 天然适合零拷贝管线:GPU 导出 → V4L2 导入 → 编码器读取。

核心价值:一次分配、多方引用、零拷贝传递。fd 是跨进程、跨驱动传递缓冲区的“通行证”。

二、exporter 与 importer:角色与职责

DMA-BUF 生态中有两个角色:exporter(导出者)和importer(导入者)。exporter 是缓冲区的实际分配者和管理者,通常是 DRM 驱动(如 GPU、显示控制器)或专门的 DMA-BUF 堆(如 system heap、cma heap)。它负责实现 struct dma_buf_ops,包括 attach、map_dma_buf、unmap_dma_buf、release 等回调。

importer 是缓冲区的使用者,通常是 V4L2 驱动、视频编解码器或另一个 DRM 设备。它通过 dma_buf_attach() 把自身设备挂到缓冲区上,再通过 dma_buf_map_attachment() 获取该设备可用的 scatterlist,最终得到设备可访问的 DMA 地址。

角色 典型代表 关键操作 职责
exporter DRM GPU 驱动、dma-heap dma_buf_export、实现 dma_buf_ops 分配内存、提供 sg_table、管理生命周期
importer V4L2 驱动、编解码器 dma_buf_attach、map_attachment 获取 sg_table、建立设备映射、访问数据

三、scatterlist:内存的“碎片地图”

现代系统内存往往不是物理连续的。一块 DMA-BUF 背后可能由许多离散的物理页组成。为了让设备正确访问,驱动需要知道这些页的物理地址和长度。scatterlist(分散聚集列表)就是描述这种“非连续物理内存”的数据结构。

每个 struct scatterlist 记录一段物理内存的起始地址、长度和 DMA 地址。多个 scatterlist 组成 struct sg_table,其中 sgl 指向数组,nents 表示条目数,orig_nents 表示原始条目数。exporter 在 map_dma_buf 回调中返回 sg_table,importer 拿到后即可进行 DMA 映射。

struct sg_table {
    struct scatterlist *sgl;   /* scatterlist 数组 */
    unsigned int nents;        /* 映射后的条目数 */
    unsigned int orig_nents;   /* 原始条目数 */
};

/* 典型遍历方式 */
struct scatterlist *sg;
for_each_sg(sg_table->sgl, sg, sg_table->nents, i) {
    dma_addr_t addr = sg_dma_address(sg);
    unsigned int len = sg_dma_len(sg);
    /* 将 addr/len 配置给设备 */
}

需要特别注意:sg_table 中的物理地址是 CPU 视角的,设备不能直接使用。必须经过 DMA 映射,才能得到设备可用的 DMA 地址。

四、dma_addr:设备眼中的地址

dma_addr_t 是设备用于访问内存的总线地址。在存在 IOMMU 的系统中,它不等于物理地址,而是 IOMMU 映射后的 IOVA。DMA-BUF 的 importer 在 map_attachment 时,会调用 dma_map_sg() 把 sg_table 映射为设备可用的 DMA 地址,并填充到每个 scatterlist 的 dma_address 字段中。

因此,从 exporter 到 importer,地址经历了一次“视角转换”:

  • exporter 视角:物理页帧号(PFN)或虚拟地址;
  • sg_table 视角:离散物理段的集合;
  • importer 视角:经过 IOMMU 映射的 dma_addr,设备可直接读写。
调试时若设备读到错误数据,优先检查 dma_addr 是否经过正确映射,以及 sg_table 的 nents 是否与设备期望一致。

五、整体数据流与关系图

下图展示了从 exporter 分配缓冲区,到 importer 获取 DMA 地址的完整链路。注意 fd 是跨模块传递的句柄,sg_table 是内存描述的载体,dma_addr 是设备最终使用的地址。

Exporter DRM GPU / dma-heap dma_buf_export() 实现 dma_buf_ops 分配物理页 DMA-BUF struct dma_buf fd 文件描述符 sg_table 内存描述 引用计数管理 Importer V4L2 / 编解码器 dma_buf_attach() map_attachment() 获取 dma_addr export import sg_table sgl → scatterlist[] nents / orig_nents 离散物理页描述 map_dma_buf 返回 dma_addr dma_map_sg() 映射 IOMMU / IOVA 设备可直接访问 映射转换 数据流:Exporter 分配内存 → 导出 fd → Importer 获取 fd → attach → map → 得到 dma_addr → 设备 DMA 读写 全程零拷贝,fd 是跨驱动传递的句柄,sg_table 是内存描述,dma_addr 是设备访问地址 分配者 / 管理者 使用者 / 映射者

六、常见误区与复盘

误区一:把 sg_table 的物理地址直接给设备用。物理地址是 CPU 视角的,设备必须使用经过 DMA 映射的 dma_addr。在无 IOMMU 的系统中两者可能相同,但在有 IOMMU 时完全不同。

误区二:认为 DMA-BUF 一定物理连续。DMA-BUF 只保证逻辑上是一块缓冲区,物理上完全可以是离散的。驱动必须通过 sg_table 遍历,而不能假设连续。

误区三:忘记 attach 就 map。importer 必须先 dma_buf_attach(),再 dma_buf_map_attachment()。attach 建立设备与缓冲区的关联,map 才真正建立 DMA 映射。

误区四:生命周期管理混乱。fd 关闭不代表缓冲区立即释放,引用计数归零才释放。importer 使用完毕后必须 dma_buf_unmap_attachment() 和 dma_buf_detach(),否则会造成泄漏或悬空映射。

排障建议:若 V4L2 设备读取到花屏或全黑,依次检查 fd 是否有效、attach 是否成功、sg_table 的 nents 是否正确、dma_addr 是否经过 IOMMU 映射、以及 cache 一致性是否需要 sync。

总结来说,DMA-BUF 用 fd 实现跨设备共享,用 exporter/importer 划分职责,用 scatterlist 描述离散内存,用 dma_addr 完成设备视角的地址转换。掌握这四者之间的关系,是后续把 DRM PRIME 缓冲区导出并导入 V4L2 设备的理论基础。