ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

3.5 设备间视图:dma_buf、import_attach 与 resv

3.5 设备间视图:dma_buf、import_attach 与 resv

本文是drm_gem_object三类访问视图中的第三类(三类视图的划分见 GEM 概述)。CPU 视图与 GPU 视图的访问方都属于本驱动:CPU 经本驱动的 mmap 路径访问,GPU 经本驱动维护的页表访问。设备间视图的访问方属于另一个驱动,GEM 无法替对方建立映射,只能把对象包装成 dma-buf 交给对方,由对方自行 attach 与映射。

本文作为共享机制的概述,只讲 dma-buf 的基本原理、导出与导入双方的职责,以及承载这一视图的字段。对象与 dma-buf 之间的引用关系等细节留给第 6 章。

1. 需求:把对象交给另一个驱动

要让另一个驱动访问本驱动的对象,GEM 需要回答四个问题:

需求说明承载字段
共享身份对象要有一个能跨进程、跨设备传递的凭证,同一对象反复导出时应得到同一个凭证dma_buf(遗留方案为name)
来源记录从外部导入的对象没有自己的后备存储,要记住内容来自哪个 dma-buf,再导出和释放时才能正确处理import_attach
访问同步多个设备访问同一块内存,要能看到彼此尚未完成的访问resv
身份的生命期用户态不再持有对象时,共享身份应随之清理,而不是让对象一直存活由handle_count驱动

2. 字段一览

structdrm_gem_object{...structdma_buf*dma_buf;/* 关联的 dma-buf(导出或导入) */structdma_buf_attachment*import_attach;/* 导入外部 dma-buf 时的 attachment */structdma_resv*resv;/* 实际使用的 reservation 对象 */structdma_resv_resv;/* 内嵌的 reservation 对象 */...};
字段何时设置指向何时清除 / 失效
dma_buf首次导出时;导入时若对象尚未关联 dma-buf,也会设置与本对象一一对应的 dma-buf,并持有它的一个引用用户态关闭最后一个 handle 时
import_attach导入外部dma-buf 时本设备对该 dma-buf 的 attachment对象生命周期内不变,对象释放时清理
resv对象初始化时指向内嵌的_resv;导入时改指 dma-buf 的resv本对象实际使用的dma_resv随对象释放

两个容易混淆的点:

  • dma_buf不区分方向。字段注释写明它记录的是“通过导入或导出”关联的 dma-buf:导出时设置,导入时若对象还没有关联 dma-buf 也会设置。
  • 判断“是否导入对象”看import_attach。drm_gem_is_imported()的实现就是检查它是否非空。自导入(dma-buf 正是本设备导出的)直接返回原对象,不建立 attachment,因此该对象仍是本地对象。

3. dma_buf:对象的共享身份

3.1 dma-buf 的原理:文件与存储分离

dma-buf 本身是一个匿名文件(struct file),用户态拿到的 fd 指向它。文件里不放存储:存储仍属于导出方,dma_buf->priv指回导出方的对象,dma_buf->ops是导出方提供的回调表,其他驱动对存储的操作都经ops转回导出方完成。这种分离带来两点好处:

  • fd 就是凭证:只有持有者才能导入,可经SCM_RIGHTS在进程间传递,寿命由文件引用计数管理;
  • 存储形态对导入方透明:无论存储是 shmem 页、VRAM 还是连续的 DMA 内存,导入方拿到的都只是为自己设备映射好的 DMA 地址。

导出方与导入方的分工如下图所示:

导出方只做一件事:把对象包装成 dma-buf,并提供一张回调表。此后,其他驱动对存储的请求都经 dma-buf 转给这张表,由导出方自己决定如何准备存储、如何生成地址。

导入方拿到 fd 后分三步:

  • 先 attach,向导出方登记本设备;
  • 再 map,取得本设备可用的 DMA 地址,形式是一张sg_table;
  • 最后据此建立自己的 GEM 对象。这个对象不分配存储,只把这次 attach 记在import_attach中,再导出、释放时据此找回原始 dma-buf。此后导入方设备按 DMA 地址直接访问导出方的存储,数据不经拷贝。

attach 之所以要登记设备,一是让导出方检查该设备能否访问这块存储(例如存储在 VRAM 中而设备无法直接访问,又不能迁回系统内存时,attach 失败);二是同一块存储经不同设备的 IOMMU 看到的 DMA 地址可能不同,map 得到的地址只对登记的那个设备有效。

3.2 由哪两个 ioctl 设置

用户态通过 PRIME 的两个 ioctl 在 handle 与 dma-buf fd 之间互转。两者都带DRM_RENDER_ALLOW,可在 render node 上使用。它们对本文字段的影响如下:

ioctl方向对字段的影响
DRM_IOCTL_PRIME_HANDLE_TO_FDhandle → dma-buf fd首次导出时创建 dma-buf 并写入obj->dma_buf;对象已有dma_buf时直接复用;导入对象(import_attach非空)直接交出原始 dma-buf
DRM_IOCTL_PRIME_FD_TO_HANDLEdma-buf fd → handle外部 dma-buf:新建对象并设置import_attach、resv(见 3.1 与第 4 节);本设备导出的 dma-buf:直接返回原对象。对象的dma_buf为空时设为该 dma-buf

所以同一对象只对应一个 dma-buf,obj->dma_buf就是「再次导出时复用」的缓存。另外,drm_file.prime中还有一份按drm_file的缓存,使同一 dma-buf 在同一drm_file上反复导入时返回同一个 handle;由于 handle 在用户态没有引用计数,这个 handle 只能关闭一次(见 gem_object 生命周期 第 8 节)。

3.3 对照:遗留的 name 与 FLINK

PRIME 出现之前,同一设备上的跨进程共享靠 FLINK:DRM_IOCTL_GEM_FLINK把 handle 换成一个设备级的全局整数名,记在obj->name中;DRM_IOCTL_GEM_OPEN凭名字取回 handle。name与dma_buf同属“handle 之外的另一种身份”,同受drm_device.object_name_lock保护,也在handle_count归零时由drm_gem_object_handle_free()移除。

FLINK 被 PRIME 取代,根本原因在于凭证的性质:名字是全局整数,谁猜到谁就能打开对象,只在本设备内有效,也不能在 render node 上使用;dma-buf fd 只有持有者才能导入,可以跨设备传递,引用计数由文件机制管理。两个 ioctl 的细节见 3.6。

4. resv:三个对象共用一个 dma_resv

resv字段的注释是:“通常resv == &_resv,导入对象除外”。导入时,drm_gem_prime_import_dev()在驱动建好对象后同时设置import_attach与resv:

obj=dev->driver->gem_prime_import_sg_table(dev,attach,sgt);...obj->import_attach=attach;obj->resv=dma_buf->resv;

结合导出与导入两侧:

  • 导出时,drm_gem_prime_export()以.resv = obj->resv创建 dma-buf,dma-buf 直接使用对象的dma_resv;
  • 导入时,导入方对象的resv改指dma_buf->resv,其内嵌的_resv不再使用。

因此,导出方对象、dma-buf、导入方对象三者共用同一个dma_resv实例。任一设备在提交访问时向其中添加的 fence,其他设备在访问前都能看到,跨设备的隐式同步由此成立。drm_gem_map_attach()在调用funcs->pin前也是对obj->resv加锁,说明这个对象同时承担了“同步容器”与“对象锁”两种角色。fence 的使用规则见 dma_resv。

5. 与另外两类视图的衔接

设备间视图并不独立于另外两类视图,它们在以下几处交汇:

  • 经 dma-buf fd 的 mmap:用户态也可以直接 mmap dma-buf fd。GEM 把请求换算成对象的伪偏移后,交给本驱动原有的 mmap 路径处理,最终与经设备节点 mmap 走同一套 CPU 视图。
  • 同一个 address_space:导出时,dma-buf 文件被指向设备的address_space。因此经 dma-buf fd 建立的映射与经设备节点建立的映射挂在一起,驱动撤销映射时一并处理(见 3.1.1 与附录 B.8)。
  • 内核态 CPU 访问:对端驱动可以经 dma-buf 接口请求一个内核虚拟地址,请求最终转到导出方对象的 vmap 回调。
  • 导入对象的 GPU 访问:导入后对象在本进程有了 handle,可以像本地对象一样绑定到本设备的 GPU 虚拟地址。

由此可见,三类视图共用同一个对象身份:CPU 视图与 GPU 视图回答“本驱动如何访问”,设备间视图回答“如何把同一个对象交给其他驱动”;导入方拿到对象之后,又会在自己一侧重新建立 CPU 视图与 GPU 视图。

6. 小结

  • 设备间视图把对象包装成 dma-buf 交给其他驱动:导出方提供存储与回调表,导入方 attach 并 map,取得本设备可用的 DMA 地址后建立自己的对象。
  • dma_buf是对象的共享身份,同一对象只对应一个 dma-buf;判断对象是否从外部导入,看import_attach。
  • 用户态经 PRIME 的两个 ioctl 在 handle 与 dma-buf fd 之间互转;FLINK 仅用于遗留兼容,不能在 render node 上使用。
  • 导出方对象、dma-buf、导入方对象共用同一个dma_resv,跨设备的隐式同步由此成立。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进