6.1.1 到 6.1.5 逐个拆解了 dma_fence 家族的成员:base fence 的生命周期、context/seqno 的排序身份、spinlock 的并发保护、enable_signaling 的按需使能,以及两个组合原语 array 与 chain。本篇换一个视角,把它们作为一个整体重新审视——dma_fence_array、dma_fence_chain都「内嵌一个dma_fence且又持有一组dma_fence」,从面向对象设计的设计模式视角总结下。
用一句话概括本篇要论证的结论:
dma_fence 家族是「组合模式(Composite)」在内核 C 语言中的一次典范实现:用结构体内嵌 +
ops虚表实现子类型多态,让「一组 fence」对上层伪装成「一个 fence」。
下面从机制层、模式层、约束层三层展开。
1. 家族全景:谁是谁
先把成员和它们的角色列清楚:
| 成员 | 本质 | 角色 | 详见 |
|---|---|---|---|
struct dma_fence | 抽象基类(接口 + 生命周期骨架) | 抽象 fence | 6.1 |
| 驱动 hw fence | 内嵌dma_fence,对应一次硬件完成 | 叶子(Leaf) | 6.1 |
dma_fence_stub | 已 signal 的空 fence(占位/兜底) | 特殊叶子 | 6.1 |
dma_fence_array | 内嵌base+ 持有 N 个子 fence | 组合(Composite)·扁平 | 6.1.4 |
dma_fence_chain | 内嵌base+ 持有 prev 链 + 1 个工作 fence | 组合(Composite)·线性 | 6.1.5 |
它们共享同一个基类,构成一棵「部分—整体」树:叶子是真正对应硬件完成的 fence,组合节点内部聚合其它 fence,而对上层消费者,两者都只是一个struct dma_fence *。
注意 array 与 chain 的聚合边(o-->)又指回dma_fence自身——组合节点持有的仍是抽象 fence,可以是叶子,也可以(在受限条件下)是另一个组合。这种「基类聚合基类」的自引用,正是组合模式树形结构的来源。
2. 机制层:C 语言如何实现「继承」
家族没有 C++ 的class,它用内核惯用的三件套实现子类型多态:
| 面向对象概念 | C 语言实现 | 家族中的体现 |
|---|---|---|
| 继承 / is-a | 结构体把基类作为第一个成员内嵌 | struct dma_fence_array { struct dma_fence base; ... } |
| 向上转型(upcast) | 取内嵌成员地址 | &array->base当作struct dma_fence *传出 |
| 向下转型(downcast) | container_of | to_dma_fence_array()/to_dma_fence_chain() |
| 虚函数 / 动态分发 | 函数指针表 | fence->ops->{enable_signaling, signaled, wait, release} |
| 运行时类型识别(RTTI) | 比较ops指针 | dma_fence_is_array()/dma_fence_is_chain() |
base放在首成员,使struct dma_fence *与struct dma_fence_array *在地址上重合,向上转型零开销;ops指针再把同名调用分发到各自实现。这与struct kobject、struct inode、sk_buff的做法一脉相承——是 Linux 内核里「面向对象 C」的标准范式。
而lock字段是指针(spinlock_t *)这一点也在此得到解释:基类不内嵌锁实例,而由子类提供自己的lock并让base.lock指过去(array 的base.lock指向array->lock,chain 同理)。基类只约定「有一把锁」,锁的归属交给子类——这正是继承中「基类定接口、子类定实现」的体现(详见 6.1.2)。
3. 模式层:核心是组合模式(Composite)
继承只是手段,家族真正要表达的意图是组合模式。GoF 对组合模式的定义是:
将对象组合成树形结构以表示「部分—整体」层次,使客户端对「单个对象」和「组合对象」的使用方式一致。
逐条对应到 dma_fence 家族:
- 统一接口(Component):
dma_fence+dma_fence_ops。消费接口dma_fence_wait()/dma_fence_add_callback()/ 挂入dma_resv全部只认struct dma_fence *。 - 叶子(Leaf):驱动 hw fence,对应一次真实硬件完成,没有子节点。
- 组合(Composite):
dma_fence_array、dma_fence_chain,内部聚合子 fence,自身完成与否取决于子节点。 - 一致性(关键收益):消费者无需知道手里的 fence 是叶子还是组合。等待一块 BO 的写完成,和等待「N 块 BO 全部写完成」的聚合 fence,写法完全一样。
这份「一致性」就是家族全部价值所在:上层(dma_resv、drm_syncobj、命令提交)永远只面对单个dma_fence接口,而「一个还是一组」「无序还是有序」的复杂度被封装进组合节点内部。
除 Composite 外,家族还叠加了三个配套模式,各司其职:
| 设计模式 | 在家族中的落点 | 作用 |
|---|---|---|
| Composite(组合) | array / chain 聚合子 fence | 让一组 fence 伪装成一个 fence(主模式) |
| Strategy(策略)/ 虚表 | dma_fence_ops四个钩子 | 同一动作路由到 leaf/array/chain 各自实现 |
| Template Method(模板方法) | dma_fence_signal()/dma_fence_wait_timeout() | 核心定骨架,可变步骤下沉到ops |
| Observer(观察者) | add_callback注册、signal时遍历cb_list通知 | 完成事件的一对多异步通知(详见 6.2) |
可以说:Composite 是目的,Strategy(ops) 是手段,Template Method 定骨架,Observer 管通知。
4. 两种组合拓扑:array 扁平 vs chain 线性
同样是 Composite,array 与 chain 是这棵组合树的两种不同形态,这正是家族最有意思的地方:
| 维度 | dma_fence_array(6.1.4) | dma_fence_chain(6.1.5) |
|---|---|---|
| 拓扑 | 扁平、宽:一层,N 个兄弟 | 线性、深:单向链,prev递归 |
| 聚合语义 | AND(全就绪)/ OR(任一),靠num_pending | 有序 timeline:单调递增 + 蕴含性 |
| context/seqno | 自身独立 context | 一条 timeline 共享 context,seqno = point 值 |
| signal 传播 | 子回调递减num_pending,归零经 irq_work 冒泡 | 沿链逐段 enable、经 irq_work 推进 |
| 定位能力 | 无 | find_seqno按 point 定位节点 |
| 类比数据结构 | 集合 / 计数栅栏 | 链表 / 版本号轴 |
| 一句话 | 表达「并列的一组」 | 表达「递进的一串」 |
两者互补:需要「多个结果同时(无序)就绪」用 array,需要「有序进度点、后者蕴含前者」用 chain。它们共同覆盖了组合模式在同步语义上的两个正交维度——无序并列聚合与有序串接。
5. 虚表对照:同一接口,三套实现
把 Strategy 落到实处,看四个核心ops钩子在叶子与两个组合上的不同实现,最能体现「多态」:
ops钩子 | 叶子(hw fence) | dma_fence_array | dma_fence_chain |
|---|---|---|---|
get_timeline_name | 驱动 timeline 名 | "unbound" | "unbound" |
enable_signaling | 开硬件完成中断 | 为每个子 fence 挂回调 | 为链上首个未完成节点挂回调 |
signaled | 查硬件状态 | 读num_pending <= 0 | 遍历全链判定 |
release | dma_fence_free(kfree_rcu) | put 所有子 fence + kfree 数组 | 手动解链防栈溢出 |
同一个dma_fence_wait()调用,落到不同ops上就有截然不同的行为——这就是虚表分发的意义。上层代码写一次,家族里每种 fence 各自「翻译」成自己的完成判定方式。
6. 模式带来的约束
家族里几条看似琐碎的实现约束,其实都是「组合树」这一模式在内核无栈递归约束下的直接后果。理解了模式,这些规矩就不再是需要死记的特例:
- array 禁止嵌套容器(
WARN_ON(dma_fence_is_container(...)))+dma_fence_unwrap_merge()摊平:array 选择做扁平组合,用「构造前 flatten 成一层」替代「运行时递归遍历」,避免组合树过深、在 signal/遍历/release 时耗尽内核栈。 - chain 的
release手动解链:chain 是深链形态的组合,靠析构递归dma_fence_put(prev)会栈溢出,于是把递归展平为循环。与 array「禁止嵌套」是同一问题(递归深度)的两种解法——array 从源头压平,chain 在析构时展平。 num_pending+ irq_work(array)/ 逐段 irq_work(chain):组合节点要等所有/下一个子节点完成再向上游 signal,是组合树里「完成事件向根冒泡」的实现;用 irq_work 把 signal 收尾切到干净上下文,规避在子 fence 锁内触发下游回调造成的锁序反转与递归。cb/work共用 union:组合节点在「等待阶段(挂回调)」与「收尾阶段(irq_work)」不会同时发生,复用同一块内存——是组合节点两阶段生命周期的体现。- 两段式
alloc+init:把「可能失败的分配」与「不可失败的初始化」分离,支持在 reclaim / signaling 上下文用预分配对象构造组合节点(附录 B.6 的预分配模式)。 - 两套遍历接口:
dma_fence_array_for_each(浅层,只展开一层)与dma_fence_unwrap_for_each(深度,递归展开所有容器交出叶子)——正是组合模式经典的树遍历,按「是否深入子树」分成两档。
7. 家族如何协同出场
这些成员并非各自孤立,它们在上层同步设施里经常一起出现:
dma_resv_get_singleton()把一块 BO 上的多个 fence 合成一个对外句柄,底层构造扁平的 array(6.3.2)。drm_syncobj的 binary 模式直接持单个 fence(叶子),timeline 模式则用chain表达单调递增的 point(6.4.2)。- 无论哪条路径,最终的叶子都是驱动的 hw fence——组合节点只是把它们按不同拓扑组织起来。
8. 小结
从继承视角回看,dma_fence 家族是一套结构清晰的面向对象设计:
- 机制层:结构体内嵌
base+container_of转型 +ops虚表,实现 C 语言的子类型多态;lock用指针交由子类提供,是「基类定接口、子类定实现」的缩影。 - 模式层:核心是组合模式(Composite)——让「一组 fence」对上层伪装成「一个 fence」;配套 Strategy(ops 分发)、Template Method(核心定骨架)、Observer(signal 通知回调)。
- 两种拓扑:array 是扁平的 AND/OR 组合,chain 是线性有序的 timeline 组合,正交互补。
- 约束即模式:不可嵌套 / 手动解链 / unwrap 遍历 / irq_work 冒泡,都是「组合树」在内核无栈递归约束下的工程化取舍,而非零散特例。
dma_fence定义了「一次异步完成」的抽象;array 与 chain 用组合模式把这个抽象递归地组织成树——并列成组、有序成链。上层只需面对一个dma_fence *,家族在内部消化了「一个还是一组、无序还是有序」的全部复杂度。
至此 6.1 全章(基类、context/seqno、spinlock、enable_signaling、array、chain 及本总结篇)完结。接下来6.2回到 fence 的消费侧,深入阻塞等待与异步回调两种同步策略。