1. 背景与问题域
前面讲了BO共享的需求,共享必然带来同步的问题,在计算机这个既复杂又高速的的系统里共享,更是一个挑战。
1.1 异构计算时代的同步挑战
现代计算系统早已超越了单一CPU处理的模式,进入了CPU、GPU、视频编解码器、显示控制器、DSP等多种专用硬件协同工作的异构计算时代。这些硬件设备往往:
- 异步执行:提交命令后立即返回,实际执行在硬件端异步完成
- 跨设备协作:一块内存可能被多个设备依次或并发访问
- 流水线处理:前一操作未完成时,后续操作就已提交
这带来了核心问题:如何安全、高效地协调多设备对共享内存的访问?
1.2 传统同步机制的局限
传统的内核同步原语(如mutex、semaphore、spinlock)主要为CPU间同步设计,面对硬件异步操作存在明显短板:
- 轮询低效:CPU不断查询硬件状态浪费计算资源
- 阻塞粗糙:简单的sleep无法精确表达设备间的依赖关系
- 缺乏跨设备语义:无法在GPU、显示器、视频编解码器等不同驱动间传递同步信息
1.3 dma-fence的诞生
dma-fence机制诞生于2012年,由Canonical和德州仪器联合开发,最初作为dma-buf框架的配套同步原语。它的核心理念是:
将"异步操作的未来完成点"对象化,提供统一的跨驱动同步抽象。
2. dma-fence的核心作用
2.1 异步操作的抽象与标准化
dma-fence将"一个尚未完成的异步操作"抽象为内核对象(struct dma_fence),提供标准接口:
- 创建:驱动提交异步任务时创建fence
- 信号:硬件完成操作后,驱动调用
dma_fence_signal()标记完成 - 等待:消费者通过
dma_fence_wait()阻塞等待完成 - 回调:注册
dma_fence_add_callback()实现异步通知
这种标准化使得不同厂商的GPU驱动、显示驱动、视频驱动能用同一套语义交换同步信息。
2.2 解耦生产者与消费者
传统同步往往要求生产者和消费者紧密配合,dma-fence实现了彻底解耦:
- 生产者:只管创建fence,完成时signal,无需知道谁在等待
- 消费者:只管获取fence并等待/注册回调,无需知道谁创建的
中间通过dma-buf的dma_resv(reservation object)管理fence集合,实现完全的松耦合架构。
2.3 显式与隐式同步的统一基础
Linux内核支持两种fence使用模式:
显式同步(Explicit Fencing)
用户态通过文件描述符传递fence(如sync_file),应用明确控制同步点,适合Vulkan等现代API。例如:Vulkan的VkSemaphore、Android的SyncFence。
隐式同步(Implicit Fencing)
fence隐式附加在dma-buf上(通过dma_resv),驱动自动管理同步,对应用透。传统OpenGL、X11的缓冲区管理
两种模式的底层都依赖dma-fence机制,只是使用方式不同。我们后续的内容的组织,就按这个分类展开细化。
3. 核心机制解析
struct dma_fence是整个机制的核心数据结构,其完整定义如下:
struct dma_fence {
// 保护fence状态的自旋锁指针
spinlock_t *lock;
// 驱动特定操作的回调函数集
const struct dma_fence_ops *ops;
// 三合一union:根据生命周期阶段使用不同字段
union {
struct list_head cb_list;// 未完成时:回调函数链表
ktime_t timestamp; // 已完成时:信号完成的时间戳
struct rcu_head rcu; // 释放时:RCU延迟释放
};
// 执行上下文ID(由dma_fence_context_alloc分配)
u64 context;
u64 seqno; // 上下文内的序列号(用于排序)
unsigned long flags; // 原子标志位(见下方enum)
struct kref refcount; // 引用计数
int error; // 错误码(<0表示异常完成)
};
关键设计解析
1. 自旋锁指针 (lock)
- 指针而非嵌入锁:允许多个fence共享同一把锁(如同一驱动的多个fence)
- 使用
spinlock_t *而非mutex:支持在中断上下文操作fence - 保护对
cb_list、flags等的并发访问
2. 操作接口 (ops)
指向struct dma_fence_ops,定义驱动特定的行为:
struct dma_fence_ops {
const char *(*get_driver_name)(struct dma_fence *fence);
const char *(*get_timeline_name)(struct dma_fence *fence);
bool (*enable_signaling)(struct dma_fence *fence); // 启用硬件中断
bool (*signaled)(struct dma_fence *fence); // 快速查询状态
signed long (*wait)(struct dma_fence *fence, bool intr, signed long timeout);
void (*release)(struct dma_fence *fence); // 释放时的清理
void (*set_deadline)(struct dma_fence *fence, ktime_t deadline);
};
3. 三合一union的设计
这是fence结构中最巧妙的内存优化:
[未完成阶段] 使用 cb_list
↓
struct list_head cb_list // 回调链表(16字节)
[signal时] list_replace保存cb_list → 替换为timestamp
↓
ktime_t timestamp // 完成时间(8字节)
[释放时] 替换为rcu
↓
struct rcu_head rcu // RCU回收(16字节)
生命周期保证:
cb_list仅在未signal时有效timestamp仅在signal后、释放前有效rcu仅在引用计数归零后使用- 通过
flags中的位标志判断当前阶段
4. 上下文与序列号 (context & seqno)
// 驱动初始化时分配context(全局唯一)
u64 ctx = dma_fence_context_alloc(1);
// 每次创建fence递增seqno
dma_fence_init(fence, &my_ops, &my_lock, ctx, seqno++);
排序语义:
- 同一
context内的fence通过seqno严格排序 seqno较大的fence必然晚于seqno较小的fence完成- 不同
context的fence之间无序
5. 原子标志位 (flags)
enum dma_fence_flag_bits {
DMA_FENCE_FLAG_SEQNO64_BIT, // 使用完整64位seqno比较(否则仅用低32位)
DMA_FENCE_FLAG_SIGNALED_BIT, // 已完成标志(原子设置)
DMA_FENCE_FLAG_TIMESTAMP_BIT, // 时间戳已记录
DMA_FENCE_FLAG_ENABLE_SIGNAL_BIT, // 已调用enable_signaling
DMA_FENCE_FLAG_USER_BITS, // 用户可扩展位起始
};
原子操作:
// 设置完成标志(防止重复signal)
if (test_and_set_bit(DMA_FENCE_FLAG_SIGNALED_BIT, &fence->flags))
return -EINVAL; // 已经signal过了
// 检查是否完成(无需加锁)
if (test_bit(DMA_FENCE_FLAG_SIGNALED_BIT, &fence->flags))
return 0; // 快速路径
总结:设计哲学
dma_fence结构体现了Linux内核的三大设计原则:
- 内存高效:union设计将不同阶段的数据复用同一内存空间
- 并发安全:原子位操作 + 自旋锁 + RCU机制保证多核安全
- 灵活可扩展:
ops回调允许驱动定制行为,USER_BITS支持私有扩展
整个结构仅占72字节(64位系统),却承载了复杂的同步语义,是内核"零成本抽象"理念的典范。
3.2 fence的生命周期
阶段一:初始化
驱动调用 dma_fence_init(fence, ops, lock, context, seqno)
↓
设置基本信息,引用计数为1
↓
返回给调用者,fence处于未完成状态
阶段二:传递与等待
fence被放入dma_resv或作为sync_file导出
↓
消费者获取fence并增加引用计数
↓
消费者选择同步方式:
- dma_fence_wait() → 阻塞等待
- dma_fence_add_callback() → 注册回调
阶段三:完成信号
硬件完成操作,触发中断
↓
驱动在中断处理或工作队列中调用 dma_fence_signal()
↓
设置 SIGNALED_BIT,记录时间戳
↓
遍历 cb_list,依次调用所有注册的回调
↓
唤醒所有阻塞等待的进程
阶段四:释放
所有持有者调用 dma_fence_put() 减少引用计数
↓
引用计数归零
↓
通过RCU机制延迟释放内存(保证并发读安全)
3.3 dma-fence的同步方式
dma-fence提供了两种异步操作的同步方式:
- 阻塞等待 (dma_fence_wait):进程主动休眠,等待fence信号
- 注册回调 (dma_fence_add_callback):注册函数,fence完成时异步调用
3.3.1 阻塞等待机制
dma_fence_wait()提供同步等待语义,核心流程:
1. 注册临时回调 dma_fence_default_wait_cb
- 回调函数保存当前进程的task_struct指针
2. 设置进程状态
- TASK_INTERRUPTIBLE(可被信号中断)
- TASK_UNINTERRUPTIBLE(不可中断)
3. 调用 schedule_timeout(timeout)
- 进程让出CPU,进入休眠
- 被移出运行队列
4. fence完成时回调被触发
- wake_up_state(task, TASK_NORMAL)
- 进程恢复TASK_RUNNING,重新进入运行队列
5. 清理回调节点,返回剩余超时时间
这种实现避免了忙等待,进程休眠期间不占用CPU资源。
3.3.2 异步回调机制
dma_fence_add_callback()提供非阻塞的异步通知:
注册阶段
int dma_fence_add_callback(fence, cb, func) {
// 快速检查:已完成则返回错误,不调用回调
if (test_bit(SIGNALED_BIT, &fence->flags))
return -ENOENT;
// 启用硬件中断(如果需要)
__dma_fence_enable_signaling(fence);
// 将回调加入链表
cb->func = func;
list_add_tail(&cb->node, &fence->cb_list);
return 0; // 立即返回,不阻塞
}
触发阶段
int dma_fence_signal_timestamp_locked(fence, timestamp) {
// 设置完成标志(原子操作,避免重复signal)
test_and_set_bit(SIGNALED_BIT, &fence->flags);
// 保存回调链表(即将被timestamp替换)
list_replace(&fence->cb_list, &cb_list);
// 记录时间戳
fence->timestamp = timestamp;
// 依次调用所有回调(可能在中断上下文)
list_for_each_entry_safe(cur, tmp, &cb_list, node) {
cur->func(fence, cur);
}
}
3.5 使能信号机制
许多硬件支持"硬件到硬件"同步(hw→hw),无需CPU介入。但当需要"硬件到软件"通知(hw→sw)时,必须显式启用中断:
bool (*enable_signaling)(struct dma_fence *fence) {
// 驱动特定实现
// 1. 启用硬件中断
// 2. 或插入命令到GPU队列触发回调
// 3. 返回true表示成功,false表示fence已完成
}
这种设计的优势:
- 性能优化:纯硬件同步无需CPU开销
- 按需启用:只有调用wait或add_callback时才启用中断
- 避免风暴:减少不必要的中断
首次调用dma_fence_wait()或dma_fence_add_callback()时,框架会自动调用enable_signaling()。
4. 与dma-buf的集成
dma_fence和dma-buf是同时提出的,两者协作完成了共享buffer的同步访问。
dma-buf通过struct dma_resv 管理关联的fence集合:
struct dma_buf {
struct dma_resv *resv; // 指向reservation object
// ...其他字段
};
struct dma_resv {
struct ww_mutex lock; // 等待-受伤互斥锁
struct dma_resv_list *fences; // fence数组(RCU保护)
};
5. 总结
dma-fence作为Linux内核异步操作同步的基础设施,其价值在于:
- 抽象统一:为GPU、显示、视频、相机等不同硬件提供统一的同步语义
- 性能优化:支持硬件直接同步,最大化并行度,减少CPU开销
- 灵活强大:支持阻塞/非阻塞、显式/隐式等多种模式
本文从异步访问的需求引出dma_fence的设计,需要重点理解的几个概念有:
- 异构系统的执行方式;
- 显式同步与隐式同步的概念;
- 阻塞与非阻塞式的同步方式;
下面的章节我们先来理解阻塞与非阻塞同步方式的具体实现和差异;然后在来看显式与隐式的使用方法。
979

被折叠的 条评论
为什么被折叠?



