0/22 已展开

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_EXECCONFIG_SCHED_CLASS_EXT 的 Kconfig 互斥,并引入 SCX_OPS_ENQ_BLOCKEDscx_bpf_task_proxy_cpu/cid(),让 BPF 调度器显式认领 blocked donor,决定是否、何时、何地派发。发行版可以用同一份 binary 同时启用两条特性链路。

旧流程的问题

  1. Kconfig 强制 SCHED_PROXY_EXEC depends on !SCHED_CLASS_EXT,两个特性无法共存。
  2. set_next_task_scx() 会在 blocked donor 上调用 ops.running(),随后 put_prev_task_scx() 又调用 ops.stopping(),发出不配对的 running/stopping 事件。
  3. find_proxy_task() 在持有 mutex wait_lock + blocked task lock 时调用 proxy_resched_idle(),里面的 sched_class 回调若再回头拿同一把锁会自死锁。
  4. consume_remote_task() 在切换 rq 锁的窗口里不复检 task_can_run_on_remote_rq(),proxy-exec 启用后 stress-ng --pipeherd 会触发 sleeping-while-atomic + lockdep 污染。
  5. 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->donorrq->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_BLOCKEDSCX_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_BLOCKED enqueue flag
  • 放弃 v2 的 kf_tasks[] 与 nested ops.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 的入场。