第九章:GPUVM:GPUVM的并发访问---从内部 list 的并发访问需求看 drm_gpuvm_flags

前言

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) 延迟销毁的 zombie drm_gpuvm_bo fence signalling 路径 vs. cleanup 路径
gpuva.list 每个 struct drm_gem_object.gpuva 该 GEM 被映射进各 VM 的 drm_gpuva 反向索引 drm_gpuva_link/unlink vs. 遍历

其中:

  • 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.cDOC: Locking。所以 drm_gpuvm_flags 管不到它

  • 真正被 flag 影响的是后面几条:extobj/evict/bo_defer(VM 级列表)gpuva.list(GEM 级列表)

这两组列表面临两个不同 的并发难题,于是催生了两个不同的 flag。


2. 难题一:extobj / evict 列表------"框架自己加锁" vs. "驱动已持大锁"

2.1 并发情形

extobj.listevict.list 的典型访问模式是一边遍历、一边被并发增删

  • 命令提交路径要遍历 extobj 列表把所有外部 BO 锁进 drm_execdrm_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_resvr_obj->resv)以及相关 BO 的 resv 一次性锁住。对这类驱动来说:

既然 VM 的 dma_resv 大锁已经保证了这些列表的互斥,框架内部那两把 spinlock 就是纯粹多余的开销(额外的原子操作 + cache line 争用)。

于是框架给出优化开关------DRM_GPUVM_RESV_PROTECTED

2.3 DRM_GPUVM_RESV_PROTECTED = BIT(0)

语义(drm_gpuvm.cDOC: 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 disabled and the corresponding lockdep checks are enabled.

它的实现方式非常"外科手术":所有对 extobj/evict 的操作都走一个条件加锁辅助函数,锁不锁取决于这个 flag:

c 复制代码
// drivers/gpu/drm/drm_gpuvm.c
static void
cond_spin_lock(spinlock_t *lock, bool cond)
{
	if (cond)
		spin_lock(lock);
}

drm_gpuvm.ccond_spin_lock()。而 cond 恰恰由 flag 反推出来,例如析构路径:

c 复制代码
// drm_gpuvm_bo_destroy()
bool lock = !drm_gpuvm_resv_protected(gpuvm);

if (!lock)
	drm_gpuvm_resv_assert_held(gpuvm);   // 没内部锁?那就断言你持有 resv

drm_gpuvm_bo_list_del(vm_bo, extobj, lock);
drm_gpuvm_bo_list_del(vm_bo, evict, lock);

drm_gpuvm_bo_destroy()

两条路径的对照也体现在入口分发上:

c 复制代码
int drm_gpuvm_prepare_objects(...) {
	if (drm_gpuvm_resv_protected(gpuvm))
		return drm_gpuvm_prepare_objects_locked(gpuvm, ...); // 依赖 resv
	else
		return __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.1 gpuva.list列表的特殊性

gpuva.list 不在 drm_gpuvm 里,而是挂在每个 drm_gem_object 上,用来反向索引"这个 BO 被映射到了哪些 VA"。drm_gpuva_link() / drm_gpuva_unlink() 会增删它:

c 复制代码
void drm_gpuva_link(struct drm_gpuva *va, struct drm_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.3 DRM_GPUVM_IMMEDIATE_MODE = BIT(1)

为此,drm_gem_object 里额外准备了一把 mutex gpuva.lock,专门在这种模式下保护 gpuva.list

c 复制代码
// include/drm/drm_gem.h
struct {
	struct list_head list;   // gpuva.list
	/*
	 * @gpuva.lock: 仅在 DRM_GPUVM_IMMEDIATE_MODE 下使用。
	 * 它必须能在 fence signalling 路径里安全获取,
	 * 所以持有它时不得分配内存。否则应使用 dma_resv。
	 */
	struct mutex lock;
} gpuva;

drm_gem.hdrm_gem_objectgpuva 字段注释。语义切换点很明确:

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 带来一整套配套约束,都是被"不能睡眠/不能分配"倒逼出来的:

  1. 不能在持锁时分配内存 → 因此 immediate 模式驱动不能 用会分配的 drm_gpuvm_bo_obtain_locked()(它内部 drm_WARN_ON(immediate_mode)),必须改用预分配 版本 drm_gpuvm_bo_obtain_prealloc()(先在外面分配好,持 gpuva.lock 时只做链表插入)。

  2. 析构不能同步做 → 因为 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 模式服务。

  3. 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.lock mutex(承诺不睡眠/不分配),从而允许在 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_PROTECTEDDRM_GPUVM_IMMEDIATE_MODE 保护的是不同的列表 ,因此在概念上是正交的两个维度:

GEM gpuva.list extobj/evict 锁
维度归属 IMMEDIATE_MODE RESV_PROTECTED
默认(都不设) GEM dma_resv 框架内部 spinlock
只设 RESV_PROTECTED GEM dma_resv VM dma_resv(关内部锁)
只设 IMMEDIATE_MODE GEM gpuva.lock mutex 框架内部 spinlock

选择逻辑可以这样理解:
#mermaid-svg-qhlkEMXq1hj7zdt9{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qhlkEMXq1hj7zdt9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qhlkEMXq1hj7zdt9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qhlkEMXq1hj7zdt9 .error-icon{fill:#552222;}#mermaid-svg-qhlkEMXq1hj7zdt9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qhlkEMXq1hj7zdt9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qhlkEMXq1hj7zdt9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qhlkEMXq1hj7zdt9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qhlkEMXq1hj7zdt9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qhlkEMXq1hj7zdt9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qhlkEMXq1hj7zdt9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qhlkEMXq1hj7zdt9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qhlkEMXq1hj7zdt9 .marker.cross{stroke:#333333;}#mermaid-svg-qhlkEMXq1hj7zdt9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qhlkEMXq1hj7zdt9 p{margin:0;}#mermaid-svg-qhlkEMXq1hj7zdt9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qhlkEMXq1hj7zdt9 .cluster-label text{fill:#333;}#mermaid-svg-qhlkEMXq1hj7zdt9 .cluster-label span{color:#333;}#mermaid-svg-qhlkEMXq1hj7zdt9 .cluster-label span p{background-color:transparent;}#mermaid-svg-qhlkEMXq1hj7zdt9 .label text,#mermaid-svg-qhlkEMXq1hj7zdt9 span{fill:#333;color:#333;}#mermaid-svg-qhlkEMXq1hj7zdt9 .node rect,#mermaid-svg-qhlkEMXq1hj7zdt9 .node circle,#mermaid-svg-qhlkEMXq1hj7zdt9 .node ellipse,#mermaid-svg-qhlkEMXq1hj7zdt9 .node polygon,#mermaid-svg-qhlkEMXq1hj7zdt9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qhlkEMXq1hj7zdt9 .rough-node .label text,#mermaid-svg-qhlkEMXq1hj7zdt9 .node .label text,#mermaid-svg-qhlkEMXq1hj7zdt9 .image-shape .label,#mermaid-svg-qhlkEMXq1hj7zdt9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-qhlkEMXq1hj7zdt9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qhlkEMXq1hj7zdt9 .rough-node .label,#mermaid-svg-qhlkEMXq1hj7zdt9 .node .label,#mermaid-svg-qhlkEMXq1hj7zdt9 .image-shape .label,#mermaid-svg-qhlkEMXq1hj7zdt9 .icon-shape .label{text-align:center;}#mermaid-svg-qhlkEMXq1hj7zdt9 .node.clickable{cursor:pointer;}#mermaid-svg-qhlkEMXq1hj7zdt9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qhlkEMXq1hj7zdt9 .arrowheadPath{fill:#333333;}#mermaid-svg-qhlkEMXq1hj7zdt9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qhlkEMXq1hj7zdt9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qhlkEMXq1hj7zdt9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qhlkEMXq1hj7zdt9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qhlkEMXq1hj7zdt9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qhlkEMXq1hj7zdt9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qhlkEMXq1hj7zdt9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qhlkEMXq1hj7zdt9 .cluster text{fill:#333;}#mermaid-svg-qhlkEMXq1hj7zdt9 .cluster span{color:#333;}#mermaid-svg-qhlkEMXq1hj7zdt9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qhlkEMXq1hj7zdt9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qhlkEMXq1hj7zdt9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-qhlkEMXq1hj7zdt9 .icon-shape,#mermaid-svg-qhlkEMXq1hj7zdt9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qhlkEMXq1hj7zdt9 .icon-shape p,#mermaid-svg-qhlkEMXq1hj7zdt9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qhlkEMXq1hj7zdt9 .icon-shape .label rect,#mermaid-svg-qhlkEMXq1hj7zdt9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qhlkEMXq1hj7zdt9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qhlkEMXq1hj7zdt9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qhlkEMXq1hj7zdt9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

是, 想省锁
否/图省事
需要在 fence signalling 路径

修改 GPUVM 映射?
设 DRM_GPUVM_IMMEDIATE_MODE

gpuva.list 改用不睡眠的 gpuva.lock

  • 预分配 + 延迟销毁
    gpuva.list 用 GEM dma_resv 即可
    提交时是否已用 drm_exec

持有 VM 的 dma_resv 大锁?
设 DRM_GPUVM_RESV_PROTECTED

关闭 extobj/evict 内部 spinlock

由 resv 统一保护 + lockdep 断言
不设, 框架内部 spinlock 自保护


6. 结论

drm_gpuvm_flags 放回"内部 list 并发"的语境里,它其实回答了容器对象最核心的两个同步问题:

  1. VM 级列表(extobj/evict)谁来保护? ------ DRM_GPUVM_RESV_PROTECTED 在"框架内部 spinlock 自保护"与"复用驱动已持的 VM dma_resv"之间做性能取舍
  2. GEM 级列表(gpuva.list)用什么锁? ------ DRM_GPUVM_IMMEDIATE_MODE 在"可睡眠的 dma_resv"与"不可睡眠的 gpuva.lock mutex"之间做执行上下文适配,从而支持 fence signalling 路径改图。
  3. DRM_GPUVM_USERBITS 则为驱动私有并发状态位划定安全区,避免与框架 flag 冲突。

它们的共同点是:都不改变 GPUVM 的功能语义,只切换其并发同步策略。这正是把"锁策略"做成初始化期静态 flag 的意义------同一套 GPUVM 代码,既能服务传统进程上下文提交模型,又能服务现代 fence-signalling / VM_BIND 的异步模型,而不必为每种模型各写一份容器实现。