GPU Scheduler 是 dma-fence 的重要消费者,可以作为一个总要的应用场景来理解。
1. 为什么需要两个 Fence
在第六章(dma-fence)中我们知道,dma_fence 是 GPU 异步操作的完成信号。一个直觉的设计是:每个 job 一个 fence,硬件完成时信号。
但 GPU scheduler 引入了一个中间状态 :job 被调度器选中并发射到硬件(run_job()),与 job 在硬件上真正执行完毕,是两个不同的时间点。
时间线: ──push──────────────run_job()──────────────hw完成──────→
│ │
▼ ▼
scheduled fence finished fence
(已进入硬件队列) (硬件执行完毕)
如果只有一个 fence(finished),那么依赖 job A 的 job B 必须等到 A 在硬件上执行完毕才能被发射。但实际上,一旦 A 已经进入硬件 ring,B 紧随其后进入同一 ring 是完全安全的------硬件 ring 本身保证顺序执行。
双 fence 设计的核心收益 :允许依赖链的流水线化(pipelining)------后续 job 只要等前序 job 进入硬件(scheduled),无需等其执行完毕(finished),从而最大化硬件利用率。
2. drm_sched_fence 结构
c
struct drm_sched_fence {
struct dma_fence scheduled; // 子 fence 1:已调度
struct dma_fence finished; // 子 fence 2:已完成
ktime_t deadline; // deadline hint
struct dma_fence *parent; // run_job() 返回的硬件 fence
struct drm_gpu_scheduler *sched; // 所属 scheduler
spinlock_t lock; // 两个 fence 共享的锁
void *owner; // 调试用:job 所有者
uint64_t drm_client_id; // drm_file 的 client_id
};
关键设计:scheduled 和 finished 是两个独立的 dma_fence ,各自有独立的 fence_ops、独立的信号时机,但共享同一个 spinlock,且使用连续的 fence context(entity->fence_context 和 entity->fence_context + 1)。
3. 生命周期与信号时机
3.1 分配(alloc)
drm_sched_fence_alloc():在 drm_sched_job_init() 中调用。
c
struct drm_sched_fence *drm_sched_fence_alloc(struct drm_sched_entity *entity,
void *owner, u64 drm_client_id)
{
fence = kmem_cache_zalloc(sched_fence_slab, GFP_KERNEL);
fence->owner = owner;
fence->drm_client_id = drm_client_id;
spin_lock_init(&fence->lock);
return fence;
}
此时 fence 只是一块内存,scheduled 和 finished 尚未初始化为有效的 dma_fence。
3.2 初始化(init)
drm_sched_fence_init():在 drm_sched_job_arm() 中调用。
c
void drm_sched_fence_init(struct drm_sched_fence *fence,
struct drm_sched_entity *entity)
{
fence->sched = entity->rq->sched;
seq = atomic_inc_return(&entity->fence_seq);
dma_fence_init(&fence->scheduled,
&drm_sched_fence_ops_scheduled,
&fence->lock,
entity->fence_context, // context N
seq);
dma_fence_init(&fence->finished,
&drm_sched_fence_ops_finished,
&fence->lock,
entity->fence_context + 1, // context N+1
seq); // 同一 seqno
}
两个 fence 使用相邻的 context、相同的 seqno。这确保:
- 同 entity 的 fence 严格递增(fence ordering 保证)
- scheduled 和 finished 属于不同 context(不会被
dma_fence_is_later()误比较)
3.3 Scheduled 信号
drm_sched_fence_scheduled():在 drm_sched_run_job_work() 中,run_job() 调用后立即触发。
c
void drm_sched_fence_scheduled(struct drm_sched_fence *fence,
struct dma_fence *parent)
{
if (!IS_ERR_OR_NULL(parent))
drm_sched_fence_set_parent(fence, parent); // 关联硬件 fence
dma_fence_signal(&fence->scheduled); // 信号!
}
语义:job 的命令已写入硬件 ring,GPU 即将(或正在)执行。所有等待 scheduled fence 的 waiter 被唤醒。
3.4 Finished 信号
drm_sched_fence_finished():在 drm_sched_job_done() 中触发(由 parent fence 的回调间接调用)。
c
void drm_sched_fence_finished(struct drm_sched_fence *fence, int result)
{
if (result)
dma_fence_set_error(&fence->finished, result); // 传播错误码
dma_fence_signal(&fence->finished); // 信号!
}
语义:GPU 已完成该 job 的所有命令执行。数据已写入目标 BO,可以安全读取。
3.5 完整时序图
drm_sched_job_init()
└── drm_sched_fence_alloc() → s_fence 分配(未初始化)
drm_sched_job_arm()
└── drm_sched_fence_init() → scheduled/finished 初始化
fence 可以被外部引用
(写入 dma_resv, 返回给用户态)
drm_sched_run_job_work()
├── run_job() → 返回 hw_fence → parent = hw_fence
└── drm_sched_fence_scheduled() → ★ scheduled fence 信号
hw_fence 信号 (GPU 完成)
→ drm_sched_job_done_cb()
→ drm_sched_job_done()
→ drm_sched_fence_finished() → ★ finished fence 信号
4. Parent Fence:连接软件与硬件
parent 是 run_job() 回调返回的驱动硬件 fence------它代表 GPU 硬件对命令完成的承诺。
关系链:
s_fence->scheduled ←─ scheduler 内部信号(run_job 时)
s_fence->parent ←─ 硬件 fence(GPU 完成时信号)
s_fence->finished ←─ parent 信号后,由回调链触发信号
c
static void drm_sched_fence_set_parent(struct drm_sched_fence *s_fence,
struct dma_fence *fence)
{
smp_store_release(&s_fence->parent, dma_fence_get(fence));
// 如果 finished fence 已经有 deadline hint,传播给 parent
if (test_bit(DRM_SCHED_FENCE_FLAG_HAS_DEADLINE_BIT,
&s_fence->finished.flags))
dma_fence_set_deadline(fence, s_fence->deadline);
}
注意 smp_store_release:这是与 drm_sched_fence_set_deadline_finished() 的配对,防止并发设置 deadline 时看到未初始化的 parent。
5. 流水线优化:scheduled fence 的核心价值
在 X.3(entity)中我们看到 drm_sched_entity_add_dependency_cb() 的流水线优化:
c
s_fence = to_drm_sched_fence(fence);
if (!fence->error && s_fence && s_fence->sched == sched &&
!test_bit(DRM_SCHED_FENCE_DONT_PIPELINE, &fence->flags)) {
// 依赖来自同一 scheduler ------ 降级为等 scheduled fence
fence = dma_fence_get(&s_fence->scheduled);
dma_fence_put(entity->dependency);
entity->dependency = fence;
}
效果:
无流水线 (传统模式):
Job A: ──run──────────────[hw执行]──────finished──→
Job B: ──run──[hw执行]──finished──→
等 A finished 才能 run B 串行,硬件利用率低
有流水线 (scheduled fence):
Job A: ──run──────────────[hw执行]──────finished──→
Job B: ──run──────────────[hw执行]──────finished──→
等 A scheduled 即可 run B 重叠,硬件利用率高
条件限制:
- 同一 scheduler (
s_fence->sched == sched):不同 ring 之间不能假设顺序 - 无错误 (
!fence->error):前序 job 失败则不能流水线 - 未设 DONT_PIPELINE flag:某些场景需要强制完整等待
6. DRM_SCHED_FENCE_DONT_PIPELINE
c
#define DRM_SCHED_FENCE_DONT_PIPELINE DMA_FENCE_FLAG_USER_BITS
设置此 flag 后,即使依赖来自同一 scheduler,也不降级为等 scheduled fence,强制等 finished fence(即硬件完成后才调度后续 job)。
使用场景:
- 页表更新:VM page table update job 必须在硬件上完成后,后续 job 才能使用新映射
- Preemption fence:抢占完成后才能重新提交
- 跨 ring 语义屏障:某些驱动需要显式的执行屏障
7. Deadline 传播
用户态或内核可以通过 dma_fence_set_deadline() 给 finished fence 设置 deadline hint("希望在这个时间点前完成")。
问题:deadline 设置时 parent(硬件 fence)可能还不存在(job 还没被 run)。
解决方案------两阶段传播:
场景 1: deadline 先于 parent 设置
dma_fence_set_deadline(finished, T)
→ 保存到 s_fence->deadline
→ 设置 HAS_DEADLINE_BIT
(稍后) drm_sched_fence_set_parent(s_fence, hw_fence)
→ 检查 HAS_DEADLINE_BIT
→ dma_fence_set_deadline(hw_fence, T) ← 传播!
场景 2: parent 先于 deadline 设置
drm_sched_fence_set_parent(s_fence, hw_fence)
→ smp_store_release(&parent, hw_fence)
(稍后) dma_fence_set_deadline(finished, T)
→ drm_sched_fence_set_deadline_finished()
→ 保存 deadline + 设 bit
→ parent = smp_load_acquire(&s_fence->parent)
→ dma_fence_set_deadline(parent, T) ← 传播!
无论哪种顺序,deadline 最终都会到达硬件 fence,驱动可据此调整 GPU 频率或优先级。smp_store_release / smp_load_acquire 配对保证了无锁的正确性。
8. 引用计数与释放
两个 fence 的释放策略不同:
| Fence | release 回调 | 释放逻辑 |
|---|---|---|
scheduled |
drm_sched_fence_release_scheduled |
释放 parent 引用 + RCU 释放整个 drm_sched_fence 结构 |
finished |
drm_sched_fence_release_finished |
释放 scheduled fence 的引用(dma_fence_put(&scheduled)) |
释放链:
finished 引用归零
→ release_finished()
→ dma_fence_put(&scheduled)
scheduled 引用归零
→ release_scheduled()
→ dma_fence_put(parent) // 释放硬件 fence
→ call_rcu(..., free_rcu) // RCU 延迟释放整个结构体
RCU grace period 后
→ kmem_cache_free(sched_fence_slab, fence)
为什么用 RCU? 因为 to_drm_sched_fence() 可能在无锁路径中被调用(如 entity 的依赖检查),需要保证结构体在 RCU 读临界区内有效。
为什么 scheduled 持有结构体的生命周期? 因为 drm_sched_fence 结构包含两个 fence,整体释放必须等两者都无人引用。设计上让 finished 引用 scheduled,scheduled 引用结构体。finished 归零 → scheduled 归零 → 结构体释放。
9. 与 dma_resv 的联动
这是双 fence 语义在实际使用中的关键环节:
驱动提交流程:
1. drm_sched_job_init() + add_dependency()
2. drm_sched_job_arm() ← s_fence 有效
3. dma_resv_add_fence(bo->resv,
&job->s_fence->finished,
DMA_RESV_USAGE_WRITE) ← 注册到 BO
4. drm_sched_entity_push_job()
后续访问者:
drm_sched_job_add_implicit_dependencies(new_job, bo, true)
→ 从 bo->resv 中取出 s_fence->finished
→ 添加为 new_job 的依赖
→ pop 时检查:同 scheduler? → 降级为等 scheduled fence(流水线)
整个闭环:
- 提交时将 finished fence 注册到 BO 的 reservation object
- 后续 job 通过隐式依赖从 BO 的 resv 中获取这个 fence
- 如果后续 job 在同一 scheduler 上,自动降级为等 scheduled fence(流水线优化)
- 如果在不同 scheduler/设备上,等待 finished fence(硬件完成后才安全)
这就是 scheduler 如何驱动整个 DRM 同步生态------它既是 fence 的生产者(创建 scheduled/finished),也是消费者(检查 job 的 dependencies)。
10. Fence Ops 对比
| 属性 | scheduled fence | finished fence |
|---|---|---|
| fence_ops | drm_sched_fence_ops_scheduled |
drm_sched_fence_ops_finished |
| context | entity->fence_context |
entity->fence_context + 1 |
| seqno | entity->fence_seq (相同) |
entity->fence_seq (相同) |
| 信号时机 | run_job() 后立即 | 硬件完成后 |
| set_deadline | 无 | 有(传播到 parent) |
| release | 释放 parent + RCU 释放结构体 | 释放 scheduled 引用 |
| 用途 | 流水线依赖解除 | 最终完成通知、dma_resv |
11. 小结
| 维度 | 要点 |
|---|---|
| 核心价值 | 双 fence 实现依赖链流水线化,最大化硬件利用率 |
| scheduled | run_job() 时信号,表示"已进入硬件队列" |
| finished | 硬件完成时信号,表示"数据已就绪" |
| parent | 连接 scheduler 软件 fence 与驱动硬件 fence |
| DONT_PIPELINE | 禁止流水线,强制等硬件完成 |
| Deadline | 双向传播,无论设置顺序都能到达硬件 fence |
| 引用计数 | finished → scheduled → 结构体,RCU 延迟释放 |
| dma_resv 联动 | finished 注册到 BO,后续 job 通过隐式依赖自动获取 |
下一篇 7.6 将把前面所有组件串联起来------分析 sched_main.c 中的调度循环如何选取 entity、pop job、检查依赖和 credit、调用 run_job,以及完成后的 free 流程。