资讯中心

6.3.2 ww_mutex —— 多锁场景下的死锁避免机制

📅 2026/8/21 7:40:04
6.3.2 ww_mutex —— 多锁场景下的死锁避免机制
dma_resv的核心是一把保护 BO 元数据内存位置、fence 列表的锁。但当一次操作需要同时锁定多个 BO时单纯的互斥锁将无法回避一类根本性的问题——加锁顺序死锁。本节剖析内核为此设计的ww_mutexwait/wound mutex它是dma_resv加锁、乃至上层drm_exec见 6.3.3得以成立的底层基石。1. 问题加锁顺序不受内核控制GPU 的一次提交往往涉及成百上千个 BO这些 BO 可跨上下文、跨进程共享甚至经 PRIME/dma-buf 跨设备共享。内核在执行前需要逐一锁定它们。问题在于BO 在一次 execbuf 中出现的顺序由用户态决定取决于应用的 GL/Vulkan 调用序列内核无法保证不同上下文以相同顺序引用同一组 BO。于是经典的ABBA 死锁随时可能发生等待等待持有持有线程甲持有 BO-A等待 BO-BBO-B线程乙持有 BO-B等待 BO-ABO-A普通mutex对此无能为力它只能保证单把锁的互斥无法感知「一组锁应作为一个整体获取」这一事务语义。2. 思路为每次加锁事务分配一个年龄TTM 子系统最早提出的解法非常简洁为**每一组需要锁定的 BO一次事务**从全局计数器分配一个唯一且递增的reservation ticket预留票据 / stamp。ticket 越小代表事务越老越早开始。当两个事务竞争同一把锁而可能死锁时依据双方 ticket 的老幼来裁决谁退让从而打破环路。这一思想在数据库理论中早有对应衍生出两种对称的算法。下表统一从「抢锁者 T」的视角描述设 T 正试图获取一把已被另一事务 H 持有的锁表格内容即 T及在 Wound-Wait 中受影响的 H此时的动作算法T更老T更年轻Wait-Die等待-死亡T等待H 释放T退避并死亡释放自己已持有的全部锁、返回-EDEADLK后重试Wound-Wait创伤-等待T“创伤” H要求 H 退避H 将放弃锁并重试T 随后取得锁T等待H 释放两种算法的共同规则可归纳为一句话永远让更老的事务占优——老者要么安全地等待、要么迫使年轻者让路只有年轻者会退避。区别仅在于谁来执行退避动作Wait-Die 由年轻的抢锁者自己退避被动等待或主动 dieWound-Wait 由年轻的持锁者被他人创伤而退避抢占式。因此 Wound-Wait 通常回退更少但需要一套可靠机制让被创伤者察觉并让出锁恢复开销更大Wait-Die 实现更简单仅由抢锁者自行判断。具体到实现Wound-Wait 的创伤并非立即打断持锁者而是置位其ww_acquire_ctx.wounded标志由被创伤的事务在下一次加锁或解锁点检查到该标志后自行退避——即前文所述事务因被创伤而死亡实为一种协作式抢占。ww_mutex之名取自 “wait/wound”泛指该框架整体框架同时支持**两种算法由锁类在定义时选定。DRM 的dma_resv采用的reservation_ww_class实际以DEFINE_WD_CLASS定义即 Wait-Die 算法见dma-resv.c。因此本专栏语境下 BO 锁的死锁避免走的是 Wait-Die 路径——较年轻者主动回退。3. 两个核心概念相较普通 mutexww_mutex的接口引入两个额外对象Acquire contextstruct ww_acquire_ctx——事务的化身。它持有本次加锁事务的stamp票据。关键在于一个事务在整个加锁过程中必须始终复用最初分配的这一个 stamp即便中途回退重来也不重新取号——否则每次重试都变年轻将永远打不赢别人而饿死。context 同时记录acquired已持锁计数、wounded是否被创伤等状态。W/W classstruct ww_class——锁类。普通 mutex 的锁类是隐式的而ww_mutex要求显式指定锁类因为初始化 acquire context 时需要它锁类还决定采用 Wait-Die 还是 Wound-WaitDEFINE_WD_CLASSvsDEFINE_WW_CLASS。全局stamp计数器即挂在锁类上。structww_acquire_ctx{structtask_struct*task;unsignedlongstamp;/* 事务票据越小越老 */unsignedintacquired;/* 已成功持有的锁数 */unsignedshortwounded;unsignedshortis_wait_die;/* 从锁类继承的算法选择 */...};4. 典型使用范式取号—加锁—遇冲突回退—重试ww_mutex的标准用法是一个回退重试循环structww_acquire_ctxctx;ww_acquire_init(ctx,ww_class);/* 取号分配本事务的 stamp */retry:retww_mutex_lock(objA-lock,ctx);if(ret-EDEADLK)/* 冲突本事务较年轻需退避 */gotobackoff;retww_mutex_lock(objB-lock,ctx);if(ret-EDEADLK){ww_mutex_unlock(objA-lock);/* 释放已持有的全部锁 */gotoslow;}/* ... 成功持有 A、B执行受保护的操作 ... */ww_mutex_unlock(objB-lock);ww_mutex_unlock(objA-lock);ww_acquire_fini(ctx);return0;slow:/* 用 _slow 变体先锁住冲突对象 */ww_mutex_lock_slow(objB-lock,ctx);gotoretry_with_B_held;要点-EDEADLK不是错误而是请回退的信号。抢锁者收到它后须释放已持有的全部ww_mutex然后从冲突的那把锁重新开始。_slow变体回退后重新抢锁时对上次导致冲突的那把锁应改用ww_mutex_lock_slow。它语义上等价于普通ww_mutex_lock此刻尚未持有其他锁无死锁风险但返回void且在调试模式下会校验确已释放全部锁从而避免在-EDEADLK慢路径上空转。单锁场景若只需锁一把 ww_mutex可传入NULLcontext此时其行为与普通 mutex 完全一致无须取号。ww_acquire_fini事务结束时归还 context。5. Wait-Die 的裁决逻辑以 DRM 实际采用的 Wait-Die 为例当事务 T 试图锁一把已被事务 H 持有的 ww_mutex 时T 更老stamp 更小T 更年轻stamp 更大T 抢锁锁已被 H 持有比较 stampT 等待 H 释放老者有优先权安全等待T 返回 -EDEADLK释放全部锁并重试DieH 释放后 T 获锁以原 stamp 重新发起其正确性直觉在于只有较老的事务才被允许等待较年轻的事务。由于 stamp 全局单调递增且事务重试时不换号任一时刻最老的事务永远不会退避、也不会被阻塞成环因此系统整体必然向前推进——最老者终将拿全所有锁并完成随后次老者补位如此循环杜绝了死锁与饥饿。6. 与上层的关系ww_mutex是纯粹的锁原语本身不涉及 GPU 语义。DRM 在其上构建了两层封装dma_resv内嵌一把ww_mutex锁类为reservation_ww_classWait-Die使锁定一个 BO即锁定其 reservationdrm_exec进一步把「取号 → 遍历加锁 →-EDEADLK回退 → 重试」的完整样板封装为一组宏调用方只需声明要锁哪些 GEM 对象无须手写回退逻辑。命令提交、页表更新等所有需要批量锁定 BO 的路径最终都落到这套ww_mutex机制之上。7. 小结多 BO 加锁的顺序由用户态决定内核无法回避ABBA 死锁普通 mutex 不足以应对ww_mutex为每次加锁事务分配一个全局递增的stampticket据此判定事务年龄并裁决冲突两种算法Wait-Die / Wound-Wait均无死锁无饥饿DRM 的reservation_ww_class采用Wait-DieDEFINE_WD_CLASS核心接口为ww_acquire_init/finiww_mutex_lock返回-EDEADLK即回退_slow变体事务重试时必须复用同一 stampww_mutex是dma_resv与drm_exec的共同底座是理解 6.3.3 与第八章命令提交加锁流程的前提。