尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

第九章:GPUVM:GPUVM的并发访问---从内部 list 的并发访问需求看 drm_gpuvm_flags 前言GPU 虚拟地址空间管理框架概述 一文中我们已经把GPUVM涉及的对象和关系梳理了一下从本节开始我们来分析具体的实现。并发访问的同步永远是内核对象绕不开的一项“基础业务”。像 GPUVM 这种容器型对象——它内部挂着好几条链表既被进程上下文的 ioctl 路径访问又被 fence signalling 的原子/回调路径访问——同步问题会被放大。enum drm_gpuvm_flags表面上只是三个 bit本质上却是 GPUVM 框架对“这些内部 list 由谁来保护、用什么锁保护”这一问题给出的静态策略开关。本文分析 GPUVM 到底有哪些需要同步的内部 list、各自的并发需求是什么再回过头解释这三个 flag 。1. GPUVM 内部同步的 liststruct drm_gpuvm是一个典型的容器对象它内部维护了多条链表。这些链表的并发需求并不相同这正是理解 flag 的前提。内部 list位置存什么谁在并发访问rb.tree/rb.liststruct drm_gpuvm该 VM 的全部drm_gpuva区间树 顺序链表建图/改图路径map/unmap/remapextobj.liststruct drm_gpuvm.extobj外部对象resv 与 VM 不同的 BO对应的drm_gpuvm_bo提交路径drm_gpuvm_prepare_objects()vs. 映射建立/销毁evict.liststruct drm_gpuvm.evict被换出、需要 revalidate 的drm_gpuvm_bo提交路径drm_gpuvm_validate()vs. eviction 回调bo_deferstruct drm_gpuvm.bo_defer(llist)延迟销毁的 zombiedrm_gpuvm_bofence signalling 路径 vs. cleanup 路径gpuva.list每个struct drm_gem_object.gpuva该 GEM 被映射进各 VM 的drm_gpuva反向索引drm_gpuva_link/unlinkvs. 遍历其中rb.tree/rb.listGPUVM 明确声明不负责它的加锁完全交给驱动。文档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_deferVM 级列表和gpuva.listGEM 级列表。这两组列表面临两个不同的并发难题于是催生了两个不同的 flag。2. 难题一extobj / evict 列表——“框架自己加锁” vs. “驱动已持大锁”2.1 并发情形extobj.list和evict.list的典型访问模式是一边遍历、一边被并发增删命令提交路径要遍历 extobj 列表把所有外部 BO 锁进drm_execdrm_gpuvm_prepare_objects()遍历 evict 列表做 revalidatedrm_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.3DRM_GPUVM_RESV_PROTECTED BIT(0)语义drm_gpuvm.c的DOC: LockingAlternatively, 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 自保护驱动省心代价是多两把锁。设置框架关闭内部锁改由驱动持有的 VMdma_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. 第三个 bitDRM_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_PROTECTEDGEM dma_resvVM dma_resv关内部锁只设 IMMEDIATE_MODEGEMgpuva.lockmutex框架内部 spinlock选择逻辑可以这样理解是否是, 想省锁否/图省事需要在 fence signalling 路径修改 GPUVM 映射设 DRM_GPUVM_IMMEDIATE_MODEgpuva.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 并发”的语境里它其实回答了容器对象最核心的两个同步问题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 的异步模型而不必为每种模型各写一份容器实现。
返回列表