news 2026/9/3 15:43:47

5.3.1 PRIME 的两座桥:GEM ↔ dma-buf 桥接回调与 fd/handle 转换

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
5.3.1 PRIME 的两座桥:GEM ↔ dma-buf 桥接回调与 fd/handle 转换

5.2 节 分析 了dma-buf 这套「跨设备缓冲区共享」的通用机制,但它始终停在 dma-buf 自己的抽象层。回到 DRM/GEM 的场景:用户态访问 BO 的接口是一个进程内的 GEMhandle(一个 32 位整数),渲染时用 handle 引用 BO,提交命令时用 handle 索引。可 handle 只在本进程本设备有意义,一旦要把 BO 交给另一个进程、另一块 GPU,就必须换成能跨进程传递的 dma-buffd。然后 fd 要变成importer 进程中的 handle 才能被用户态访问。

PRIME 就是 GEM 世界与 dma-buf 世界之间的双向桥:它把「进程私有的 handle」翻译成「可传递的 fd」(导出),又把别处传来的「fd」翻译回「本进程的 handle」(导入)。用户态只需两个 ioctl,内核则用一组桥接回调把两套对象模型的生命周期、引用计数、缓存一一对齐。

1. 两个 ioctl,两座桥

用户态的全部入口就是两个对称的 ioctl:

ioctl方向内核入口作用
DRM_IOCTL_PRIME_HANDLE_TO_FD导出drm_prime_handle_to_fd_ioctldrm_gem_prime_handle_to_fd本进程 handle → 可传递的 dma-buf fd
DRM_IOCTL_PRIME_FD_TO_HANDLE导入drm_prime_fd_to_handle_ioctldrm_gem_prime_fd_to_handle别处传来的 fd → 本进程 handle

两个 ioctl 都留了「驱动可覆盖」的回调:若dev->driver->prime_handle_to_fd/prime_fd_to_handle非空则用驱动自己的,否则用 DRM 核心的默认实现。绝大多数驱动直接用默认的。

进程 B / GPU-1

进程 A / GPU-0

HANDLE_TO_FD

SCM_RIGHTS 传递 fd

FD_TO_HANDLE

GEM handle

dma-buf fd

dma-buf fd

GEM handle

同一个 struct dma_buf
同一块后端存储

2. 导出桥:handle → fd

drm_gem_prime_handle_to_fd()本身只做「申请 fd → 拿 dma_buf → 安装 fd」三步:

intdrm_gem_prime_handle_to_fd(structdrm_device*dev,structdrm_file*file_priv,uint32_thandle,uint32_tflags,int*prime_fd){structdma_buf*dmabuf;intfd=get_unused_fd_flags(flags);/* 1. 预留一个空 fd */...dmabuf=drm_gem_prime_handle_to_dmabuf(dev,file_priv,handle,flags);/* 2. handle→dma_buf */...fd_install(fd,dmabuf->file);/* 3. 把 dma_buf 的 file 装到 fd 上 */*prime_fd=fd;return0;}

真正有讲究的是第 2 步drm_gem_prime_handle_to_dmabuf(),它按三种情况决定 dma_buf 从哪来:

/* 情况一:这个 GEM 对象本身就是从别处导入来的 → 复用原始 dma-buf */if(obj->import_attach){dmabuf=obj->import_attach->dmabuf;get_dma_buf(dmabuf);gotoout_have_obj;}/* 情况二:之前已经导出过 → 复用缓存的 dma_buf(不重复创建) */if(obj->dma_buf){get_dma_buf(obj->dma_buf);dmabuf=obj->dma_buf;gotoout_have_obj;}/* 情况三:第一次导出 → 真正创建并注册 dma_buf */dmabuf=export_and_register_object(dev,obj,flags);

这三个分支保证了一条关键不变量——同一个 GEM 对象在其生命周期内只对应唯一一个 dma_buf

  • 情况一import_attach非空):这块 BO 是导入来的「影子对象」,导出它时不能再包一层,而要把最初的原始 dma-buf 原样交出去——否则会出现「dma-buf 套 GEM 套 dma-buf」的荒谬嵌套。
  • 情况二obj->dma_buf已缓存):曾经导出过,直接复用并get_dma_buf加引用,避免同一 BO 产生两个互不相干的 dma_buf。
  • 情况三:首次导出才走export_and_register_object,内部最终调obj->funcs->export(默认是drm_gem_prime_export)真正创建 dma_buf,并把它缓存进obj->dma_buf

drm_gem_prime_export()就是那个「真正创建」的默认实现,它把 GEM 对象填进标准的dma_buf_export_info

structdma_buf*drm_gem_prime_export(structdrm_gem_object*obj,intflags){structdma_buf_export_infoexp_info={.exp_name=KBUILD_MODNAME,.ops=&drm_gem_prime_dmabuf_ops,/* GEM 专用的 dma_buf_ops */.size=obj->size,.flags=flags,.priv=obj,/* dma_buf->priv 指回 GEM 对象 */.resv=obj->resv,/* 共用同一个 reservation object */};returndrm_gem_dmabuf_export(dev,&exp_info);}

两个字段值得记住:priv = obj建立了「dma_buf → GEM 对象」的反向指针(前几节 amdgpu 里gem_to_amdgpu_bo(dma_buf->priv)就靠它);resv = obj->resv让 dma-buf 与 GEM 对象共享同一把同步锁与 fence 容器,这是隐式同步(6.3)能跨设备生效的根基。

ops = &drm_gem_prime_dmabuf_ops则是这座桥的另一半——它把 dma-buf 的通用回调转接到 GEM 的drm_gem_object_funcs

dma_buf_ops 回调GEM 桥接实现最终委派到
attachdrm_gem_map_attachobj->funcs->pin
map_dma_bufdrm_gem_map_dma_bufobj->funcs->get_sg_table
unmap_dma_bufdrm_gem_unmap_dma_buf——
releasedrm_gem_dmabuf_releaseobj引用释放
mmapdrm_gem_dmabuf_mmapobj->funcs->mmap
vmap/vunmapdrm_gem_dmabuf_vmapobj->funcs->vmap

dma-buf core 通过drm_gem_prime_dmabuf_ops敲门,最终都落到某个drm_gem_object_funcs。这也解释了 5.2.4 里 amdgpu 的map_dma_buf为何最终会走到 GEM 的get_sg_table——中间正是这张转接表在牵线。若驱动的get_sg_table未实现,drm_gem_map_attach会直接返回-ENOSYS,即拒绝导出到别的设备。

3. 导入桥:fd → handle

反方向的drm_gem_prime_fd_to_handle()要把一枚陌生的 fd 变成本进程的 handle:

intdrm_gem_prime_fd_to_handle(structdrm_device*dev,structdrm_file*file_priv,intprime_fd,uint32_t*handle){structdma_buf*dma_buf=dma_buf_get(prime_fd);/* fd → struct dma_buf */.../* 1. 查本进程的导入缓存:这个 dma_buf 之前导入过吗? */ret=drm_prime_lookup_buf_handle(&file_priv->prime,dma_buf,handle);if(ret==0)gotoout_put;/* 命中 → 复用旧 handle *//* 2. 没见过 → 调驱动的 import 回调,造一个 GEM 对象 */if(dev->driver->gem_prime_import)obj=dev->driver->gem_prime_import(dev,dma_buf);elseobj=drm_gem_prime_import(dev,dma_buf);.../* 3. 建立 GEM 对象 ↔ dma_buf 的双向引用 */if(!obj->dma_buf){obj->dma_buf=dma_buf;get_dma_buf(dma_buf);}/* 4. 为这个 GEM 对象分配本进程 handle,并写入缓存 */ret=drm_gem_handle_create_tail(file_priv,obj,handle);ret=drm_prime_add_buf_handle(&file_priv->prime,dma_buf,*handle);...}

关键仍是缓存 + 唯一性file_priv->prime是每个 DRM file 私有的双向缓存(dma_buf ↔ handle)。同一个 dma_buf 在同一进程被导入两次,第 1 步就会命中缓存返回同一个 handle——保证「一块共享 buffer 在一个进程里只有一个 handle」。

第 2 步的drm_gem_prime_import()转调核心的drm_gem_prime_import_dev(),它才是真正「把 dma_buf 变成 GEM 对象」的地方,其中藏着一个重要优化:

structdrm_gem_object*drm_gem_prime_import_dev(structdrm_device*dev,structdma_buf*dma_buf,structdevice*attach_dev){/* 自导入优化:如果这个 dma_buf 正是本 dev 自己导出的, * 不必 attach,直接给原 GEM 对象加引用即可 */if(drm_gem_is_prime_exported_dma_buf(dev,dma_buf)){obj=dma_buf->priv;drm_gem_object_get(obj);returnobj;}if(!dev->driver->gem_prime_import_sg_table)returnERR_PTR(-EINVAL);/* 正常路径:attach → map 拿 sg_table → 让驱动据此造 GEM 对象 */attach=dma_buf_attach(dma_buf,attach_dev);get_dma_buf(dma_buf);sgt=dma_buf_map_attachment_unlocked(attach,DMA_BIDIRECTIONAL);obj=dev->driver->gem_prime_import_sg_table(dev,attach,sgt);/* 驱动回调 */obj->import_attach=attach;/* 记住「我是导入来的」及其 attachment */obj->resv=dma_buf->resv;/* 共用源 dma-buf 的 resv → 同步能跨设备生效 */returnobj;}

三个要点串起了前几节:

  1. 自导入优化drm_gem_is_prime_exported_dma_buf):把自己导出的 dma-buf 再导入回来,只增加 GEM 对象引用、不增加 dma-buf 的f_count——避免自己 attach 自己的病态循环。这与第 2 节导出侧「情况一」互为镜像。
  2. attach + map 复用 5.2.4 的机制:导入桥并不自己发明轮子,而是老老实实走dma_buf_attach → dma_buf_map_attachment拿到sg_table,再交给驱动的gem_prime_import_sg_table据此构造一个「影子 GEM 对象」。
  3. import_attach+ 共用resvobj->import_attach标记这是导入对象(第 2 节导出侧据此复用原始 dma-buf);obj->resv = dma_buf->resv让影子对象与源 buffer 共享 fence 容器——这是跨设备隐式同步(6.3)成立的前提。驱动还须在其free回调里调drm_prime_gem_destroy()收尾 attach。

4. 一张全景图:两套对象模型如何对齐

把导出与导入合起来看,PRIME 的本质是维护「GEM 对象 ↔ dma_buf」的一一对应与引用计数:

dma-buf 世界

GEM 世界

export: funcs->export / drm_gem_prime_export

import: gem_prime_import_sg_table

handle ↔ dma_buf 缓存
(file_priv->prime)

fd ↔ handle

drm_gem_object
obj->dma_buf(缓存)
obj->import_attach(若导入)
obj->resv

struct dma_buf
dma_buf->priv = obj
dma_buf->ops = drm_gem_prime_dmabuf_ops
dma_buf->resv = obj->resv

进程内 handle

跨进程 fd

  • priv/dma_buf互指:dma_buf 用priv指回 GEM 对象,GEM 对象用dma_buf缓存反向引用,二者一一对应。
  • resv共用:导出时dma_buf->resv = obj->resv,导入时obj->resv = dma_buf->resv,最终跨设备的两个 GEM 影子对象共享同一个dma_resv——同步语义得以贯通。
  • 两级缓存file_priv->prime管「fd/dma_buf ↔ handle」,obj->dma_buf管「GEM 对象 ↔ dma_buf」,共同保证任何一块共享 buffer 在任一进程、任一方向都不会分裂出多个副本。

5. 小结与延伸

  1. PRIME 是 GEM 与 dma-buf 之间的双向桥HANDLE_TO_FD导出、FD_TO_HANDLE导入,把「进程私有 handle」与「可传递 fd」互相翻译。
  2. drm_gem_prime_dmabuf_ops是转接表:dma-buf 的通用回调(attach/map/mmap/vmap)经它统一落到 GEM 的drm_gem_object_funcsmap_dma_buf最终委派get_sg_table——这正是 5.2.4 amdgpu 那条回调链的中枢。
  3. 唯一性与引用计数是灵魂obj->dma_buf缓存、import_attach复用、file_priv->prime双向缓存、自导入优化、共用resv,五条机制共同保证「一块 buffer 在一个进程里只有一个 handle、一个 GEM 对象只对应一个 dma_buf」。

到这里,PRIME 的核心桥接已经清晰,但还有一个实际问题:gem_prime_import_sg_tableget_sg_tablepin/unpin这些回调,普通驱动要不要每个都自己写一遍?对大量基于系统内存的简单驱动,DRM 提供了开箱即用的GEM shmem helper,把这套共享路径全部实现好了。这正是[5.3.2 GEM shmem helper 的共享路径]的主题。

相关阅读:dma-buf 的 attach/map 机制见 5.2.4;get_sg_table/pin/unpin等 GEM 回调的语义见 gem_object_funcs.md;共用resv带来的隐式同步见 6.3 dma_resv。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 15:37:17

WPF悬浮球实现原理与工业级落地指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 15:34:14

TLPI 第32章 读书笔记:Threads: Thread Cancellation

笔记和练习博客总目录见:开始读TLPI。 通常,多个线程会并行执行,每个线程执行自己的任务,直到它通过调用 pthread_exit() 终止,或者从线程的启动函数返回。 有时候,取消线程可能会很有用,也就…

作者头像 李华
网站建设 2026/9/3 15:33:20

大型赛事投票总出幺蛾子?稳定性 + 防刷 + 全流程避坑指南

参与过好几次大型赛事的投票环节,从城市少儿才艺大赛、行业十大人物评选到十万人级的品牌人气票选,踩过的坑太多了:有一次赛事刚到决赛投票高峰,1 小时内流量翻了 5 倍,页面直接崩了,选手和粉丝炸锅&#x…

作者头像 李华
网站建设 2026/9/3 15:32:45

Three.js 颜色管理:别让你的 3D 场景忽明忽暗、颜色发灰

Three.js 颜色管理:别让你的 3D 场景忽明忽暗、颜色发灰 原文出处:Three.js Manual – Color Management 本文基于官方手册「Color Management」章节整理,用通俗方式带你看懂:为什么同样是红色,贴进场景里就变暗了&…

作者头像 李华
网站建设 2026/9/3 15:32:35

从一张商品图到全套电商视觉内容:AI生成工作流实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 15:31:36

MinimaxH3音频驱动数字人:口型同步原理与ComfyUI本地部署实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华