0/18 已展开

LLM 分析

sched: Make proxy execution compatible with sched_ext (v6)

系列概况

  • 标题: [PATCHSET v6 sched_ext/for-7.3] sched: Make proxy execution compatible with sched_ext
  • 作者: Andrea Righi (NVIDIA),基于 John Stultz 的早期工作;patch 07 由 John Stultz 署名
  • 版本: v6(前序 v5=2026-07-13、v4=07-10、v3=07-06、v2=07-02、v1=2026-05-06)
  • 规模: 11 个 patch + 4 封 Sashiko 机器人回复 + 2 封人工回复(K Prateek Nayak、Andrea),共 18 封
  • 修改文件: kernel/sched/{core.c,sched.h,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,.c}, tools/testing/selftests/sched_ext/{,test_modules/}
  • 代码统计: patch 01 (9+/9-), 02 (45+), 03 (57+/1-), 04 (27+/8-), 05 (29+/2-), 10 (22+/2-), 11 (-2);新增约 1000+ 行 selftest(enq_blocked.c 908 + .bpf.c 116)
  • Message-ID: 20260715205622.276220-1-arighi@nvidia.com
  • 完整性: 11/11 patch、4 封 Sashiko、2 封人工回复均在输入内;patch 08、09 及 index 18 邮件正文被截断

补丁目的

Proxy execution (proxy-exec) 让阻塞的 mutex waiter D 留在 runqueue 上,把自己的调度上下文(调度类、优先级、时间片)捐给持锁者 O,让 O 用 D 的身份把临界区跑完。现状是 CONFIG_SCHED_PROXY_EXEC 与 CONFIG_SCHED_CLASS_EXT 在 Kconfig 层互斥,发行版无法用一个内核在运行时选功能。本系列拆掉这个互斥,并把 sched_ext 内部所有"谁在运行"统一改为"谁提供调度上下文",再给 BPF 调度器一个显式 opt-in 开关 SCX_OPS_ENQ_BLOCKED。

旧流程的问题

sched_ext 的世界观:BPF dispatch 过的任务,才是这个 CPU 上在跑的任务。

  • kfunc/回调 (ops.running/stopping/tick) 假设 rq->curr 就是 BPF 选出的那个任务
  • DSQ + vtime 维护"谁该跑、什么顺序"
  • proxy-exec 直接打破——内核可能执行一个 BPF 从未 dispatch 过的 mutex owner
  • 结果:ops.running 为"幽灵 donor"触发、ops.stopping 不成对、update_curr_scx 从错误任务扣时间片、抢占/kick_cpu/DSQ/debug 全部看到不一致状态

新流程

核心原则:donor 是调度上下文,owner 仅是执行上下文;donor→owner 是函数调用,不是调度器可见的切换。

        D ------------> M ------------> O <----------- T
     [donor]      [mutex]         [owner]         [rival]
   blocked_on M   held by O     executes on CPU  may preempt
        \______________ donates scheduling context ______________/

   rq->donor = D    <-- sched_ext policy, accounting, callbacks
   rq->curr  = O    <-- CPU code that physically runs

BPF 侧获得显式控制权:

BPF ops.flags has SCX_OPS_ENQ_BLOCKED ?
  +-- no  --> mutex waiter blocks normally, no proxy-exec (safe default)
  +-- yes --> donor handed to BPF via ops.enqueue(p, SCX_ENQ_BLOCKED);
              BPF decides if / where / in what order to dispatch

Patch 概览

#主题作用
01NOHZ CFS bandwidth 跟随 donor独立 bugfix,带 Fixes
02sched_proxy_block_task() helper核心:把 retained donor 真正摘下 rq
03跨调度器切换时阻塞 donor建立"EXT 默认拒绝 donor"安全底线
04SCX_TASK_RUN_TRACKED修 ops.running()/stopping() 配对
05consume_remote_task() TOCTOU修 rq 锁交接窗口资格变化
06curr/donor 引用全面拆分本系列主体改动
07blocked donor 迁移规则何时允许 BPF 正常迁移 donor
08SCX_OPS_ENQ_BLOCKED opt-in把 donor 准入权交给 BPF
09enq_blocked kselftest优先级反转场景
10scx_qmap -B示例调度器的激进 donor 策略
11去 Kconfig 互斥收尾

关键实现

Patch 01:sched_can_stop_tick() 看错任务

static inline bool __need_bw_check(struct task_struct *p)
{
    if (p->sched_class != &fair_sched_class)
        return false;
    return true;
}
...
if (__need_bw_check(rq->donor)) {
    if (cfs_task_bw_constrained(rq->donor))

retained donor 让 donor 与 owner 同时留在 rq 上,nr_running != 1 永远为真把检查短路;而 rq->curr 是 owner,可能根本不是带宽约束任务。修法是查 donor 并去掉 nr_running 限制。带 Fixes: af0c8b2bf67b

Patch 02:sched_proxy_block_task()

要点是"两次判断 sched_delayed":EEVDF DELAY_DEQUEUE 会让已阻塞 fair 任务留在队列且带 sched_delayed,随后的 sched_change 会把它保留并在新调度器下重新入队——必须完成 delayed dequeue 保证完全出队。

Patch 04:幽灵 donor 的回调配对

新增 SCX_TASK_RUN_TRACKED = 1 << 6set_next 在 QUEUED 且 !is_blocked 时触发 ops.running() 并置位;put_prev/dequeue 仅在已置位时调用 ops.stopping() 并清位。标志的置/清与回调是否实现相互独立。

Patch 05:consume_remote_task() TOCTOU

scx_consume_dispatch_q()
  task_can_run_on_remote_rq(p, this_rq)  --> OK   (持 this_rq)
  |
  |  <==== 锁交接窗口: 放 this_rq, 拿 src_rq ====>
  |       此窗口 p 可能 migration-disabled
  |       或作为 donor 在 src_rq 上 active
  v
  拿 src_rq 后重做 task_can_run_on_remote_rq(p, this_rq, false)
  失败 -> 清 DSQ 关联, holding_cpu=-1, 改入 global DSQ

复现:stress-ng --pipeherd 0。后果:迁移活跃上下文让其带错误调度路径的 IRQ/抢占状态恢复,触发 sleeping-while-atomic 与 lockdep 破坏。

Patch 06:curr→donor 系统性替换

覆盖 update_curr_scxrq_owned_post_enq 的抢占判断、dispatch_to_local_dsqsched_class_abovefirst_local_tasktask_tick_scx(参数改名 donor)、run_deferredkick_one_cpu 的 cur_class/set_task_slice,以及 scx_dump_cpu 新增 donor= 行。覆盖信列出 8 种 D/O/T 组合:组合 3、7 因 EXT 低于 FAIR 而不成立;组合 5、6(FAIR donor + EXT owner)对 BPF 完全不可见但 EXT owner 在消耗 donor 预算。

nohz_full 妥协:blocked donor 即使 SCX_SLICE_INF 也保留 tick,因为 remote NOHZ tick 路径假设 rq->curr == rq->donor。代价是 donor 被选中期间有周期性 tick。

Patch 07/08:迁移与准入

v6 迁移判据细化为两条:

if (p->is_blocked) {
    if (task_cpu(p) != p->wake_cpu)
        return false;
    if (rcu_access_pointer(task_rq(p)->donor) == p)
        return false;
}

第一条避免"BPF 按 callback rq 拉回 → proxy-exec 又推到 owner"的乒乓。proxy_set_task_cpu 保留 wake_cpu,所以 task_cpu != wake_cpu 识别出"这是 proxy 迁移过的 donor"。

Patch 08 引入 SCX_TASK_ENQ_WAKEUP (bit 7),解决 p->is_blockedwakeup_preempt 之后才清除的时序问题。校验:SCX_OPS_ENQ_BLOCKED 必须配 ops.enqueue(),否则 scx_error;并且对 blocked donor 优先于 SCX_OPS_ENQ_EXITINGSCX_OPS_ENQ_MIGRATION_DISABLED

Patch 09/10:可观测性

scx_qmap -B 故意激进(fresh slice + 插入 local DSQ 头 + SCX_ENQ_PREEMPT):让 proxy-exec 肉眼可见,不追求公平。kselftest 构造标准优先级反转,mutex 由 test_modules/scx_enq_blocked_test.ko 提供。同 CPU 拓扑开启后 mutex 等待时间平均下降 20.41%、持有时间下降 4.48%;跨 CPU 拓扑分别下降 12.86% 与 12.69%;开启后统计到 60(同 CPU)与 80(跨 CPU)次 blocked donor enqueue,跨 CPU 中有 60 次落在 owner CPU 上——正是"donor 调度上下文被搬到 owner rq"的直接观测。

类比

一家餐厅:D 是持 VIP 卡的客人(队首),O 是唯一拿着冷库钥匙的临时工(队尾),M 是那把钥匙,T 是普通客人。普通做法:VIP 干等、临时工慢慢挪——这就是优先级反转。proxy-exec:VIP 不离开队首,把号牌递给临时工,让他"用 VIP 名义"去开冷库;对排号系统来说队首始终是 VIP(rq->donor),只是干活的人换成临时工(rq->curr)——即覆盖信的"像函数调用,不是调度器可见的切换"。

sched_ext 好比排号外包给第三方 App(BPF 调度器),它的账本上从没有"临时工"这一号,突然看到临时工在后厨忙,统计就乱了。解法两层:(1)对账规则改了——涉及时长/插队的记账一律记在号牌持有者(donor)名下;(2)知情同意——App 必须主动勾选 SCX_OPS_ENQ_BLOCKED,内核才会把 VIP 号牌送进它的 ops.enqueue()。Patch 02 的 delayed dequeue 处理则是把客人请出队伍时必须确认他真的走出了大门,否则换班时保洁会误以为他还在排队又塞回新队列。

Highlight:风险与注意点

1. K Prateek Nayak 指出的真实 bug

sched_proxy_block_task():
    if (task_current_donor(rq, p))
        proxy_resched_idle(rq);    <-- Prateek: 应为 proxy_reset_donor()

后果链:
  proxy_resched_idle() 把 donor 设成 idle
        |
        v
  current 随后调用 sched_yield()
        |
        v
  以 idle 为 donor 调用 idle_sched_class 的 yield 回调 -> crash

v6 仍带此缺陷,Andrea 的回复(index 18)正文被截断,无法确认是否接受修改、是否会有 v7。

2. Sashiko 两条 [High] 告警(未见作者回应)

  • 针对 patch 06:rq->curr → rq->donor 迁移不彻底,可能破坏 capability revocation、抢占检查、BPF kfunc。合理——patch 06 是逐点替换,遗漏一处即语义 bug,而输入中无作者答复。
  • 针对 patch 10:CPU capability 被撤销时 blocked 任务可能无限重入队循环导致 hard lockup。patch 08 明确 SCX_OPS_ENQ_BLOCKED 优先于 SCX_OPS_ENQ_MIGRATION_DISABLED 等 fallback,恰好削弱原有兜底,Sashiko 怀疑方向与代码改动吻合。

Sashiko 是 AI 评审机器人,其结论应作"待验证线索",但两条都指向 v6 薄弱处。

3. 设计妥协

  • nohz_full tick 无法停:donor 被选中即使 slice 无限也保留 tick,对实时/HPC 场景是可感知开销。
  • FAIR donor 对 BPF 不可见(组合 5/6):BPF 时长统计与实际 CPU 占用不符。
  • scx_qmap -B 策略不可照搬:注释明示 "intentionally unfair",生产调度器照抄会形成"抢锁即提权"的可被滥用行为。
  • 两处 WARN_ON_ONCE 是活断言——测试应把它当失败信号。
  • kselftest 时序数据 informational——注释明确说明,别把 −20.41% 当可回归指标。

4. 最容易误解的一点

SCX_OPS_ENQ_BLOCKED 让 donor 进入 ops.enqueue(),但 BPF 收到的仍然是 donor,不是 owner。BPF 一旦 dispatch 这个 donor,实际在 CPU 上跑的是 owner 代码。BPF 调度器作者若按"我 dispatch 谁谁就在跑"的直觉写 ops.running() 逻辑会得到错误模型——patch 04 保证 BPF 不会为这个 donor 收到 ops.running()

版本变化

v6→v5 关键改动(据覆盖信):

  1. 新增 patch 01:让 NOHZ CFS bandwidth 检查跟随 proxy donor 而非物理执行上下文;
  2. 扩大阻塞时机:PI de-boost 或全局调度器激活进入 sched_ext 时也要阻塞 retained donor;
  3. 区分唤醒类型:把普通 wakeup 激活与 retained-donor 激活分开(新增 SCX_TASK_ENQ_WAKEUP)。

patch 07→08 演化:put_prev_task_scx 对 blocked donor 从"直接 scx_dispatch_enqueue() 到 local DSQ"改为"一律 scx_do_enqueue_task() 委托给 BPF";task_can_run_on_remote_rq 判据从单一 active-donor 检查扩展为 wake_cpu + active-donor 两条。

一句话总结

通过"donor 提供调度上下文、curr 仅是执行上下文"的一致性改造,加上 SCX_OPS_ENQ_BLOCKED 让 BPF 显式 opt-in 的准入开关,让 proxy execution 与 sched_ext 首次可同时编译启用;顺带修掉 NOHZ 带宽检查看错任务、ops.running/stopping 不配对、consume_remote_task() 的 rq 锁交接 TOCTOU 三个真实 bug,但 proxy_resched_idle() 应为 proxy_reset_donor() 的崩溃隐患以及两条 [High] 级 AI 告警仍待作者回应。