十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

第六章:异步访问的同步 —— 6.3.2 dma_resv fence 操作接口详解

第六章:异步访问的同步 —— 6.3.2 dma_resv fence 操作接口详解 本篇讲解dma_resv操作 fence 的接口——如何挂入、顶替、查询与等待并重点说明三个易错点为什么加 fence 要拆成两步、无锁迭代为什么会「重启」、为什么没有「删除单个 fence」的接口。1. 接口全景增 / 改 / 查 / 等dma_resv对外的 fence 操作可归为四组分组接口是否需持写锁作用增dma_resv_reserve_fences()dma_resv_add_fence()是预留容量并挂入新 fence改dma_resv_replace_fences()是按 context 替换旧 fence查dma_resv_get_fences()/dma_resv_get_singleton()/ 迭代器否RCU取全部 / 合成单个 / 逐个遍历等dma_resv_wait_timeout()/dma_resv_test_signaled()/dma_resv_set_deadline()否RCU阻塞等 / 非阻塞测 / 截止时间提示这里有一条贯穿始终的规则凡是带usage参数的查询/等待接口都遵循 6.3.1 的层级包含关系——请求某级即连带覆盖所有更严格数值更小的级别。我们常说“增删改查”但对 fence 的操作列表里为何没有「删」dma_resv不提供「移除单个 fence」的公开接口因为它没有必要已 signal 的 fence 会在下一次reserve_fences()时被自动修剪回收见 §2.2针对同一 timeline 的旧 fence由replace_fences()或add_fence()的原地替换逻辑覆盖见 §2.3、§3。因此「删」被融入了「增 / 改」之中而非独立操作。写侧仅需添加回收由框架完成。2. 增reserve_fencesadd_fenceintdma_resv_reserve_fences(structdma_resv*obj,unsignedintnum_fences);voiddma_resv_add_fence(structdma_resv*obj,structdma_fence*fence,enumdma_resv_usageusage);添加一个 fence 被刻意拆成两步且两步的失败语义截然不同。2.1 两步式把「可能失败」隔离到临界区之外dma_resv_reserve_fences()先为即将加入的num_fences个 fence预留数组容量。这一步分配内存可能返回-ENOMEM。dma_resv_add_fence()才把 fence 真正挂入它返回 void、保证不失败。这样设计的原因在于add_fence的调用现场它往往发生在命令提交的临界区里——此时已持有一批 BO 的ww_mutex、fence 也已 arm进入可被等待的状态。如果挂入这一步还可能因内存分配失败而中断错误回滚将极其困难已 arm 的 fence 难以撤回。把「可能失败的分配」提前到reserve阶段隔离出去临界区内的挂入就成了确定性操作。因此存在一条硬性约束add_fence前必须已reserve足够容量否则触发BUG_ON(fobj-num_fences fobj-max_fences)。返回 -ENOMEM成功dma_resv_lock()dma_resv_reserve_fences(n)加锁前安全回退... 构建并 arm fence ...dma_resv_add_fence()不会失败dma_resv_unlock()2.2reserve_fences的扩容与修剪reserve不仅仅是「够就返回、不够就扩容」它在扩容时顺带完成一次垃圾回收olddma_resv_fences_list(obj);if(oldold-max_fences){if((old-num_fencesnum_fences)old-max_fences)return0;// 容量足够直接返回maxmax(old-num_fencesnum_fences,old-max_fences*2);}else{maxmax(4ul,roundup_pow_of_two(num_fences));// 首次分配}newdma_resv_list_alloc(max);扩容策略首次至少 4 个槽位之后按「所需总量」与「翻倍」取较大者摊薄频繁 reserve 的分配开销。顺带修剪把旧数组的内容搬到新数组时未 signal 的 fence 压缩到前段、已 signal 的移到尾部并在切换后dma_fence_put释放。这就是「已完成的 fence 无需手动删除」的实现所在。2.3add_fence的原地顶替去重与升级add_fence并非单纯追加。挂入前它会先扫描现有列表命中以下任一条件时原地替换而非新增槽位for(i0;icount;i){dma_resv_list_entry(fobj,i,obj,old,old_usage);if((old-contextfence-contextold_usageusagedma_fence_is_later_or_same(fence,old))||dma_fence_is_signaled(old)){dma_resv_list_set(fobj,i,fence,usage);// 顶替dma_fence_put(old);return;}}两类顶替同一 timeline 的新进度新 fence 与某个旧 fence 同context、且更靠后is_later_or_same、且old_usage usage即不会降低可见性则用新 fence 顶替旧的。这既保证同一条 timeline 在列表里只留最新一个又实现了 6.3.1 所述的只升不降——usage只能朝更可见的方向变化。回收已 signal 的槽位任意已 signal 的旧 fence 都可被新 fence 直接顶替复用其槽位。只有当无可顶替时才真正占用一个新槽位此时smp_wmb()保证「fence 写入」先于「num_fences自增」对 RCU 读者可见——读者要么看不到这个新 fence要么看到的是完整的条目不会读到半初始化状态。注意add_fence会WARN_ON(dma_fence_is_container(fence))。不能把dma_fence_array/dma_fence_chain这类容器 fence 直接挂入必须拆成单个 fence 逐一添加否则会破坏列表的遍历与回收假设。3. 改replace_fencesvoiddma_resv_replace_fences(structdma_resv*obj,uint64_tcontext,structdma_fence*replacement,enumdma_resv_usageusage);它把obj中所有属于指定context的 fence 一次性替换为replacement每次替换都对replacement取一次引用、对旧 fenceput一次。与add_fence的「追加/顶替最新」不同replace_fences是面向整条 timeline 的定向替换。典型用途是内核内部的资源状态迁移——例如用页表更新 fence顶替抢占 fence当页表更新完成后原抢占 fence 所代表的操作已无法再访问该资源于是用新 fence 表达「资源已进入新状态」。其正确性前提正是内核注释强调的旧 fence 代表的操作在新 fence 完成时必须已丧失对资源的访问权。4. 查三种粒度查询按「要多少、要什么形态」分为三档。4.1get_fences导出全部到数组intdma_resv_get_fences(structdma_resv*obj,enumdma_resv_usageusage,unsignedint*num_fences,structdma_fence***fences);无锁遍历指定usage级别下的所有 fence逐个dma_fence_get后填入数组返回。调用方负责对每个 fenceput并kfree数组。由于底层用的是无锁迭代器遍历中途若发生 restart见 §4.3函数会丢弃已收集的部分、按新的cursor.num_fences重新krealloc数组并从头收集——最终返回的一定是某个一致快照。4.2get_singleton合成单个代表 fenceintdma_resv_get_singleton(structdma_resv*obj,enumdma_resv_usageusage,structdma_fence**fence);当上层只想要「一个代表整体完成」的 fence 时使用内部先调get_fences再按数量分流0 个→ 返回NULL1 个→ 直接返回该 fence多个→ 用dma_fence_array_create聚合成一个dma_fence_array其 base 作为结果返回。最典型的消费者是显式同步把这个单一 fence 挂到sync_file上导出给用户态详见 6.4。使用禁忌get_singleton合成的 fence不能再加回原dma_resv。内核注释明确警告这样做会在 finalizedma_fence_array时引发栈损坏自引用导致递归析构。它是「导出用」的只读聚合不是可回收的普通 fence。4.3 迭代器加锁版与无锁版底层遍历由迭代器提供两个包装宏对应两种并发模型// 持写锁遍历fence 在锁保护下有效无需额外引用dma_resv_for_each_fence(cursor,obj,DMA_RESV_USAGE_READ,fence){...}// 无锁遍历RCU每个 fence 已被 get循环体外需要 dma_resv_iter_end 收尾dma_resv_iter_begin(cursor,obj,DMA_RESV_USAGE_READ);dma_resv_for_each_fence_unlocked(cursor,fence){...}dma_resv_iter_end(cursor);传入的usage决定遍历范围迭代器只返回等于或高于该级别的 fence过滤条件cursor-usage cursor-fence_usage这正是 6.3.1 层级规则在查询侧的落地。无锁版的「重启」机制是本篇的关键难点。写侧可能在读者遍历期间通过reserve_fences替换整个数组指针无锁读者必须能感知并纠正structdma_fence*dma_resv_iter_first_unlocked(structdma_resv_iter*cursor){rcu_read_lock();do{dma_resv_iter_restart_unlocked(cursor);// 重新读取 list 指针与长度dma_resv_iter_walk_unlocked(cursor);// 走到下一个未 signal 的 fence}while(dma_resv_fences_list(cursor-obj)!cursor-fences);// 指针变了就重来rcu_read_unlock();returncursor-fence;}两道保护叠加列表指针校验每轮遍历结束后比较dma_resv_fences_list(obj)与迭代开始时记录的cursor-fences。一旦写侧换过数组二者不等整个遍历从头重启。fence 存活校验取 fence 时用dma_fence_get_rcu()而非普通 get若目标 fence 正在被并发释放引用计数已归零返回NULL迭代器同样触发 restart。因此无锁遍历的循环体必须可重入不能假设只遍历一次也不能在循环里做「累加计数」这类对重复访问敏感的操作除非用dma_resv_iter_is_restarted()判断并在重启时清零。若逻辑难以做到可重入应改用持锁版dma_resv_for_each_fence()。get_rcu 返回 NULL取到 fence是否restart_unlocked()记录 list 指针/长度walk_unlocked()dma_fence_get_rcu 取下一个遍历一轮后list 指针变了?返回 fence 给循环体5. 等wait_timeout/test_signaled/set_deadlinelongdma_resv_wait_timeout(structdma_resv*obj,enumdma_resv_usageusage,bool intr,unsignedlongtimeout);booldma_resv_test_signaled(structdma_resv*obj,enumdma_resv_usageusage);voiddma_resv_set_deadline(structdma_resv*obj,enumdma_resv_usageusage,ktime_tdeadline);三者都建立在无锁迭代器之上逐个处理usage覆盖范围内的 fencewait_timeout对每个 fence 调dma_fence_wait_timeout剩余超时递减传递。intr控制是否可被信号打断。返回值语义大于 0表示成功剩余 jiffies0表示超时负值-ERESTARTSYS表示被信号打断。test_signaled非阻塞地测试覆盖范围内的 fence 是否已全部 signal用于轮询式快速判断如显示翻页前的探测。set_deadline对每个 fence 调dma_fence_set_deadline向下传递一个截止时间提示供 GPU 调度 / DVFS 据此调频。它是性能 hint不改变同步语义——不会让任何操作提前或延后完成只影响调频决策。由于走的是无锁迭代器这三个接口不要求调用方持锁持锁调用也允许这也是隐式同步的读侧能高并发运行的基础。6. 小结本篇把dma_resv的 fence 操作接口按「增 / 改 / 查 / 等」四组讲清几条设计主线值得记住增拆两步reserve可失败、隔离分配add不失败、确定性挂入并在reserve时顺带修剪已 signal 的 fence写侧只增不删同 timeline 的旧 fence 由原地替换覆盖回收交给框架因此没有独立的「删」接口改是定向替换replace_fences面向整条 context用于内核内部的资源状态迁移查有三档数组导出、单 fence 聚合、逐个迭代无锁迭代以「列表指针校验 get_rcu存活校验」实现安全重启代价是循环体必须可重入等按层级收敛wait / test / set_deadline均无锁、均遵循usage包含规则。至此dma_resv的数据结构、usage 语义与操作接口已完整。写侧尚余一个问题命令提交常需同时锁住一批 BO 的dma_resv加锁顺序不一致即可能死锁。这把ww_mutex如何避免死锁见6.3.3 ww_mutex其上的批量加锁封装见6.3.4 drm_exec。延伸阅读持锁与内存分配关键路径的「预分配」模式
返回列表