前言
GPU 虚拟地址空间管理框架概述 一文中,我们已经把GPUVM涉及的对象和关系梳理了一下,从本节开始,我们来分析具体的实现。
并发访问的同步,永远是内核对象绕不开的一项“基础业务”。像 GPUVM 这种容器型对象——它内部挂着好几条链表,既被进程上下文的 ioctl 路径访问,又被 fence signalling 的原子/回调路径访问——同步问题会被放大。enum drm_gpuvm_flags表面上只是三个 bit,本质上却是 GPUVM 框架对“这些内部 list 由谁来保护、用什么锁保护”这一问题给出的静态策略开关。
本文分析 GPUVM 到底有哪些需要同步的内部 list、各自的并发需求是什么,再回过头解释这三个 flag 。
1. GPUVM 内部同步的 list
struct drm_gpuvm是一个典型的容器对象,它内部维护了多条链表。这些链表的并发需求并不相同,这正是理解 flag 的前提。
| 内部 list | 位置 | 存什么 | 谁在并发访问 |
|---|---|---|---|
rb.tree/rb.list | struct drm_gpuvm | 该 VM 的全部drm_gpuva(区间树 + 顺序链表) | 建图/改图路径(map/unmap/remap) |
extobj.list | struct drm_gpuvm.extobj | 外部对象(resv 与 VM 不同的 BO)对应的drm_gpuvm_bo | 提交路径drm_gpuvm_prepare_objects()vs. 映射建立/销毁 |
evict.list | struct drm_gpuvm.evict | 被换出、需要 revalidate 的drm_gpuvm_bo | 提交路径drm_gpuvm_validate()vs. eviction 回调 |
bo_defer | struct drm_gpuvm.bo_defer(llist) | 延迟销毁的 zombiedrm_gpuvm_bo | fence signalling 路径 vs. cleanup 路径 |
gpuva.list | 每个struct drm_gem_object.gpuva | 该 GEM 被映射进各 VM 的drm_gpuva反向索引 | drm_gpuva_link/unlinkvs. 遍历 |
其中:
rb.tree/rb.list:GPUVM 明确声明不负责它的加锁,完全交给驱动。文档DOC: Locking里写得很清楚:In terms of managing &drm_gpuva entries DRM GPUVM does not take care of locking itself, it is the drivers responsibility.
见
drm_gpuvm.c的DOC: Locking。所以drm_gpuvm_flags管不到它。真正被 flag 影响的是后面几条:
extobj/evict/bo_defer(VM 级列表)和gpuva.list(GEM 级列表)。
这两组列表面临两个不同的并发难题,于是催生了两个不同的 flag。
2. 难题一:extobj / evict 列表——“框架自己加锁” vs. “驱动已持大锁”
2.1 并发情形
extobj.list和evict.list的典型访问模式是一边遍历、一边被并发增删:
- 命令提交路径要遍历 extobj 列表把所有外部 BO 锁进
drm_exec(drm_gpuvm_prepare_objects()),遍历 evict 列表做 revalidate(drm_gpuvm_validate())。 - 与此同时,映射的建立/销毁(
drm_gpuvm_bo的创建/析构)、eviction 通知,会往这两条列表里插入或删除元素。
框架为此实现了一套无锁遍历 + 内部 spinlock的机制:遍历时用get_next_vm_bo_from_list()取一个元素就立刻释放 spinlock,把元素搬到 local list,从而允许遍历期间并发增删。插入/删除则用extobj.lock/evict.lock两把 spinlock 保护(在drm_gpuvm_init()中初始化)。
2.2 但很多驱动其实“已经”持有了 VM 的 dma_resv
现代提交路径普遍用drm_exec把 VM 的公共dma_resv(r_obj->resv)以及相关 BO 的 resv 一次性锁住。对这类驱动来说:
既然 VM 的 dma_resv 大锁已经保证了这些列表的互斥,框架内部那两把 spinlock 就是纯粹多余的开销(额外的原子操作 + cache line 争用)。
于是框架给出优化开关——DRM_GPUVM_RESV_PROTECTED。
2.3DRM_GPUVM_RESV_PROTECTED = BIT(0)
语义(drm_gpuvm.c的DOC: Locking):
Alternatively, drivers can set the &DRM_GPUVM_RESV_PROTECTED flag to indicate that the corresponding &dma_resv locks are held in order to protect the lists. If set,internal locking is disabledand the corresponding lockdep checks are enabled.
它的实现方式非常“外科手术”:所有对 extobj/evict 的操作都走一个条件加锁辅助函数,锁不锁取决于这个 flag:
// drivers/gpu/drm/drm_gpuvm.cstaticvoidcond_spin_lock(spinlock_t*lock,bool cond){if(cond)spin_lock(lock);}见drm_gpuvm.c的cond_spin_lock()。而cond恰恰由 flag 反推出来,例如析构路径:
// drm_gpuvm_bo_destroy()bool lock=!drm_gpuvm_resv_protected(gpuvm);if(!lock)drm_gpuvm_resv_assert_held(gpuvm);// 没内部锁?那就断言你持有 resvdrm_gpuvm_bo_list_del(vm_bo,extobj,lock);drm_gpuvm_bo_list_del(vm_bo,evict,lock);见drm_gpuvm_bo_destroy()。
两条路径的对照也体现在入口分发上:
intdrm_gpuvm_prepare_objects(...){if(drm_gpuvm_resv_protected(gpuvm))returndrm_gpuvm_prepare_objects_locked(gpuvm,...);// 依赖 resvelsereturn__drm_gpuvm_prepare_objects(gpuvm,...);// 用内部 spinlock 无锁遍历}见drm_gpuvm_prepare_objects()、drm_gpuvm_validate()。
一句话总结 BIT(0):它回答的是“extobj/evict 这两条 VM 级列表由谁保护”——
- 不设:框架用内部 spinlock 自保护(驱动省心,代价是多两把锁)。
- 设置:框架关闭内部锁,改由驱动持有的 VM
dma_resv统一保护(驱动担责,换来零额外锁开销)。lockdep 会用drm_gpuvm_resv_assert_held()帮你兜底检查。
副作用细节:在 RESV_PROTECTED 模式下,外部对象不能直接进 evict 列表(因为此时 evict 列表靠 VM 公共 resv 保护,而 extobj 的 resv 与 VM 不同),见
drm_gpuvm_bo_evict()里的if (drm_gpuvm_is_extobj(gpuvm, obj) && !lock) return;。
3. 难题二:GEM 的gpuva.list——fence signalling 路径“不能睡觉”
3.1gpuva.list列表的特殊性
gpuva.list不在drm_gpuvm里,而是挂在每个drm_gem_object上,用来反向索引“这个 BO 被映射到了哪些 VA”。drm_gpuva_link()/drm_gpuva_unlink()会增删它:
voiddrm_gpuva_link(structdrm_gpuva*va,structdrm_gpuvm_bo*vm_bo){...drm_gem_gpuva_assert_lock_held(gpuvm,obj);// 必须持有 GEM 的 gpuva 锁list_add_tail(&va->gem.entry,&vm_bo->list.gpuva);}默认情况下,这条列表由GEM 的dma_resv保护。这在传统的“进程上下文提交”模型里没问题——resv 是个可睡眠的 ww_mutex,随便锁。
3.2 矛盾点:驱动要在 fence signalling 路径改映射
问题出在新的异步/VM_BIND 模型:驱动可能需要在 fence signalling critical path 里修改 GPUVM(例如 job 完成回调里做 unmap 清理)。而 fence signalling 路径有一条铁律:
不允许睡眠、不允许分配内存。
dma_resv是 ww_mutex,会睡眠——在这条路径上根本不能拿。于是这条gpuva.list需要换一把不睡眠的锁。
3.3DRM_GPUVM_IMMEDIATE_MODE = BIT(1)
为此,drm_gem_object里额外准备了一把 mutexgpuva.lock,专门在这种模式下保护gpuva.list:
// include/drm/drm_gem.hstruct{structlist_headlist;// gpuva.list/* * @gpuva.lock: 仅在 DRM_GPUVM_IMMEDIATE_MODE 下使用。 * 它必须能在 fence signalling 路径里安全获取, * 所以持有它时不得分配内存。否则应使用 dma_resv。 */structmutexlock;}gpuva;见drm_gem.h中drm_gem_object的gpuva字段注释。语义切换点很明确:
When DRM_GPUVM_IMMEDIATE_MODE is set, this list is protected by the mutex. Otherwise, the list is protected by the GEMs &dma_resv lock.
这个 flag 带来一整套配套约束,都是被“不能睡眠/不能分配”倒逼出来的:
不能在持锁时分配内存→ 因此 immediate 模式驱动不能用会分配的
drm_gpuvm_bo_obtain_locked()(它内部drm_WARN_ON(immediate_mode)),必须改用预分配版本drm_gpuvm_bo_obtain_prealloc()(先在外面分配好,持gpuva.lock时只做链表插入)。析构不能同步做→ 因为
drm_gpuvm_bo_put()可能把最后一个引用清零并触发销毁,而销毁要拿 GEM 锁、可能 kfree mutex 本身,在 fence 路径不安全。于是引入延迟销毁:drm_gpuvm_bo_put_deferred()把 vm_bo 变成 zombie 挂到bo_defer(一条 lockless llist),之后由drm_gpuvm_bo_deferred_cleanup()在安全上下文批量回收。它同样drm_WARN_ON(!immediate_mode),即这套机制专为 immediate 模式服务。zombie 容忍→ evict/extobj 列表上会短暂出现引用计数为 0 的 zombie 项,遍历者需要用
drm_gpuvm_bo_is_zombie()跳过。原因是:immediate 模式下run_job()里拿不到 resv 锁,所以 zombie 只能滞留到 deferred cleanup。
一句话总结 BIT(1):它回答的是“GEM 的gpuva.list用哪把锁”——
- 不设:用 GEM 的
dma_resv(可睡眠,适合进程上下文提交)。 - 设置:用 GEM 的
gpuva.lockmutex(承诺不睡眠/不分配),从而允许在 fence signalling 路径里改映射,代价是必须配合预分配 + 延迟销毁这一整套约束。
注意注释里的强约束:“all entries in this list must agree on whether DRM_GPUVM_IMMEDIATE_MODE is set”——同一条
gpuva.list不能一半按 resv、一半按 mutex,否则锁模型会撕裂。
4. 第三个 bit:DRM_GPUVM_USERBITS = BIT(2)
前两个 bit 是框架预留的策略位,DRM_GPUVM_USERBITS则是一条“分界线”:从 BIT(2) 开始的更高位,框架保证永不占用,留给驱动自定义。
它的价值在并发语境下同样成立:驱动可以在同一个flags字段里塞自己的 VM 级状态位,而不必担心未来内核版本新增框架 flag 时发生 bit 冲突。这是内核 flag 枚举的惯用手法(对比同文件里drm_gpuva_flags也有对应的DRM_GPUVA_USERBITS)。
5. 组合视角:两个正交维度
DRM_GPUVM_RESV_PROTECTED和DRM_GPUVM_IMMEDIATE_MODE保护的是不同的列表,因此在概念上是正交的两个维度:
GEMgpuva.list锁 | extobj/evict 锁 | |
|---|---|---|
| 维度归属 | IMMEDIATE_MODE管 | RESV_PROTECTED管 |
| 默认(都不设) | GEM dma_resv | 框架内部 spinlock |
| 只设 RESV_PROTECTED | GEM dma_resv | VM dma_resv(关内部锁) |
| 只设 IMMEDIATE_MODE | GEMgpuva.lockmutex | 框架内部 spinlock |
选择逻辑可以这样理解:
6. 结论
把drm_gpuvm_flags放回“内部 list 并发”的语境里,它其实回答了容器对象最核心的两个同步问题:
- VM 级列表(extobj/evict)谁来保护?——
DRM_GPUVM_RESV_PROTECTED在“框架内部 spinlock 自保护”与“复用驱动已持的 VM dma_resv”之间做性能取舍。 - GEM 级列表(gpuva.list)用什么锁?——
DRM_GPUVM_IMMEDIATE_MODE在“可睡眠的 dma_resv”与“不可睡眠的 gpuva.lock mutex”之间做执行上下文适配,从而支持 fence signalling 路径改图。 DRM_GPUVM_USERBITS则为驱动私有并发状态位划定安全区,避免与框架 flag 冲突。
它们的共同点是:都不改变 GPUVM 的功能语义,只切换其并发同步策略。这正是把“锁策略”做成初始化期静态 flag 的意义——同一套 GPUVM 代码,既能服务传统进程上下文提交模型,又能服务现代 fence-signalling / VM_BIND 的异步模型,而不必为每种模型各写一份容器实现。