sched-ext discussion
[PATCHSET v4 sched_ext/for-7.3] sched: Make proxy execution compatible with sched_ext
LLM 分析
sched_ext 与 proxy-exec 兼容:BPF 调度的代理执行
系列概况
- 标题:[PATCHSET v4 sched_ext/for-7.3] sched: Make proxy execution compatible with sched_ext
- 作者:Andrea Righi arighi@nvidia.com
- 版本:v4(10 patches),基于 John Stultz 早期工作
- 规模:10 个 patch,跨 core、ext、selftests、qmap
- 修改文件:kernel/sched/core.c、kernel/sched/ext/{ext.c,ext.h,internal.h}、kernel/sched/sched.h、init/Kconfig、tools/sched_ext/{scx_qmap.bpf.c,scx_qmap.c,include/scx/{common,compat}.bpf.h}、tools/testing/ched_ext/{enq_blocked.bpf.c,enq_blocked.c,.gitignore,Makefile,config}
- 统计:cover 信未给汇总;逐 patch 行数:01/10 +6‑4;02/10 +27‑8;03/10 +36‑17;04/10 +31‑4;05/10 +29/0;06/10 体量较大;07/10 +96‑2;08/10 新增完整 selftest;09/10 +20‑2;10/10 −2
- Message-ID:20260710083913.30573-1-arighi@nvidia.com(cover),2–11 为 10 子 patch,12–22 为 11 封回复
- 完整性:v4 完整;回复集中在 patch 01/02/06/07,patch 03/04/05/08/09/10 暂未单独评论
补丁目的
proxy-exec 让被 mutex 阻塞的高优先级 donor 把调度上下文"借"给锁 owner,使 owner 在保持高优先级票权的同时推进临界区。本系列拿掉 CONFIG_SCHED_PROXY_EXEC 与 CONFIG_SCHED_CLASS_EXT 的 Kconfig 互斥,并引入 SCX_OPS_ENQ_BLOCKED 与 scx_bpf_task_proxy_cpu/cid(),让 BPF 调度器显式认领 blocked donor,决定是否、何时、何地派发。发行版可以用同一份 binary 同时启用两条特性链路。
旧流程的问题
- Kconfig 强制
SCHED_PROXY_EXEC depends on !SCHED_CLASS_EXT,两个特性无法共存。 set_next_task_scx()会在 blocked donor 上调用ops.running(),随后put_prev_task_scx()又调用ops.stopping(),发出不配对的 running/stopping 事件。find_proxy_task()在持有 mutex wait_lock + blocked task lock 时调用proxy_resched_idle(),里面的 sched_class 回调若再回头拿同一把锁会自死锁。consume_remote_task()在切换 rq 锁的窗口里不复检task_can_run_on_remote_rq(),proxy-exec 启用后stress-ng --pipeherd会触发 sleeping-while-atomic + lockdep 污染。rq->curr同时承担"调度上下文"和"执行任务"两个角色,proxy-exec 拆分后 sched_ext 的 DSQ/vtime/who-is-running 账目错位。
新流程
blocked donor (D) mutex owner (O) contender (T)
| blocks on mutex held by O | runs on CPU
v |
BPF enqueue(D, SCX_ENQ_BLOCKED) -> DSQ on owner cid/CPU |
| |
find_proxy_task() walks chain -> migrate D's sched ctx |
| |
set_next_task_scx(D) marks SCX_TASK_IS_RUNNING |
| |
put_prev_set_next_task -> O executes with D's slice/vtime |
| |
scx_bpf_task_proxy_cpu(D) -> hint = task_cpu(O) (kfunc) |
| |
owner releases mutex -> D wakes -> normal ops.enqueue() v
口径:BPF 端通过 SCX_OPS_ENQ_BLOCKED 显式认领 blocked donor;donor 永远是"调度上下文",owner 永远是"执行上下文",所有 sched_ext 记账走 rq->donor,rq->curr 仅描述"在 CPU 上跑代码的人"。
Patch 概览
- 01/10 core.c:把
proxy_resched_idle()移出 guard 作用域,避免持锁调 sched_class 回调 - 02/10 ext.c:引入
SCX_TASK_IS_RUNNING,让ops.running/stopping()在真实 running 转换时才配对 - 03/10 ext.c:把
rq->curr账单全面切换到rq->donor,并扩展scx_dump_state() - 04/10 ext.c:修
consume_remote_task()的 TOCTOU race,lock handoff 后重新校验并走 GDSQ fallback - 05/10 ext.c:允许 blocked donor 正常迁移,但若它仍是 rq 上的 active donor 则推迟
- 06/10 ext.c:引入
SCX_OPS_ENQ_BLOCKED与SCX_ENQ_BLOCKED,准入完全交给 BPF,新增scx_allow_proxy_exec()、sched_proxy_block_task()等 - 07/10 ext.c:新增
scx_bpf_task_proxy_cpu()/scx_bpf_task_proxy_cid()kfunc 及__COMPAT_*wrapper - 08/10 selftests:新增
enq_blocked(BPF + 用户态 + 内核模块)三任务优先级反转测试 - 09/10 scx_qmap:增加
-B选项,启用后把 blocked donor 放进其 cid 的 local DSQ 并复用剩余 slice - 10/10 init/Kconfig:去掉
SCHED_PROXY_EXEC的!SCHED_CLASS_EXT依赖
关键实现
- 状态拆分:donor 提供 sched_class/priority/vtime/slice;owner 提供执行上下文。
update_curr_scx()、DSQ 派发、tick 决策、scx_dump_state()都改走rq->donor。 - 标志位传递:
SCX_OPS_ENQ_BLOCKED是 ops 标志,决定"是否让 BPF 接管 blocked donor 准入";SCX_ENQ_BLOCKED是 enqueue 时间 flag,标记"这次 enqueue 来的就是 blocked donor"。 - 链路稳定:donor 迁移时 source rq 仍通过
task_rq(p)->donor引用它,迁移前必须先proxy_resched_idle;读侧用rcu_access_pointer(rq->donor)防 KCSAN data-race。 - 入口策略:donor 准入前 BPF 可调用
scx_bpf_task_proxy_cpu()询问 owner CPU;返回值仅作 hint,调用结束后 owner 可能已变化。 - 调度切换:scheduler ownership change 时若新调度器未设
SCX_OPS_ENQ_BLOCKED,通过sched_proxy_block_task()把 donor 完整 dequeue(处理sched_delayed),避免延续旧调度器的 proxy 行为。
/* patch 02关键片段:把 IS_RUNNING 与 callback 解耦 */
if ((p->scx.flags & SCX_TASK_QUEUED) && !task_is_blocked(p)) {
if (SCX_HAS_OP(sch, running))
SCX_CALL_OP_TASK(sch, running, rq, p);
p->scx.flags |= SCX_TASK_IS_RUNNING;
}
/* patch 06:把 donor 准入委托给 BPF */
if (task_is_blocked(p)) {
WARN_ON_ONCE(!(sch->ops.flags & SCX_OPS_ENQ_BLOCKED));
scx_do_enqueue_task(rq, p, 0, -1);
goto switch_class;
}
类比
把 proxy-exec 想象成餐厅叫号。donor 是已经拿号、点了菜的高优先级 VIP 客人,但他在等朋友(owner)从洗手间回来。proxy-exec 让 VIP 把"叫号牌"借给朋友,让朋友先坐下来代表 VIP 吃菜——所有看板(DSQ、vtime)都记录 VIP 的状态,而实际吃菜的人(owner)只负责动筷子。sched_ext 让 BPF 充当领班:领班可以选择要不要让 VIP 继续占座(SCX_OPS_ENQ_BLOCKED),还能问"VIP 的朋友打算坐哪一桌"(scx_bpf_task_proxy_cpu()),从而决定要不要提前翻桌。
Highlight:风险与注意点
- 现场 bug:review 时 John Stultz 报告
scx_pair跑 priority-inversion-demo 一段时间后,put_prev_task_scx()触发WARN_ON_ONCE(!(sch->ops.flags & SCX_OPS_ENQ_BLOCKED)),说明 blocked donor 仍进入 put_prev 路径但所属 scheduler 不认领,需要 v5 修补。 - 命名歧义:
SCX_TASK_IS_RUNNING语义是"经历过真实 running 转换",并非"任务正在运行",John 建议改为TASK_BLOCKED_DONOR之类否定语义更易读。 - 优化动机:
scx_bpf_task_proxy_cpu()仅是优化,patch 01 也因此被作者提出"如果优化动机不够强可以整组删掉",要分清"必备"与"可选"。 - Tick 保守:donor 处于无限 slice 时仍保留 tick,禁掉 dynticks;后续要让 core 远端 tick 路径感知
rq->curr != rq->donor才能放开。 - 调度切换:scheduler 切换时若忘记检
sched_delayed就直接 dequeue,会留下伪队列项;sched_proxy_block_task()显式处理了这条路径。 - TOCTOU:
consume_remote_task()修复依赖 lock handoff 之后再次校验,远端 rq 锁追踪要同步清理,否则嵌套ops.dequeue()会试图恢复不存在的 rq。
版本变化
v3 → v4:
- 移除 proxy-migrating 对 migration-disabled / single-CPU donor 的核心限制(Peter Zijlstra、K Prateek Nayak)
- 去掉 sched_ext 的
set_next_task/put_prev_task例外;改用SCX_TASK_IS_RUNNING跟踪真实 running 转换并配对 running/stopping - 弃用
scx_bpf_task_is_blocked(),改为SCX_ENQ_BLOCKEDenqueue flag - 放弃 v2 的
kf_tasks[]与 nestedops.runnable()思路(实测未复现任务 op 重入) - 强化
scx_bpf_task_proxy_cpu()的锁与状态校验,返回 -ENOENT - 远端 rq 捐赠 deactivation 后重新调度;KCSAN data-race 用
rcu_access_pointer()规避 - 拆分出独立的 proxy destination kfunc patch,并加入
__COMPAT_*wrapper
一句话总结
通过一组 sched_ext 内部记账/切换路径的清理,以及 SCX_OPS_ENQ_BLOCKED 标志 + proxy destination kfunc,proxy-exec 与 sched_ext 终于可以同时启用并由 BPF 调度器主导 blocked donor 的入场。