第七章:GPU Scheduler 分析:7.6 调度循环与流控 — sched_main 核心流程
1. 引言:一切在此串联
前面五篇分别介绍了设计目标(7.1)、scheduler 实例(7.2)、entity(7.3)、job(7.4)和双 fence(7.5)。本篇将它们串联成一个运转中的系统——分析sched_main.c中的调度循环如何选取 entity、弹出 job、检查 credit、调用run_job()、以及完成后的free_job()流程。
核心问题:从用户态 push 一个 job 到它被硬件执行完毕,scheduler 内部经历了哪些步骤?
2. 总体架构:两个 Work Item
GPU scheduler不使用独立内核线程,而是基于 workqueue 的两个 work item 驱动:
submit_wq (ordered workqueue) ├── work_run_job → 选 entity → pop job → credit 检查 → run_job() └── work_free_job → 取 finished job → free_job() → 归还 credit为什么用 ordered workqueue?
- 保证同一 scheduler 的 run_job 和 free_job 不会并发执行
- 省去大量锁竞争——选取 entity、pop job、run_job 调用天然串行
- 但不同 scheduler(不同 ring)之间完全并行
触发时机:
| 事件 | 触发的 work |
|---|---|
drm_sched_entity_push_job() | work_run_job(通过drm_sched_wakeup()) |
drm_sched_job_done() | work_free_job(完成后需要释放) |
work_free_job执行完毕 | work_run_job(credit 归还后可能可以跑新 job) |
3. 调度循环主函数:drm_sched_run_job_work()
drm_sched_run_job_work()——整个调度器的心脏:
staticvoiddrm_sched_run_job_work(structwork_struct*w){structdrm_gpu_scheduler*sched=container_of(w,...);structdrm_sched_entity*entity;structdrm_sched_job*sched_job;structdma_fence*fence;// ① 选取 entityentity=drm_sched_select_entity(sched);if(!entity)return;// 无就绪 entity,退出// ② 弹出 job(含依赖检查)sched_job=drm_sched_entity_pop_job(entity);if(!sched_job){complete_all(&entity->entity_idle);drm_sched_run_job_queue(sched);// 重新排队自己return;}// ③ 记账 credit + 加入 pending_listatomic_add(sched_job->credits,&sched->credit_count);drm_sched_job_begin(sched_job);// ④ 调用驱动回调:发射到硬件fence=sched->ops->run_job(sched_job);complete_all(&entity->entity_idle);// ⑤ 信号 scheduled fence + 注册完成回调drm_sched_fence_scheduled(s_fence,fence);if(!IS_ERR_OR_NULL(fence)){dma_fence_add_callback(fence,&sched_job->cb,drm_sched_job_done_cb);dma_fence_put(fence);}else{drm_sched_job_done(sched_job,IS_ERR(fence)?PTR_ERR(fence):0);}// ⑥ 唤醒等待者 + 重新排队自己(处理下一个 job)wake_up(&sched->job_scheduled);drm_sched_run_job_queue(sched);}注意最后的drm_sched_run_job_queue(sched)——这让 work_run_job自我重入(重新入队 submit_wq),形成一个"循环"。只要有就绪的 entity 和足够的 credit,它就会持续处理 job。
4. 步骤详解
4.1 选取 Entity:drm_sched_select_entity()
staticstructdrm_sched_entity*drm_sched_select_entity(structdrm_gpu_scheduler*sched){for(i=DRM_SCHED_PRIORITY_KERNEL;i<sched->num_rqs;i++){entity=(policy==FIFO)?drm_sched_rq_select_entity_fifo(sched,sched->sched_rq[i]):drm_sched_rq_select_entity_rr(sched,sched->sched_rq[i]);if(entity)break;// 找到就不再看低优先级 rq}returnIS_ERR(entity)?NULL:entity;}关键规则:
- 严格优先级:从 KERNEL → HIGH → NORMAL → LOW 遍历,高优先级 rq 有就绪 entity 就选中,不看低优先级
- 两种策略:由模块参数
drm_sched_policy控制
FIFO 策略
staticstructdrm_sched_entity*drm_sched_rq_select_entity_fifo(structdrm_gpu_scheduler*sched,structdrm_sched_rq*rq){for(rb=rb_first_cached(&rq->rb_tree_root);rb;rb=rb_next(rb)){entity=rb_entry(rb,structdrm_sched_entity,rb_tree_node);if(drm_sched_entity_is_ready(entity)){if(!drm_sched_can_queue(sched,entity))returnERR_PTR(-ENOSPC);// credit 不够reinit_completion(&entity->entity_idle);break;}}returnrb?entity:NULL;}- 使用
rb_tree_root(红黑树),按oldest_job_waiting时间排序 - 全局最老 job 优先:哪个 entity 最早入队的 job 等得最久,就先调度它
- 保证公平性——不会因为某个 entity 提交量大就饿死其他 entity
Round Robin 策略
staticstructdrm_sched_entity*drm_sched_rq_select_entity_rr(structdrm_gpu_scheduler*sched,structdrm_sched_rq*rq){entity=rq->current_entity;// 从 current_entity 之后开始找list_for_each_entry_continue(entity,&rq->entities,list){if(drm_sched_entity_is_ready(entity))gotofound;}// 绕回从头找list_for_each_entry(entity,&rq->entities,list){if(drm_sched_entity_is_ready(entity))gotofound;if(entity==rq->current_entity)break;}returnNULL;found:rq->current_entity=entity;// 记录位置,下次从这之后继续returnentity;}- 维护
current_entity指针,每次从上次位置继续 - 每个 entity 轮流获得一次执行机会
Entity "就绪"的定义
staticinlinebooldrm_sched_entity_is_ready(structdrm_sched_entity*entity){if(!spsc_queue_count(&entity->job_queue))returnfalse;// 队列为空if(READ_ONCE(entity->dependency))returnfalse;// 有未解决的依赖returntrue;}4.2 弹出 Job:drm_sched_entity_pop_job()
选中 entity 后,从它的 job_queue 取出队首 job:
structdrm_sched_job*drm_sched_entity_pop_job(structdrm_sched_entity*entity){sched_job=drm_sched_entity_queue_peek(entity);// 检查所有依赖while((entity->dependency=drm_sched_job_dependency(sched_job,entity))){if(drm_sched_entity_add_dependency_cb(entity,sched_job))returnNULL;// 依赖未就绪,注册回调后返回}// guilty 检查if(entity->guilty&&atomic_read(entity->guilty))dma_fence_set_error(&sched_job->s_fence->finished,-ECANCELED);// 更新 last_scheduledrcu_assign_pointer(entity->last_scheduled,&sched_job->s_fence->finished);spsc_queue_pop(&entity->job_queue);sched_job->entity=NULL;// 断开 job→entity 指针returnsched_job;}如果依赖未就绪,drm_sched_entity_add_dependency_cb()会注册 fence 回调,回调触发时重新唤醒 scheduler(通过drm_sched_wakeup())。此时pop_job()返回 NULL,run_job_work 重新入队等待。
4.3 Credit 检查与记账
Credit 检查发生在选取 entity 阶段(drm_sched_can_queue()),而非 pop_job 之后:
staticbooldrm_sched_can_queue(structdrm_gpu_scheduler*sched,structdrm_sched_entity*entity){s_job=drm_sched_entity_queue_peek(entity);// 安全阀:超过 credit_limit 的 job 被截断到 limitif(s_job->credits>sched->credit_limit)s_job->credits=sched->credit_limit;returndrm_sched_available_credits(sched)>=s_job->credits;}staticu32drm_sched_available_credits(structdrm_gpu_scheduler*sched){returnsched->credit_limit-atomic_read(&sched->credit_count);}Credit 生命周期:
credit_count ─────────────────┬────────────────────────────────── pop_job 后 │ atomic_add(job->credits) ↑ 增加 run_job 成功 │ (已计入) hw_fence 信号 │ drm_sched_job_done() │ atomic_sub(job->credits) ↓ 归还 ─────────────────┴──────────────────────────────────关键设计:credit 在run_job()之前就计入(atomic_add在drm_sched_job_begin()之前),确保并发安全。归还发生在drm_sched_job_done()中,此时 work_free_job 被触发,它在最后调用drm_sched_run_job_queue()重新激活 run_job_work——形成流控反馈环路。
4.4 发射到硬件:run_job()
fence=sched->ops->run_job(sched_job);这是驱动的 backend 回调。语义:
- 将 job 的命令写入硬件 ring buffer
- 返回一个
dma_fence*(硬件 fence),当硬件执行完命令时信号 - 返回 NULL:job 同步完成(无需等待)
- 返回 ERR_PTR:job 失败
紧随其后:
drm_sched_fence_scheduled(s_fence,fence);这会设置 parent fence 并信号 scheduled fence(详见 X.5)。
4.5 完成回调注册
if(!IS_ERR_OR_NULL(fence)){r=dma_fence_add_callback(fence,&sched_job->cb,drm_sched_job_done_cb);if(r==-ENOENT)drm_sched_job_done(sched_job,fence->error);// 已信号}- 正常路径:注册
drm_sched_job_done_cb到硬件 fence - 硬件 fence 已经信号(
-ENOENT):直接调用 done - fence 为 NULL/ERR:立即 done
4.6 Pending List 与 TDR 计时
drm_sched_job_begin()将 job 加入pending_list并启动超时定时器:
staticvoiddrm_sched_job_begin(structdrm_sched_job*s_job){spin_lock(&sched->job_list_lock);list_add_tail(&s_job->list,&sched->pending_list);drm_sched_start_timeout(sched);spin_unlock(&sched->job_list_lock);}pending_list 中的 job 按提交顺序排列(FIFO)。TDR 只监视队首 job——如果它超时未完成,则认为 GPU 挂了。
5. 完成与释放:drm_sched_free_job_work()
drm_sched_free_job_work():
staticvoiddrm_sched_free_job_work(structwork_struct*w){structdrm_gpu_scheduler*sched=container_of(w,...);bool have_more;job=drm_sched_get_finished_job(sched,&have_more);if(job){sched->ops->free_job(job);// 驱动释放 job 资源if(have_more)drm_sched_run_free_queue(sched);// 还有完成的 job}drm_sched_run_job_queue(sched);// credit 归还了,尝试跑新 job}drm_sched_get_finished_job()
staticstructdrm_sched_job*drm_sched_get_finished_job(structdrm_gpu_scheduler*sched,bool*have_more){spin_lock(&sched->job_list_lock);job=list_first_entry_or_null(&sched->pending_list,...);if(job&&dma_fence_is_signaled(&job->s_fence->finished)){list_del_init(&job->list);cancel_delayed_work(&sched->work_tdr);// 更新下一个 job 的 timestamp(更准确的 TDR 起点)next=list_first_entry_or_null(...);if(next){next->s_fence->scheduled.timestamp=dma_fence_timestamp(&job->s_fence->finished);*have_more=dma_fence_is_signaled(&next->s_fence->finished);drm_sched_start_timeout(sched);// 为下一个 job 重启 TDR}}else{job=NULL;}spin_unlock(&sched->job_list_lock);returnjob;}关键点:
- 只取 pending_list 的队首job(FIFO 顺序释放)
- 取出后取消 TDR 定时器,为下一个 job 重新启动
- 更新下一个 job 的 scheduled timestamp——让 TDR 超时从前一个 job 完成时开始计算
6. 完整数据流时序
用户态: ioctl submit → drm_sched_job_init() → 分配 s_fence → drm_sched_job_arm() → 初始化 fence,选择 scheduler → dma_resv_add_fence(bo, &s_fence->finished) → drm_sched_entity_push_job() → spsc_queue_push() → job 入队 entity → drm_sched_wakeup() → queue_work(work_run_job) work_run_job (submit_wq): drm_sched_select_entity() → 按优先级遍历 rq[] → FIFO: rb_tree 最老优先 / RR: 轮转 → drm_sched_can_queue(): credit 检查 → 返回就绪 entity drm_sched_entity_pop_job() → 遍历 dependencies, 检查每个 fence → 依赖未就绪? 注册 cb, return NULL → 等待 → 所有依赖就绪? pop job, 断开 entity 指针 atomic_add(credits) → credit 记账 drm_sched_job_begin() → 加入 pending_list, 启动 TDR sched->ops->run_job() → 驱动发射到硬件 ring drm_sched_fence_scheduled() → ★ scheduled fence 信号 dma_fence_add_callback(hw_fence, done_cb) → 注册完成回调 drm_sched_run_job_queue() → 自我重入,处理下一个 job 硬件完成: hw_fence 信号 → drm_sched_job_done_cb() → atomic_sub(credits) → 归还 credit → drm_sched_fence_finished() → ★ finished fence 信号 → drm_sched_run_free_queue() → queue_work(work_free_job) work_free_job (submit_wq): drm_sched_get_finished_job() → 从 pending_list 取队首完成的 job sched->ops->free_job() → 驱动释放 job 资源 drm_sched_run_job_queue() → credit 归还后尝试新 job7. 流控机制深度分析
7.1 为什么需要流控
硬件 ring buffer 容量有限。如果 scheduler 无限制地调用run_job(),ring buffer 溢出会导致:
- 驱动必须在
run_job()中阻塞(违反 workqueue 语义) - 或者丢弃 job(不可接受)
Credit 机制让 scheduler在软件层面背压:当已发射但未完成的 job 总 credit 达到上限时,停止发射新 job。
7.2 流控反馈环路
┌──────────────────────────────────────────────────────────────┐ │ │ │ run_job_work: │ │ can_queue()? ──No──→ 返回 ERR_PTR(-ENOSPC) │ │ │ select_entity 返回 NULL │ │ Yes run_job_work 退出(不自我重入) │ │ │ │ │ ▼ │ │ run_job() │ │ │ │ │ ├──→ credit_count += job.credits │ │ │ │ │ ▼ (等待硬件完成) │ │ job_done() │ │ │ │ │ ├──→ credit_count -= job.credits │ │ │ │ │ └──→ run_free_queue() ──→ free_job_work │ │ │ │ │ └──→ run_job_queue() ────┘ │ (重新激活 run_job) └──────────────────────────────────────────────────────────────┘当 credit 不够时,drm_sched_select_entity()返回 NULL(因为drm_sched_can_queue()返回ERR_PTR(-ENOSPC),被转为 NULL)。此时 run_job_work不会自我重入——调度器进入"等待"状态。
当硬件完成某个 job,drm_sched_job_done()归还 credit 并触发work_free_job。work_free_job最后调用drm_sched_run_job_queue()重新激活work_run_job——此时 credit 已释放,新 job 可以执行。
7.3 安全阀:超大 Job
if(s_job->credits>sched->credit_limit){dev_WARN(sched->dev,"Jobs may not exceed the credit limit, truncate.\n");s_job->credits=sched->credit_limit;}如果某个 job 的 credit 超过 scheduler 的 credit_limit,强制截断到 limit。这保证了前向进展——否则这个 job 永远无法被调度(因为 available_credits 永远不够)。
8. 暂停与恢复
Scheduler 支持两种暂停机制:
8.1 pause_submit(轻量暂停)
staticvoiddrm_sched_run_job_queue(structdrm_gpu_scheduler*sched){if(!READ_ONCE(sched->pause_submit))queue_work(sched->submit_wq,&sched->work_run_job);}当pause_submit = true时,drm_sched_run_job_queue()和drm_sched_run_free_queue()都不会入队新 work。效果:
- 不会调度新 job
- 不会释放已完成的 job
- 已在硬件上执行的 job 不受影响
8.2 drm_sched_wqueue_stop/start(完全停止)
voiddrm_sched_wqueue_stop(structdrm_gpu_scheduler*sched){WRITE_ONCE(sched->pause_submit,true);cancel_work_sync(&sched->work_run_job);// 等待正在执行的 work 完成cancel_work_sync(&sched->work_free_job);}voiddrm_sched_wqueue_start(structdrm_gpu_scheduler*sched){WRITE_ONCE(sched->pause_submit,false);queue_work(sched->submit_wq,&sched->work_run_job);queue_work(sched->submit_wq,&sched->work_free_job);}用于 TDR 恢复流程:
drm_sched_stop()→drm_sched_wqueue_stop()→ 等待 work 完成- 驱动执行 GPU reset
drm_sched_start()→ 重新提交 pending jobs →drm_sched_wqueue_start()
8.3 TDR 超时控制
unsignedlongdrm_sched_suspend_timeout(structdrm_gpu_scheduler*sched){// 将 timeout 延长到 MAX_SCHEDULE_TIMEOUT(等效暂停 TDR)mod_delayed_work(sched->timeout_wq,&sched->work_tdr,MAX_SCHEDULE_TIMEOUT);returnremaining;}voiddrm_sched_resume_timeout(structdrm_gpu_scheduler*sched,unsignedlongremaining){if(list_empty(&sched->pending_list))cancel_delayed_work(&sched->work_tdr);elsemod_delayed_work(sched->timeout_wq,&sched->work_tdr,remaining);}用于长时间操作(如 VM page table 更新)期间暂停 TDR 监控,避免误触超时。
9. 自我驱动循环的完整触发链
Scheduler 没有一个永远运行的线程。它是事件驱动的:
| 触发事件 | 触发链 | 结果 |
|---|---|---|
| 用户 push job | push_job()→drm_sched_wakeup()→queue_work(run_job) | 开始调度 |
| run_job_work 完成一个 job | 末尾drm_sched_run_job_queue() | 继续调度下一个 |
| 依赖 fence 信号 | cb →drm_sched_wakeup()→queue_work(run_job) | 被阻塞的 entity 解除 |
| 硬件 fence 信号 | job_done_cb()→drm_sched_run_free_queue() | 释放 + credit 归还 |
| free_job_work 完成 | 末尾drm_sched_run_job_queue() | credit 归还后继续调度 |
停止条件:当所有 entity 都空或都被依赖阻塞,且 credit 耗尽时,run_job_work 不再自我入队——调度器安静等待外部事件。
10. 与其他组件的交互图
用户态 ioctl │ ▼ drm_sched_entity_push_job() │ ▼ drm_sched_wakeup() ┌──────────────────────────────────┐ │ work_run_job │ │ │ │ select_entity() │ │ │ │ │ ▼ │ │ pop_job() │ │ │ │ │ ├── dep fence unsignaled ───┼──→ 注册 cb, 退出 │ │ │ (fence 信号后重新激活) │ ▼ │ │ can_queue()? │ │ │ │ │ ├── No (credit 不够) ───────┼──→ 退出 │ │ │ (free_job 归还后重新激活) │ ▼ │ │ run_job() ──→ 硬件 ring │ │ │ │ │ ▼ │ │ scheduled fence 信号 │ │ │ │ │ └──→ 自我重入 ──────────────┘ │ │ work_free_job │ │ │ │ get_finished_job() │ │ │ │ │ ▼ │ │ free_job() │ │ │ │ │ └──→ run_job_queue() ───────┘ │ └──────────────────────────────────┘ submit_wq11. 小结
| 维度 | 要点 |
|---|---|
| 执行模型 | 基于 ordered workqueue 的两个 work item,事件驱动 |
| 选取策略 | 严格优先级 + 同优先级内 FIFO(红黑树)或 Round Robin |
| 流控 | credit_count/credit_limit,满时不调度,释放后自动恢复 |
| 自驱动 | run_job_work 末尾自我入队 + free_job_work 末尾激活 run_job |
| 停止条件 | 无就绪 entity、credit 满、或 pause_submit=true |
| 暂停/恢复 | pause_submit(轻量)或 wqueue_stop/start(完全,含 sync) |
| pending_list | 已发射未完成的 job 队列,TDR 监控对象 |
| 串行保证 | ordered workqueue 保证同 scheduler 的 run/free 不并发 |
下一篇将深入 TDR 机制——当 pending_list 队首 job 超时未完成时,drm_sched_job_timedout()如何触发驱动的 GPU 恢复流程。
