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_ioctl→drm_gem_prime_handle_to_fd | 本进程 handle → 可传递的 dma-buf fd |
DRM_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 核心的默认实现。绝大多数驱动直接用默认的。
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 桥接实现 | 最终委派到 |
|---|---|---|
attach | drm_gem_map_attach | obj->funcs->pin |
map_dma_buf | drm_gem_map_dma_buf | obj->funcs->get_sg_table |
unmap_dma_buf | drm_gem_unmap_dma_buf | —— |
release | drm_gem_dmabuf_release | obj引用释放 |
mmap | drm_gem_dmabuf_mmap | obj->funcs->mmap |
vmap/vunmap | drm_gem_dmabuf_vmap | obj->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;}三个要点串起了前几节:
- 自导入优化(
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+ 共用resv:obj->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」的一一对应与引用计数:
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. 小结与延伸
- 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_funcs,map_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.4;
get_sg_table/pin/unpin等 GEM 回调的语义见 gem_object_funcs.md;共用resv带来的隐式同步见 6.3 dma_resv。