6.1.4 的dma_fence_array把 N 个 fence无序聚合成一个,表达「一组工作同时就绪」。另一类需求是有序的:一条 timeline 上有一串单调递增的进度点(point),point 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 内部只持有单个 fence,signal 一次、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_fence,base.seqno 即该节点的 point 值structdma_fence__rcu*prev;// 指向链中前一个节点(RCU 保护,支持无锁遍历与 GC)u64 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「达成」的实际工作 fence;prev指向前一个(更小的)point。 base内嵌使 chain 本身是一个标准dma_fence,对上完全复用 6.1 讲的生命周期与接口。prev用 RCU 保护,遍历时无需持锁,同时支持在遍历中原子回收已 signal 的节点(§6)。cb与work共用 union:signal 传播的两个阶段(挂回调 / 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 或 chain)structdma_fence*fence,// 本 point 的工作 fenceuint64_tseqno);// 本 point 的值分离的意义在于:signal 路径上追加 timeline point 时,可以先在允许睡眠的上下文alloc,再在不可失败的临界区init(预分配模式,参见附录 B.6)。init自身不做分配,因此不会失败。
3.2 context 复用规则:一条 timeline 一个 context
init的核心是决定新节点的(context, seqno):
structdma_fence_chain*prev_chain=to_dma_fence_chain(prev);if(prev_chain&&__dma_fence_is_later(prev,seqno,prev->seqno)){// 前驱是 chain,且新 point 值更靠后 → 复用同一条 timeline 的 contextcontext=prev->context;chain->prev_seqno=prev->seqno;}else{// 否则开一条新 timelinecontext=dma_fence_context_alloc(1);if(prev_chain)seqno=max(prev->seqno,seqno);// 保证 seqno 始终有效递增}dma_fence_init64(&chain->base,&dma_fence_chain_ops,&chain->lock,context,seqno);这段逻辑正是 timeline 语义的落点:同一条 timeline 上的所有 point 共享同一个context,seqno取 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 链的形态
新 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*f=dma_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*chain=to_dma_fence_chain(*pfence);if(!seqno)return0;if(!chain||chain->base.seqno<seqno)return-EINVAL;// 不是 chain,或 timeline 尚未推进到 seqnodma_fence_chain_for_each(*pfence,&chain->base){if((*pfence)->context!=chain->base.context||to_dma_fence_chain(*pfence)->prev_seqno<seqno)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*head=to_dma_fence_chain(fence);dma_fence_get(&head->base);dma_fence_chain_for_each(fence,&head->base){structdma_fence*f=dma_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_work;irq_work里重新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 中 GC):dma_fence_chain_walk下行时,若前驱节点已 signal,就用cmpxchg把prev原子地改指向「前驱的前驱」,摘除已完成节点:
tmp=unrcu_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((prev=rcu_dereference_protected(chain->prev,true))){if(kref_read(&prev->refcount)>1)break;// 还有别的持有者,交给它释放prev_chain=to_dma_fence_chain(prev);if(!prev_chain)break;chain->prev=prev_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, seqno=point),链尾更新为新节点 |
| timeline wait(point) | dma_fence_chain_find_seqno()定位到覆盖 point 的节点,再dma_fence_wait |
| binary syncobj | 内部直接是单个 fence,不经过 chain |
wait-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_array | dma_fence_chain |
|---|---|---|
| 组合形态 | N 个子 fence无序聚合成一个 | 节点有序串成单向链 |
| 完成语义 | AND(全部)/ OR(任一) | 单调蕴含:point N 完成 ⟹ 更早 point 完成 |
| context/seqno | 自身独立 context | 一条 timeline 共享 context,seqno = point 值(64 位) |
| 典型场景 | 一次等待多块 BO / 多条 in-fence | Vulkan timeline semaphore、drm_syncobjtimeline |
| 定位能力 | 无 | find_seqno按 point 定位节点 |
| 共同约束 | 封装的子 fence 不能是容器,需先摊平 | 同左(WARN_ON(dma_fence_is_chain(fence))) |
两者是互补的组合原语:array 表达「并列的一组」,chain 表达「递进的一串」。
9. 小结
dma_fence_chain以链式结构把 fence 组织成一条 timeline:
- 数据结构:内嵌
base使其本身是 fence;prev串接前驱(RCU 保护);fence封装本 point 的工作;prev_seqno标明 point 覆盖区间。 - timeline 语义:同一 timeline 共享 context,seqno 取 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的家族。