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

资讯详情

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

6.1.5 dma_fence_chain — timeline 语义的底层支撑

6.1.5 dma_fence_chain — timeline 语义的底层支撑 6.1.4 的dma_fence_array把 N 个 fence无序聚合成一个表达「一组工作同时就绪」。另一类需求是有序的一条 timeline 上有一串单调递增的进度点pointpoint N 完成即蕴含所有更早的 point 已完成。dma_fence_chain就是承载这种语义的组合原语——它把 fence 串成一条单向链每个节点代表 timeline 上的一个 point是drm_syncobjtimeline对应 Vulkan timeline semaphore的底层支撑。本篇讲解 chain 的数据结构、创建与串接规则、timeline 的 point 查找、signal 传播以及它与drm_syncobjtimeline 的对接。1. 为什么需要 timeline chain用户态的drm_syncobj有两种模式binary二值syncobj 内部只持有单个 fencesignal 一次、wait 一次语义等价于一个 fence 句柄。timeline时间线syncobj 关联一个单调递增的 64 位计数器用户按point 值提交 signal、按point 值等待。语义遵循 Vulkan timeline semaphore单调递增point 值只增不减蕴含性point N 完成 ⟹ 所有 point N 均已完成wait-before-signal允许先对某个尚未提交的 point 挂等待之后该 point 的 fence 才被 signal。要用dma_fence表达这条 timeline需要一个结构满足三点既是标准 fence可wait/add_callback/ 挂入dma_resv又能表达「某个 point 的完成蕴含更早 point 的完成」还能在 point 的实际工作 fence 尚未就绪前先建立这个 point。dma_fence_chain用「链式串接 复用 context 遍历判定」实现了这三点。2. 数据结构/* include/linux/dma-fence-chain.h */structdma_fence_chain{structdma_fencebase;// 自身也是一个 dma_fencebase.seqno 即该节点的 point 值structdma_fence__rcu*prev;// 指向链中前一个节点RCU 保护支持无锁遍历与 GCu64 prev_seqno;// GC 前的前驱 seqno用于按 point 定位节点覆盖的区间structdma_fence*fence;// 本节点封装的实际工作 fence该 point 的完成条件union{structdma_fence_cbcb;// 未完成时挂到某个子 fence 上的回调structirq_workwork;// 收尾时irq_work规避锁序反转};spinlock_tlock;// base.lock 指向它};关键点每个节点 timeline 上一个 point。base.seqno是该 point 的值fence是让这个 point「达成」的实际工作 fenceprev指向前一个更小的point。base内嵌使 chain 本身是一个标准dma_fence对上完全复用 6.1 讲的生命周期与接口。prev用 RCU 保护遍历时无需持锁同时支持在遍历中原子回收已 signal 的节点§6。cb与work共用 unionsignal 传播的两个阶段挂回调 / irq_work 收尾不会同时发生复用同一块内存与 6.1.4 array 的设计同理。两个类型辅助函数// fence 是 chain 则转成 dma_fence_chain*否则 NULLstructdma_fence_chain*to_dma_fence_chain(structdma_fence*fence);// fence 是 chain 则返回其封装的 fence否则返回自身剥壳structdma_fence*dma_fence_chain_contained(structdma_fence*fence);3. 创建与串接3.1 两段式alloc init与dma_fence_array一致chain 的分配与初始化分离// 分配宏便于按调用点单独统计 alloc_tag——GFP_KERNELstructdma_fence_chain*dma_fence_chain_alloc(void);// 未发布前释放无需 RCU 宽限期voiddma_fence_chain_free(structdma_fence_chain*chain);// 初始化并串接voiddma_fence_chain_init(structdma_fence_chain*chain,structdma_fence*prev,// 前一个节点可为普通 fence 或 chainstructdma_fence*fence,// 本 point 的工作 fenceuint64_tseqno);// 本 point 的值分离的意义在于signal 路径上追加 timeline point 时可以先在允许睡眠的上下文alloc再在不可失败的临界区init预分配模式参见附录 B.6。init自身不做分配因此不会失败。3.2 context 复用规则一条 timeline 一个 contextinit的核心是决定新节点的(context, seqno)structdma_fence_chain*prev_chainto_dma_fence_chain(prev);if(prev_chain__dma_fence_is_later(prev,seqno,prev-seqno)){// 前驱是 chain且新 point 值更靠后 → 复用同一条 timeline 的 contextcontextprev-context;chain-prev_seqnoprev-seqno;}else{// 否则开一条新 timelinecontextdma_fence_context_alloc(1);if(prev_chain)seqnomax(prev-seqno,seqno);// 保证 seqno 始终有效递增}dma_fence_init64(chain-base,dma_fence_chain_ops,chain-lock,context,seqno);这段逻辑正是 timeline 语义的落点同一条 timeline 上的所有 point 共享同一个contextseqno取 point 值并单调递增呼应 6.1.1「context timeline」。注意这里用dma_fence_init64——point 值是完整 64 位置位DMA_FENCE_FLAG_SEQNO64_BIT走 64 位比较6.1.1 §3.1。prev_seqno记录 GC 前的前驱 seqno标明本节点覆盖的 point 区间为(prev_seqno, base.seqno]供按 point 查找使用。3.3 约束封装的 fence 不能是 chain// 容器只能通过 prev 串接不能把 chain 塞进 fence 字段WARN_ON(dma_fence_is_chain(fence));串接方向唯一链的延伸只走prev。若要把嵌套的依赖并入 point需由调用方先摊平例如合成dma_fence_array再作为fence传入。这与 6.1.4 array「容器不能嵌套」的约束一脉相承目的同样是避免遍历/释放时的深度递归。3.4 链的形态prevprevprevnode point3base.seqno3fencejob3node point2base.seqno2fencejob2node point1base.seqno1fencejob1NULL链头新 point 从链尾接入prev指向当前最新节点base.seqno取新的 point 值。对上层而言「timeline 当前的完成句柄」就是链尾那个 chain fence。4. timeline 语义单调蕴含与 point 查找4.1 完成的蕴含性chain 判定自身是否 signal靠遍历整条链staticbooldma_fence_chain_signaled(structdma_fence*fence){dma_fence_chain_for_each(fence,fence){structdma_fence*fdma_fence_chain_contained(fence);if(!dma_fence_is_signaled(f)){dma_fence_put(fence);returnfalse;}}returntrue;}一个节点 signaled当且仅当它自己的fence与其所有前驱节点的fence全部signaled。由此得到 timeline 的蕴含性point N 完成 ⟹ point 1…N-1 全部完成。4.2 按 point 查找节点等待「timeline 到达 point N」时需要先定位「将 signal point N」的那个节点intdma_fence_chain_find_seqno(structdma_fence**pfence,uint64_tseqno){structdma_fence_chain*chainto_dma_fence_chain(*pfence);if(!seqno)return0;if(!chain||chain-base.seqnoseqno)return-EINVAL;// 不是 chain或 timeline 尚未推进到 seqnodma_fence_chain_for_each(*pfence,chain-base){if((*pfence)-context!chain-base.context||to_dma_fence_chain(*pfence)-prev_seqnoseqno)break;// 停在覆盖区间 (prev_seqno, base.seqno] 含 seqno 的节点}dma_fence_put(chain-base);return0;}利用prev_seqno沿链下行直到某节点的覆盖区间包含seqno*pfence即指向该节点。随后对它dma_fence_wait就等价于「等待 timeline 到达 point N」。seqno尚未提交timeline 未推进到 N时返回-EINVAL这正是 wait-before-signal 需要处理的边界。4.3 遍历接口// 浅遍历沿 prev 逐个访问 chain 节点并顺带回收已 signaled 的节点#definedma_fence_chain_for_each(iter,head)...structdma_fence*dma_fence_chain_walk(structdma_fence*fence);dma_fence_chain_for_each遍历链上的每个节点若要深入到每个节点封装的fence可能又是 array 等容器使用 6.1.4 提到的dma_fence_unwrap_for_each。5. signal 传播机制chain 不主动轮询子 fence而是按需挂回调逐段推进staticbooldma_fence_chain_enable_signaling(structdma_fence*fence){structdma_fence_chain*headto_dma_fence_chain(fence);dma_fence_get(head-base);dma_fence_chain_for_each(fence,head-base){structdma_fence*fdma_fence_chain_contained(fence);dma_fence_get(f);if(!dma_fence_add_callback(f,head-cb,dma_fence_chain_cb)){dma_fence_put(fence);returntrue;// 挂在第一个未 signal 的子 fence 上等它回调}dma_fence_put(f);// 该子 fence 已 signal继续看下一个}dma_fence_put(head-base);returnfalse;// 整条链都已 signal}按需推进从链头head沿prev遍历跳过已 signal 的子 fence把回调head-cb挂到第一个未 signal的子 fence 上返回true。回调 → irq_work 收尾子 fence signal 时触发dma_fence_chain_cb它把工作转交irq_workirq_work里重新enable_signaling尝试挂到下一个未完成的子 fence若已无未完成者则dma_fence_signal(chain-base)点亮自身。chain.baseirq_workdma_fence_chain_cb子 fencechain.baseirq_workdma_fence_chain_cb子 fencealt[还有未完成子 fence][全部已完成]子 fence signal触发回调init_irq_work irq_work_queuerearm enable_signaling挂到下一个未完成子 fence挂新回调等待dma_fence_signal(chain.base)用irq_work而非在回调里直接推进是为了规避锁序反转与递归与 6.1.4 array、附录 B.6 中「完成信号临界区不就地做重活」的取向一致。head-cb与head-work复用同一 union两阶段互斥不会同时使用。6. 垃圾回收与防栈溢出timeline 会不断追加 point链可能很长。两处机制避免长链带来的资源与栈问题遍历时回收walk 中 GCdma_fence_chain_walk下行时若前驱节点已 signal就用cmpxchg把prev原子地改指向「前驱的前驱」摘除已完成节点tmpunrcu_pointer(cmpxchg(chain-prev,RCU_INITIALIZER(prev),RCU_INITIALIZER(replacement)));已 signal 的中间节点因此被逐步短路回收长链自动收缩。prev_seqno保留原始区间信息使 GC 后按 point 查找仍然正确。释放时手动解链release直接靠dma_fence_put递归释放整条链会在长链上耗尽内核栈。dma_fence_chain_release因此在只持有最后一个引用kref_read(prev-refcount) 1时手动沿prev逐节点解链把递归展平为循环while((prevrcu_dereference_protected(chain-prev,true))){if(kref_read(prev-refcount)1)break;// 还有别的持有者交给它释放prev_chainto_dma_fence_chain(prev);if(!prev_chain)break;chain-prevprev_chain-prev;// 摘链RCU_INIT_POINTER(prev_chain-prev,NULL);dma_fence_put(prev);}7. 与 drm_syncobj timeline 的对接dma_fence_chain的直接使用者是drm_syncobj的 timeline 模式syncobj 操作chain 侧动作timeline signal(point, fence)alloc一个 chain 节点init(prev当前链尾, fence, seqnopoint)链尾更新为新节点timeline wait(point)dma_fence_chain_find_seqno()定位到覆盖 point 的节点再dma_fence_waitbinary syncobj内部直接是单个 fence不经过 chainwait-before-signal由此成立等待方可以对尚未提交的 point 先行find_seqno此刻返回-EINVAL由 syncobj 层挂起为「未来 fence」等待待 signal 侧追加该 point 的 chain 节点后再兑现。命令提交路径中的SYNCOBJ_TIMELINE_WAIT/SYNCOBJ_TIMELINE_SIGNAL参见 8.4正是按 point 走这套接口。drm_syncobj的完整内核实现见 6.4.2。8. 与 dma_fence_array 的对比维度dma_fence_arraydma_fence_chain组合形态N 个子 fence无序聚合成一个节点有序串成单向链完成语义AND全部/ OR任一单调蕴含point N 完成 ⟹ 更早 point 完成context/seqno自身独立 context一条 timeline 共享 contextseqno point 值64 位典型场景一次等待多块 BO / 多条 in-fenceVulkan timeline semaphore、drm_syncobjtimeline定位能力无find_seqno按 point 定位节点共同约束封装的子 fence 不能是容器需先摊平同左WARN_ON(dma_fence_is_chain(fence))两者是互补的组合原语array 表达「并列的一组」chain 表达「递进的一串」。9. 小结dma_fence_chain以链式结构把 fence 组织成一条 timeline数据结构内嵌base使其本身是 fenceprev串接前驱RCU 保护fence封装本 point 的工作prev_seqno标明 point 覆盖区间。timeline 语义同一 timeline 共享 contextseqno 取 64 位 point 值单调递增signaled遍历全链保证「后完成蕴含先完成」。point 查找dma_fence_chain_find_seqno定位将 signal 指定 point 的节点支撑按 point 等待与 wait-before-signal。signal 传播enable_signaling逐段挂回调回调经 irq_work 推进到下一段规避锁序反转与递归。抗长链walk 时 GC 已完成节点release 时手动解链防栈溢出。上层对接是drm_syncobjtimeline 与 Vulkan timeline semaphore 的底层支撑。至此 6.1 的组合原语6.1.4 array、6.1.5 chain讲完。下一节我们从面向对象的设计模式角度总结下dma_fence的家族。
返回列表