ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

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

2026/9/3 15:43:53 拓冰建站 浏览量
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两座桥用户态的全部入口就是两个对称的 ioctlioctl方向内核入口作用DRM_IOCTL_PRIME_HANDLE_TO_FD导出drm_prime_handle_to_fd_ioctl→drm_gem_prime_handle_to_fd本进程 handle → 可传递的 dma-buf fdDRM_IOCTL_PRIME_FD_TO_HANDLE导入drm_prime_fd_to_handle_ioctl→drm_gem_prime_fd_to_handle别处传来的 fd → 本进程 handle两个 ioctl 都留了「驱动可覆盖」的回调若dev-driver-prime_handle_to_fd/prime_fd_to_handle非空则用驱动自己的否则用 DRM 核心的默认实现。绝大多数驱动直接用默认的。进程 B / GPU-1进程 A / GPU-0HANDLE_TO_FDSCM_RIGHTS 传递 fdFD_TO_HANDLEGEM handledma-buf fddma-buf fdGEM handle同一个 struct dma_buf同一块后端存储2. 导出桥handle → fddrm_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;intfdget_unused_fd_flags(flags);/* 1. 预留一个空 fd */...dmabufdrm_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_fdfd;return0;}真正有讲究的是第 2 步drm_gem_prime_handle_to_dmabuf()它按三种情况决定 dma_buf 从哪来/* 情况一这个 GEM 对象本身就是从别处导入来的 → 复用原始 dma-buf */if(obj-import_attach){dmabufobj-import_attach-dmabuf;get_dma_buf(dmabuf);gotoout_have_obj;}/* 情况二之前已经导出过 → 复用缓存的 dma_buf不重复创建 */if(obj-dma_buf){get_dma_buf(obj-dma_buf);dmabufobj-dma_buf;gotoout_have_obj;}/* 情况三第一次导出 → 真正创建并注册 dma_buf */dmabufexport_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_infostructdma_buf*drm_gem_prime_export(structdrm_gem_object*obj,intflags){structdma_buf_export_infoexp_info{.exp_nameKBUILD_MODNAME,.opsdrm_gem_prime_dmabuf_ops,/* GEM 专用的 dma_buf_ops */.sizeobj-size,.flagsflags,.privobj,/* dma_buf-priv 指回 GEM 对象 */.resvobj-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_funcsdma_buf_ops 回调GEM 桥接实现最终委派到attachdrm_gem_map_attachobj-funcs-pinmap_dma_bufdrm_gem_map_dma_bufobj-funcs-get_sg_tableunmap_dma_bufdrm_gem_unmap_dma_buf——releasedrm_gem_dmabuf_releaseobj引用释放mmapdrm_gem_dmabuf_mmapobj-funcs-mmapvmap/vunmapdrm_gem_dmabuf_vmapobj-funcs-vmapdma-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 变成本进程的 handleintdrm_gem_prime_fd_to_handle(structdrm_device*dev,structdrm_file*file_priv,intprime_fd,uint32_t*handle){structdma_buf*dma_bufdma_buf_get(prime_fd);/* fd → struct dma_buf */.../* 1. 查本进程的导入缓存这个 dma_buf 之前导入过吗 */retdrm_prime_lookup_buf_handle(file_priv-prime,dma_buf,handle);if(ret0)gotoout_put;/* 命中 → 复用旧 handle *//* 2. 没见过 → 调驱动的 import 回调造一个 GEM 对象 */if(dev-driver-gem_prime_import)objdev-driver-gem_prime_import(dev,dma_buf);elseobjdrm_gem_prime_import(dev,dma_buf);.../* 3. 建立 GEM 对象 ↔ dma_buf 的双向引用 */if(!obj-dma_buf){obj-dma_bufdma_buf;get_dma_buf(dma_buf);}/* 4. 为这个 GEM 对象分配本进程 handle并写入缓存 */retdrm_gem_handle_create_tail(file_priv,obj,handle);retdrm_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)){objdma_buf-priv;drm_gem_object_get(obj);returnobj;}if(!dev-driver-gem_prime_import_sg_table)returnERR_PTR(-EINVAL);/* 正常路径attach → map 拿 sg_table → 让驱动据此造 GEM 对象 */attachdma_buf_attach(dma_buf,attach_dev);get_dma_buf(dma_buf);sgtdma_buf_map_attachment_unlocked(attach,DMA_BIDIRECTIONAL);objdev-driver-gem_prime_import_sg_table(dev,attach,sgt);/* 驱动回调 */obj-import_attachattach;/* 记住「我是导入来的」及其 attachment */obj-resvdma_buf-resv;/* 共用源 dma-buf 的 resv → 同步能跨设备生效 */returnobj;}三个要点串起了前几节自导入优化drm_gem_is_prime_exported_dma_buf把自己导出的 dma-buf 再导入回来只增加 GEM 对象引用、不增加 dma-buf 的f_count——避免自己 attach 自己的病态循环。这与第 2 节导出侧「情况一」互为镜像。attach map 复用 5.2.4 的机制导入桥并不自己发明轮子而是老老实实走dma_buf_attach → dma_buf_map_attachment拿到sg_table再交给驱动的gem_prime_import_sg_table据此构造一个「影子 GEM 对象」。import_attach 共用resvobj-import_attach标记这是导入对象第 2 节导出侧据此复用原始 dma-bufobj-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_exportimport: gem_prime_import_sg_tablehandle ↔ dma_buf 缓存file_priv-primefd ↔ handledrm_gem_objectobj-dma_buf缓存obj-import_attach若导入obj-resvstruct dma_bufdma_buf-priv objdma_buf-ops drm_gem_prime_dmabuf_opsdma_buf-resv obj-resv进程内 handle跨进程 fdpriv/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. 小结与延伸PRIME 是 GEM 与 dma-buf 之间的双向桥HANDLE_TO_FD导出、FD_TO_HANDLE导入把「进程私有 handle」与「可传递 fd」互相翻译。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 那条回调链的中枢。唯一性与引用计数是灵魂obj-dma_buf缓存、import_attach复用、file_priv-prime双向缓存、自导入优化、共用resv五条机制共同保证「一块 buffer 在一个进程里只有一个 handle、一个 GEM 对象只对应一个 dma_buf」。到这里PRIME 的核心桥接已经清晰但还有一个实际问题gem_prime_import_sg_table、get_sg_table、pin/unpin这些回调普通驱动要不要每个都自己写一遍对大量基于系统内存的简单驱动DRM 提供了开箱即用的GEM shmem helper把这套共享路径全部实现好了。这正是[5.3.2 GEM shmem helper 的共享路径]的主题。相关阅读dma-buf 的 attach/map 机制见 5.2.4get_sg_table/pin/unpin等 GEM 回调的语义见 gem_object_funcs.md共用resv带来的隐式同步见 6.3 dma_resv。