6.3.2 剖析的ww_mutex提供了多锁死锁避免的原语但其「取号 → 遍历加锁 → 遇-EDEADLK回退 → 从冲突锁重试」的样板逻辑若由每个驱动手写既繁琐又极易在错误路径上遗漏回退而泄漏锁。drm_exec正是对这一模式的官方封装调用方只需声明要锁哪些 GEM 对象、各预留几个 fence slot回退与重试全部交由框架处理。第八章命令提交的amdgpu_cs_parser_bos、以及页表更新等所有批量锁 BO 的路径都建立在它之上。1. 它解决什么drm_exec的职责用其源码 DOC 的原话即“abstracts the retry loop necessary for locking multiple GEM objects”——抽象出锁定多个 GEM 对象所必需的重试循环。其行为约定为一旦在加锁某对象时检测到竞争ww_mutex返回-EDEADLK清理流程会解锁此前已锁定的全部对象并在重来时优先锁定那个引发冲突的对象对象锁定后可选择在其dma_resv上预留 fence slot供后续无分配地挂载 fence。2. 数据结构structdrm_exec{u32 flags;/* DRM_EXEC_INTERRUPTIBLE_WAIT / IGNORE_DUPLICATES */structww_acquire_ctxticket;/* 6.3.2 的 acquire context事务票据*/unsignedintnum_objects;/* 已锁定对象数 */unsignedintmax_objects;/* objects 数组容量 */structdrm_gem_object**objects;/* 已锁定对象数组 */structdrm_gem_object*contended;/* 上次因竞争而回退的对象 */structdrm_gem_object*prelocked;/* 回退后已抢先锁住的对象 */};其中ticket即 6.3.2 的ww_acquire_ctx——drm_exec本质上就是围绕一个ww_acquire_ctx管理一组 GEM 对象锁的容器。contended与prelocked是回退机制的关键前者记住上次锁不上的那个对象后者标记重来时已把它抢先锁住。3. 使用范式标准用法由一组宏构成源码 DOC 给出的模板structdrm_execexec;structdrm_gem_object*obj;unsignedlongindex;drm_exec_init(exec,DRM_EXEC_INTERRUPTIBLE_WAIT,0);drm_exec_until_all_locked(exec){retdrm_exec_prepare_obj(exec,boA,1);drm_exec_retry_on_contention(exec);if(ret)gotoerror;retdrm_exec_prepare_obj(exec,boB,1);drm_exec_retry_on_contention(exec);if(ret)gotoerror;}drm_exec_for_each_locked_object(exec,index,obj){dma_resv_add_fence(obj-resv,fence,DMA_RESV_USAGE_READ);}drm_exec_fini(exec);三个宏各司其职宏作用drm_exec_until_all_locked(exec)外层重试循环循环体每次从零对象锁定的干净状态开始直至一次性锁定全部对象且无竞争drm_exec_prepare_obj(exec, obj, n)锁定obj并在其dma_resv上预留n个 fence slotdrm_exec_retry_on_contention(exec)若刚才的加锁遇到竞争则跳回循环头重来3.4 一个形象的比喻凭号牌占储物柜在深入代码前不妨把整个机制想象成一群人在健身房同时预定一排储物柜每个储物柜 一个 BO进门取号ww_acquire_init/drm_exec_init每人入场领一个号牌号越小代表来得越早越老。按清单逐个占柜prepare_obj你拿着自己要用的储物柜清单一个个去上锁。遇到冲突-EDEADLK若某个柜子已被一个号牌更小更老的人占着规矩要求你立刻退让——把自己已占的柜子全部让出回到门口重来unlock_all 回到循环头。号牌不变复用同一 stamp重来时你不重新领号仍用最初那张——否则你会越退越年轻永远抢不过别人而饿死。优先直奔冲突柜contended/prelocked重来时你不再从头乱抢而是先走到刚才让你吃亏的那个柜子前排队死等拿到它之后再去占其余的。slot预留fence slot占到柜子后你还顺手在柜门上预留好几个挂钩供稍后挂东西fence用免得到时手忙脚乱地临时找钩子临时分配内存。由于最老的人永远不必退让他总能一次性占齐所有柜子并办完事随后次老者补上……整个队伍必然向前推进既不会死锁也不会有人永远排不上。后面的源码本质上就是把这套取号—占柜—让位—直奔冲突柜的规矩用 C 宏与指针精确实现出来。阅读提示到这里drm_exec的用法已经讲完——多数情况下你只需照上面的模板写「init→until_all_locked { prepare_obj retry_on_contention }→ 遍历 →fini」即可正确使用它无须关心内部如何回退。后续第 4、5 节将深入其实现原理计算跳转、状态机、contended/prelocked配合涉及较多内核技巧若你只想会用而非精通可以放心跳过这两节直接阅读第 6 节的 fence slot 预留。4. until_all_locked 的实现计算跳转 cleanupdrm_exec_until_all_locked的宏展开颇具技巧——它用 GCC 的**标签地址计算跳转**实现跳回循环体开头因为 C 不允许在循环体内定义可被外部goto的标签#definedrm_exec_until_all_locked(exec)\__PASTE(__drm_exec_,__LINE__):\for(void*__drm_exec_retry_ptr;({\__drm_exec_retry_ptr__PASTE(__drm_exec_,__LINE__);\(void)__drm_exec_retry_ptr;\drm_exec_cleanup(exec);\});)#definedrm_exec_retry_on_contention(exec)\do{\if(unlikely(drm_exec_is_contended(exec)))\goto*__drm_exec_retry_ptr;\}while(0)循环条件调用的drm_exec_cleanup是状态机的核心它决定是否继续循环booldrm_exec_cleanup(structdrm_exec*exec){if(likely(!exec-contended)){/* 无竞争完成 */ww_acquire_done(exec-ticket);returnfalse;/* 退出循环保持已锁对象 */}if(likely(exec-contendedDRM_EXEC_DUMMY)){/* 首次进入 */exec-contendedNULL;ww_acquire_init(exec-ticket,reservation_ww_class);returntrue;/* 进入循环体 */}drm_exec_unlock_all(exec);/* 有竞争解锁全部重来 */exec-num_objects0;returntrue;}其三条分支恰好对应循环的三种时刻 DUMMY首次 NULL无竞争指向某对象发生过竞争是否进入 until_all_locked→ drm_exec_cleanupcontended?ww_acquire_init 取号返回 true执行循环体ww_acquire_done返回 false退出循环unlock_all num_objects0返回 true重来循环体逐个 prepare_objretry_on_contention检测到竞争?goto 计算跳转回到循环头全部锁定 → 下轮 cleanup 走 NULL 分支退出初始时exec-contended被置为哨兵值DRM_EXEC_DUMMY确保第一次进入循环时走ww_acquire_init取号分支。5. 加锁与回退contended / prelocked 的配合真正的加锁在drm_exec_lock_obj中完成其精妙之处在于回退后如何优先锁住冲突对象intdrm_exec_lock_obj(structdrm_exec*exec,structdrm_gem_object*obj){retdrm_exec_lock_contended(exec);/* ① 先处理上次的冲突对象 */if(unlikely(ret))returnret;if(exec-prelockedobj){/* ② 冲突对象已被抢先锁住 */drm_gem_object_put(exec-prelocked);exec-prelockedNULL;return0;}retdma_resv_lock(obj-resv,exec-ticket);/* ③ 正常加锁 */if(unlikely(ret-EDEADLK)){/* ④ 竞争记住它回退 */drm_gem_object_get(obj);exec-contendedobj;return-EDEADLK;}...returndrm_exec_obj_locked(exec,obj);/* ⑤ 记入 objects 数组 */}配合drm_exec_lock_contended——它在重来循环的开头被调用用ww_mutex的_slow变体对应 6.3.2 的dma_resv_lock_slow率先锁住上次冲突的对象staticintdrm_exec_lock_contended(structdrm_exec*exec){structdrm_gem_object*objexec-contended;if(likely(!obj))return0;exec-contendedNULL;dma_resv_lock_slow(obj-resv,exec-ticket);/* 阻塞式先拿到它 */drm_exec_obj_locked(exec,obj);exec-prelockedobj;/* 标记已抢先 */return0;}这样便实现了 DOC 承诺的回退时优先锁定冲突对象一旦某对象引发-EDEADLK框架解锁全部、把它记为contended下一轮循环开头先用慢路径把它锁牢prelocked从而保证事务在有限轮次内收敛——这正是 6.3.2 Wait-Die 较年轻者回退、以原 stamp 重试的工程化落地。ww_mutex/dma_resvlock_obj循环体ww_mutex/dma_resvlock_obj循环体cleanup 解锁全部 num_objects0boX 成为 prelocked优先锁定prepare_obj(boX)1dma_resv_lock(ticket)2-EDEADLK竞争3contended boX返回4retry_on_contention → goto 循环头5新一轮首个 prepare_obj6lock_contendeddma_resv_lock_slow(boX)76. prepare_obj加锁 预留 fence slotdrm_exec_prepare_obj在锁定之上叠加了 fence slot 预留intdrm_exec_prepare_obj(structdrm_exec*exec,structdrm_gem_object*obj,unsignedintnum_fences){retdrm_exec_lock_obj(exec,obj);if(ret)returnret;retdma_resv_reserve_fences(obj-resv,num_fences);/* 预留 slot */if(ret){drm_exec_unlock_obj(exec,obj);returnret;}return0;}预留 fence slot 的意义后续向dma_resv挂载 fencedma_resv_add_fence时若临时扩容会触发内存分配而挂载往往发生在禁止分配的临界区如第八章 submit 持notifier_lock时。因此必须在加锁阶段就把所需 slot 数一次性备足。调用方按本次会向该 BO 挂几个 fence传入num_fences——例如命令提交按 gang 中 job 数量预留。7. 收尾与遍历drm_exec_fini调用drm_exec_unlock_all以加锁的逆序逐个dma_resv_unlock并释放 GEM 引用再ww_acquire_fini归还票据drm_exec_for_each_locked_object遍历objects[]数组是加锁成功后统一挂载 fence、执行 validate 等操作的标准迭代器第八章parser_bos即以它遍历所有锁定对象逐一ttm_bo_validate。8. 小结drm_exec把ww_mutex6.3.2的取号—加锁—-EDEADLK回退—重试样板封装为until_all_lockedprepare_objretry_on_contention三个宏until_all_locked借助标签地址计算跳转实现回循环由drm_exec_cleanup状态机在首次取号 / 无竞争退出 / 有竞争重来三分支间切换contendedprelocked配合dma_resv_lock_slow实现回退后优先锁定冲突对象保证事务收敛——即 Wait-Die 的工程化落地prepare_obj在加锁之上预留 fence slot为后续禁分配临界区中的dma_resv_add_fence铺路它是第八章命令提交与页表更新等一切批量锁 BO 路径的统一入口将驱动从手写回退逻辑中彻底解放。