第三章:GEM分析:drm_gem_object_funcs:GEM 对象回调表的设计初衷与调用时机
drm_gem_object回答"对象是什么",而drm_gem_object_funcs(对象里的funcs指针)回答"对这个对象做各种操作时,具体该怎么做"。本文分析这张回调表为什么这样设计,以及每个回调由谁、在什么时机、持什么锁调用。本节算是对gem_object动态视角的一个从回调函数角度的补充。
1. 设计初衷:为什么是一张函数指针表
GEM 的核心哲学是"只提供驱动间共享的公共代码,不试图解决所有问题"。这条哲学落到代码上,就必须解决一个矛盾:
DRM core 想用统一的流程处理所有 GEM 对象(handle 创建、mmap、dma-buf 导出、销毁……),但每个驱动的后端存储千差万别(shmem 页、VRAM、TTM 管理的 BO、CMA 连续内存……),具体每一步该怎么做只有驱动自己知道。
drm_gem_object_funcs就是这个矛盾的解法——把"流程骨架"留在 core,把"每一步的具体实现"通过函数指针下放给驱动。它体现了几个明确的设计意图:
| 设计意图 | 含义 |
|---|---|
| 机制与策略分离 | core 定义"何时该做某件事"(机制),驱动通过回调决定"具体怎么做"(策略) |
| 面向对象的多态 | 每个对象携带自己的funcs,同一段 core 代码对不同驱动的对象表现出不同行为——等价于 C++ 的虚函数表(vtable) |
| 可选即降级 | 绝大多数回调是 optional;不实现某回调,就等于声明"本对象不支持该能力",core 会走默认路径或返回不支持 |
| 每对象粒度 | funcs挂在对象而非驱动上,因此同一驱动可为不同类型的对象(如 shmem BO vs 私有 BO)挂不同的回调表 |
| 稳定的 ABI 边界 | core 与驱动只通过这张表交互,新增回调不破坏既有驱动(老驱动该字段为 NULL 即可) |
历史注记:早期这些操作分散在
drm_driver的全局操作集里(如gem_free_object等),是"每驱动一份"。后来收敛为每对象的drm_gem_object_funcs,粒度更细、也更符合"对象自带行为"的直觉。
2. 回调全景
下表汇总"谁调用、何时调用、锁上下文"(obj->resv指 GEM 的 dma_resv 预留锁):
| 回调 | 必需 | 主要调用者(core 函数) | 触发时机 | 锁上下文 |
|---|---|---|---|---|
free | ✅ | drm_gem_object_free() | refcount归零、对象最终销毁 | 无(已无人引用) |
open | ○ | drm_gem_handle_create_tail() | 每次为对象新建一个 handle(含 dma-buf 导入生成 handle) | 无特殊要求 |
close | ○ | drm_gem_object_release_handle() | 每次释放一个 handle(GEM_CLOSE、进程退出、handle_delete) | 无特殊要求 |
export | ○ | drm_gem_prime_export() | PRIME_HANDLE_TO_FD,把对象导出为 dma-buf | 无(默认实现已足够,少有覆盖) |
pin | ○ | drm_gem_map_attach() | dma-buf被导入方 attach时,钉住后端存储 | 内部持obj->resv |
unpin | ○ | drm_gem_map_detach() | dma-buf detach 时,解钉 | 内部持obj->resv |
get_sg_table | ○† | drm_gem_map_dma_buf() | 导入方dma_buf_map_attachment()需要 sg 表时 | 由 dma-buf 框架保证 |
vmap | ○ | drm_gem_dmabuf_vmap()/drm_gem_vmap() | 需要把 BO 映射到内核虚拟地址(如导入方 vmap) | 调用时已持obj->resv |
vunmap | ○ | drm_gem_dmabuf_vunmap() | 释放上面的内核映射 | 调用时已持obj->resv |
mmap | ○ | drm_gem_mmap_obj() | 用户态 mmap 该对象(含 PRIME mmap) | 无特殊要求 |
evict | ○ | drm_gem_object_evict() | 内存紧张、需把对象后端换出 | 调用时已持obj->resv |
print_info | ○ | drm_gem_print_info() | debugfs 打印对象信息 | 无 |
status | ○ | drm_show_memory_stats() | fdinfo 统计内存(resident/purgeable 等) | table_lock |
rss | ○ | drm_show_memory_stats() | fdinfo 统计物理常驻大小 | table_lock |
vm_ops | ○‡ | 缺页时由 mm 子系统回调 | 用户态访问 mmap 区域触发缺页 | mm/vma 锁 |
†
get_sg_table在"用 core 默认的drm_gem_map_dma_buf作为 dma-buf 导出实现"时是必需的;驱动若自己实现map_dma_buf则不需要。
‡vm_ops与mmap二选一:若实现了mmap回调,则由它自行设置vma->vm_ops,此时表中的vm_ops字段不被使用。
3. 分组详解
3.1 生命周期:free / open / close
这三者对应对象"从被引用到被释放"的关键节点。理解它们必须先分清 [gem设计目标]讲的双计数:refcount(内核总引用)与handle_count(用户态 handle 数)。
free(唯一必需回调):refcount归零时由drm_gem_object_free()调用,是对象的析构器。驱动在这里释放后端存储(页/VRAM/TTM BO)、调用drm_gem_object_release()清理 core 部分,然后kfree自己的对象。因为此刻已无人引用,无需加锁。open:每新建一个 handle就调一次(drm_gem_handle_create_tail)。注意它绑定的是handle,不是对象——同一对象被同一/不同进程多次 open,会多次触发。典型用途:amdgpu 在此为"进程(drm_file)× BO"建立 per-VM 的记账(如把 BO 关联到该文件的 VM)。close:每释放一个 handle调一次(GEM_CLOSE、drm_release进程退出遍历、drm_gem_handle_delete)。与open对称,用于拆掉 open 时建立的 per-file 关系。
常见误区:把
free当成"handle 关闭就触发"。实际上只要对象还被 mmap、dma-buf 导出或驱动内部引用,close之后对象不会free——真正触发free的是refcount归零。
3.2 dma-buf 共享:export / pin / unpin / get_sg_table / vmap / vunmap
这一组服务于PRIME/dma-buf 跨进程、跨设备共享它们的调用方往往是导入端或 dma-buf 框架,而非本驱动自己。
export:把对象包成dma_buf。多数驱动不覆盖它,直接用 core 的drm_gem_prime_export()(内部会装配一套标准dma_buf_ops,其回调再转回下面这些函数)。仅当驱动需要自定义 dma-buf 行为时才实现。pin/unpin:当导入方 attach到这个 dma-buf 时(drm_gem_map_attach),core 会先dma_resv_lock再调pin,把后端存储钉在一个可被外部 DMA 访问的位置(例如从可迁移的 VRAM 钉到稳定处),防止共享期间被迁移/换出。detach 时对称调unpin。这正是"共享会抑制迁移"的根源。get_sg_table:导入方真正映射时(drm_gem_map_dma_buf),core 调它拿到描述后端物理页的 scatter-gather 表,再dma_map_sgtable给导入设备。注意:core 的释放路径用dma_unmap_sg + sg_free_table,因此该回调不能用于指向驱动私有内存区间的 sg 表。vmap/vunmap:把 BO 映射到内核地址空间(返回iosys_map)。用于导入方或内核内部需要 CPU 直接读写 BO 的场景。关键约束:core 调用时已持obj->resv锁,回调内不得再去拿这把锁。
3.3 CPU 映射:mmap / vm_ops
mmap:用户态对该对象mmap()时,drm_gem_mmap_obj()会调它,由驱动完成 VMA 的建立(页缓存属性、vma->vm_ops等)。若实现了它,就由它负责设置vm_ops。vm_ops:不实现mmap回调时的默认路径所用的 VMA 操作集,其中的fault处理缺页——用户首次访问某页时按需把后端页填入。二者互斥:有mmap就不用表里的vm_ops。
这组回调让"给对象分配 mmap 伪偏移"与"真正建立 CPU 映射"衔接起来。
3.4 内存管理:evict
evict:内存紧张时,drm_gem_object_evict()调它把对象后端换出(如从 VRAM 移到系统内存或释放可重建的页)。调用时已持obj->resv。它体现了一个边界:GEM core不自己做迁移策略,只在合适时机"通知驱动去 evict",具体搬移仍由驱动(通常借 TTM)完成——与第 6 节"迁移属 TTM"一致。
与 gpuva 的联动:一个对象被 evict 后,指向它的所有 GPU VA 映射都要标记失效。驱动通常在 evict 路径里遍历
gpuva.list逐条drm_gpuva_invalidate()。
3.5 观测与调试:print_info / status / rss
这三者不影响功能,只服务于可观测性:
print_info:debugfs 打印对象的驱动私有信息,用drm_printf_indent()输出。status:返回对象状态(如 resident/purgeable),决定它被计入 fdinfo 的哪类统计;在table_lock下调用,允许与状态变更竞争(“无害”)。rss:返回对象在物理内存中的常驻大小(Resident Set Size)。
status与rss都由drm_show_memory_stats()调用,支撑/proc/<pid>/fdinfo的 GPU 内存统计(gputop 等工具依赖它)。
4. 锁上下文速记(最易出错处)
实现回调时最常见的 bug 是锁误用。归纳如下:
| 回调 | 进入时是否已持obj->resv | 实现注意 |
|---|---|---|
vmap/vunmap | 是 | 内部不要再dma_resv_lock |
evict | 是 | 同上;且此路径可能在内存回收上下文,避免大块分配 |
pin/unpin | 是(由drm_gem_map_*加持) | — |
free | 否(已无引用) | 可自由清理 |
open/close/mmap/print_info | 否 | 按需自行加锁 |
5. 与 amdgpu 的对照
amdgpu 通过amdgpu_bo(内嵌ttm_buffer_object→ 内嵌drm_gem_object)实现这张表,把 GEM 的对象语义与 TTM 的迁移能力缝合:
free→ 释放amdgpu_bo、退还 TTM 资源;open/close→ 维护"drm_file × BO"在该进程 VM 中的关系;pin/unpin/get_sg_table→ 桥接到 TTM 的 pin 与 sg 表;evict→ 交给 TTM 的迁移逻辑;mmap/vm_ops→ TTM 的缺页与页属性处理。
可见这张表正是 core 与"驱动 + TTM"之间的契约边界:core 决定时机,驱动(借 TTM)落实动作。
6. 小结
drm_gem_object_funcs是 GEM"机制与策略分离"哲学的直接产物——一张每对象的虚函数表,让统一的 core 流程对接千差万别的驱动后端。- 唯一必需的是
free;其余皆 optional,不实现即代表不支持该能力。 - 记住三条主线:生命周期(free/open/close,注意双计数)、dma-buf 共享(export/pin/unpin/get_sg_table/vmap/vunmap,注意 pin 抑制迁移、vmap 已持 resv)、CPU 映射(mmap/vm_ops 互斥)。
- 锁是最大的坑:
vmap/vunmap/evict/pin/unpin进入时 core已持obj->resv,回调内不得重复加锁。 - 这张表定义了 core 与驱动(及 TTM)之间清晰的契约:core 管"何时",驱动管"如何"。
