sched-ext discussion
[PATCHSET v5 sched_ext/for-7.3] sched: Make proxy execution compatible with sched_ext
LLM 分析
sched_ext + proxy-exec:把代理执行"打开"给 BPF 调度器
系列概况
- 标题: [PATCHSET v5 sched_ext/for-7.3] sched: Make proxy execution compatible with sched_ext
- 作者: Andrea Righi arighi@nvidia.com
- 版本: v5(基于 John Stultz 早期工作)
- 规模: 10 个 patch + cover letter,外加 6 封 sashiko-bot 自动化审查回复
- 修改文件(按 patch 累计去重): kernel/sched/core.c、kernel/sched/sched.h、kernel/sched/syscalls.c、kernel/sched/ext/{ext.c,ext.h,internal.h,sub.c}、include/linux/sched/ext.h、init/Kconfig、tools/sched_ext/{scx_qmap.bpf.c,scx_qmap.c,include/scx/compat.h,include/scx/enum_defs.autogen.h}、tools/testing/selftests/sched_ext/{Makefile,config,.gitignore,enq_blocked.{c,bpf.c},test_modules/*}
- 代码统计: 约 +260 行内核 + 1100+ 行 selftest + ~30 行 qmap 示例
- Message-ID: 20260713162112.26785-1-arighi@nvidia.com
- 完整性: cover letter、10 封 patch 邮件、6 封 AI 审查回复全部覆盖
补丁目的
proxy-exec 让等待 mutex 的"donor"把它的调度上下文借给锁 owner,让 owner 跑临界区而 donor 仍留在 runqueue 上。
当前 CONFIG_SCHED_PROXY_EXEC=y 与 CONFIG_SCHED_CLASS_EXT=y 在 Kconfig 里硬互斥,发行版想"一个内核、运行时挑功能"做不到。
本系列把互斥拆掉,并引入显式 opt-in:BPF 调度器必须设置新标志 SCX_OPS_ENQ_BLOCKED,sched_ext 才把 blocked donor 交给它处理;否则默认走常规 block 路径,行为与 proxy-exec 不兼容时一致。
旧流程的问题
- Kconfig
SCHED_PROXY_EXEC depends on !SCHED_CLASS_EXT直接拒绝两者共存。 - 即使强行打开,被 mutex 阻塞的 EXT donor 仍以"调度选中任务"的身份出现在
rq->curr上,但 CPU 实际执行的是 owner。BPF 调度器看到的"current"和 core 真正执行的 task 不一致,DSQ、vtime、callback bookkeeping 全部错位。 ops.running()/ops.stopping()会因为 proxy donor 被当成"被选中任务"而错误触发,产生不成对的 stopping 事件。consume_remote_task()在切换 rq 锁之间存在 TOCTOU 窗口,donor 可能被并发迁移到不允许的 rq,触发 sleeping-while-atomic 与 lockdep 损坏。rq->curr在多个 EXT 路径里被当成"被调度选中者"使用,proxy-exec 启用后这条假设不再成立。
新流程
- 默认拒绝:新增
scx_allow_proxy_exec(),EXT 任务默认返回 false,__schedule()走常规 block 路径。 - 调度器切换前清理:
sched_proxy_block_task()强制把 retained donor 从 runqueue 上完整出队(含sched_delayed)。 - 回调配对:新增
SCX_TASK_RUN_TRACKED,仅在真正进入 running/stopping 会话时打标,proxy donor 不会再触发 spurious running。 - 概念切分:把所有 EXT 路径里的
rq->curr替换为rq->donor,把"调度上下文"和"执行上下文"彻底分开。 - TOCTOU 修复:抢源 rq 锁后再做一次
task_can_run_on_remote_rq()检查,失败回退到全局 DSQ,不报 scheduler error。 - BPF opt-in:新增
SCX_OPS_ENQ_BLOCKED、SCX_ENQ_BLOCKED,由 BPF 在enqueue()里决定怎么排 blocked donor;wakeup_preempt_scx()在is_blocked情况下显式 resched。 - Kconfig 解锁:删除
depends on !SCHED_CLASS_EXT,允许两者同时打开。
Patch 概览
| # | 文件 | 作用 |
|---|---|---|
| 01/10 | core.c, sched.h | sched_proxy_block_task() 工具函数 |
| 02/10 | core.c, ext.{c,h}, syscalls.c | 默认拒绝 + 调度类切换时清理 |
| 03/10 | ext.c, sched/ext.h | SCX_TASK_RUN_TRACKED + 回调配对 |
| 04/10 | ext.c | rq->curr → rq->donor 替换 |
| 05/10 | ext.c | consume_remote_task() TOCTOU 修复 |
| 06/10 | ext.c | blocked/active donor 迁移策略 |
| 07/10 | ext.c, internal.h, sub.c | SCX_OPS_ENQ_BLOCKED 委托 BPF |
| 08/10 | selftests | 新增 enq_blocked 测试 |
| 09/10 | scx_qmap.bpf.c, scx_qmap.c | -B 选项 + 抢占式入队 |
| 10/10 | init/Kconfig | 解除 SCHED_PROXY_EXEC 互斥依赖 |
关键实现
(D, O, T) 三元组与 rq->donor
D -----------------> M -------------> O ----------------> T
[donor] blocked on [mutex] owned by [owner] preempted by [task]
\_________________________________^
donates scheduling context
rq->donor = BPF 选中、贡献调度上下文与时间片的任务(永远是 D)。
rq->curr = CPU 真正在执行的指令所属任务(可能是 O)。
所有调度/记账操作必须看 rq->donor,否则 BPF 会把 O 误当成自己 dispatch 的任务。
默认拒绝 + 调度切换清理
bool scx_allow_proxy_exec(const struct task_struct *p) {
struct scx_sched *sch;
if (p->sched_class != &ext_sched_class)
return true;
sch = scx_task_sched(p);
return !sch || (sch->ops.flags & SCX_OPS_ENQ_BLOCKED);
}
void scx_prepare_setscheduler(struct task_struct *p,
const struct sched_class *next_class) {
if (p->sched_class == next_class || next_class != &ext_sched_class)
return;
sched_proxy_block_task(task_rq(p), p);
}
回调配对标志
SCX_TASK_RUN_TRACKED = 1 << 6, /* task entered ops.running()/stopping() session */
/* set_next_task_scx() */
if ((p->scx.flags & SCX_TASK_QUEUED) && !p->is_blocked) {
if (SCX_HAS_OP(sch, running))
SCX_CALL_OP_TASK(sch, running, rq, p);
p->scx.flags |= SCX_TASK_RUN_TRACKED;
}
BPF 委托与 -B 选项
if (enq_blocked) /* SCX_OPS_ENQ_BLOCKED && p->is_blocked */
enq_flags |= SCX_ENQ_BLOCKED;
if (!enq_blocked && !(sch->ops.flags & SCX_OPS_ENQ_EXITING) && ...)
... /* 走原有 fallback 链 */
/* scx_qmap_enqueue() */
if (enq_flags & SCX_ENQ_BLOCKED) {
scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL_ON | scx_bpf_task_cid(p),
slice_ns, enq_flags | SCX_ENQ_PREEMPT);
return;
}
TOCTOU 兜底
if (unlikely(!task_can_run_on_remote_rq(sch, p, this_rq, false))) {
p->scx.dsq = NULL;
p->scx.holding_cpu = -1;
scx_dispatch_enqueue(sch, src_rq,
find_global_dsq(sch, task_cpu(p)), p,
enq_flags | SCX_ENQ_CLEAR_OPSS | SCX_ENQ_GDSQ_FALLBACK);
if (sched_class_above(p->sched_class, src_rq->donor->sched_class))
resched_curr(src_rq);
switch_rq_lock(src_rq, this_rq);
return false;
}
整体状态流
+----------------+ sched_setscheduler/EXT +---------------------+
| EXT 任务 p | -----------------------> | scx_prepare_setscheduler|
| (可能是 donor) | +----------+------------+
+----------------+ |
v
+----------------------------+
| sched_proxy_block_task(p) |
| 完整出队 + 处理 sched_delayed|
+----------------------------+
|
+-----------------+-----------------+
v v
+-------------------------+ +----------------------------+
| sch->flags & ENQ_BLOCKED?| | sch->flags & !ENQ_BLOCKED? |
+-------------------------+ +----------------------------+
| yes | no
v v
+------------------------------+ donor 走常规 mutex 等待路径
| enqueue() 收 SCX_ENQ_BLOCKED |
| BPF 自己决定入队哪个 DSQ |
+------------------------------+
|
v
+------------------------------+
| donor 在 local DSQ 头 + PREEMPT|
| 等待 pick_next_task 选中 |
+------------------------------+
|
v
+------------------------------+
| proxy-exec 沿 blocked_on 走 |
| 找到 owner,把 O 跑在 D 上下文|
+------------------------------+
类比
把 donor 想象成"挂账额度持有者",owner 是"拿着挂账额度去买东西的人"。调度器看的是 donor 的优先级和时间片(挂账额度),但真正下单的是 owner。
sched_ext 像"商场收银系统"——它必须明确知道哪些卡允许挂账(SCX_OPS_ENQ_BLOCKED)、挂账上限多少、记账周期怎么算,否则对账会乱成一团。
如果 BPF 没表态(默认情况),收银系统就把这笔挂账直接拒掉,让 donor 自己走常规支付流程,行为安全、可预测。
Highlight:风险与注意点
- opt-in 是必须的:
SCX_OPS_ENQ_BLOCKED没设置的 BPF 调度器会失去 proxy-exec 收益;发行版若默认开启 sched_ext 而调度器忘记设标志,会出现性能回退。 rq->curr与rq->donor长期可分离:所有写 EXT 路径的人都要警惕"我读的是哪个指针";nohz_full tick 路径目前只能保守地拒绝停 tick,等待后续 core scheduler 把"donor 可分离"推到 NOHZ。- PI de-boost 漏点(sashiko High):
scx_root_enable_workfn()走的是另一条scx_prepare_task_sched_change()钩子,patch 02 的__sched_setscheduler钩子覆盖不到,要看 patch 06/07 是否补齐——若仍有遗漏,donor 会泄漏进 ext_sched_class。 - TOCTOU 与 sleeping-while-atomic:patch 05 修复了 race,但前提是 BPF 调度器不要在 unlocked 窗口里再尝试对同一个 donor 做迁移;stress-ng --pipeherd 0 是当前已知能撞到的负载。
-B策略激进:scx_qmap 把 blocked donor 放 local DSQ 头并强制 PREEMPT,注释明确说"刻意不公平"——切勿把它当成生产策略照搬。- 测试覆盖:enq_blocked 用 priority inversion + nice +19/-20/0 三档测同 CPU / 跨 CPU 拓扑,mutex hold/wait 平均值只是 informational;断言集中在
nr_blocked_enqueues计数上,sashiko 指出 same-CPU 分支的 strict-affinity 检查条件可能有 bug。 - PREEMPT_RT 仍未解禁:Kconfig 里
depends on !PREEMPT_RT仍然存在,RT 场景下整套 proxy-exec 仍未启用。
版本变化(v5 → 现在)
- 把"保留 donor 停用"与"sched_ext 默认拒绝"拆成两个 preparatory patch,让 scheduler-core 部分可单独路由。
- 移除 John Stultz 的 proxy destination query kfunc 与 mutex lock-scope 预备修改。
- 用
p->is_blocked替代task_is_blocked(),修复 scx_pair 测试里的 WARN(John Stultz)。 SCX_TASK_IS_RUNNING重命名为SCX_TASK_RUN_TRACKED,注释明确它跟踪的是ops.running()/stopping()会话而非物理rq->curr。- proxy-migrated blocked donor 留在 owner 的 rq 直到 wakeup,不再让 BPF-directed migration 把它拉回 wake_cpu。
一句话总结
把 CONFIG_SCHED_PROXY_EXEC 与 CONFIG_SCHED_CLASS_EXT 的 Kconfig 互斥解开,让 BPF 调度器通过新增的 SCX_OPS_ENQ_BLOCKED 显式接管 mutex-blocked donor,同时修齐 rq->curr/rq->donor 视图分离带来的 callback 配对、TOCTOU 与迁移策略问题。