0/20 已展开

LLM 分析

sched_ext:修复 Core Scheduling 路径下的并发损坏

系列概况

  • 标题:[PATCHSET sched_ext/for-7.2-fixes] sched_ext: Fix core scheduling
  • 作者:Tejun Heo tj@kernel.org
  • 版本:v1;patch 3/6 重新发布为 v2
  • 规模:6 个补丁,基于 sched_ext/for-7.2-fixes (d4a00d61a5c2)
  • 修改文件kernel/sched/core.ckernel/sched/ext/ext.ckernel/sched/sched.h,以及 tools/sched_ext/include/scx/ 下三个 autogen 头
  • 代码统计:265 insertions(+),91 deletions(-)
  • Message-ID20260807210221.232543-1-tj@kernel.org
  • 完整性:完整闭环(patch + 评审 + 应用),patch 1/2 的 Fixes tag 由 4c95380701f5 回退为 539f65125d20,stable target 由 v6.19+ 改为 v5.14+,整体已合入 sched_ext/for-7.2-fixes

补丁目的

sched_ext 的 core scheduling 支持自从 4c95380701f5("sched/ext: Fold balance_scx() into pick_task_scx()")之后一直是坏的:那次 fold 把 dispatch 嵌进 ->pick_task(),而 dispatch 会丢 rq lock。core-wide selection 本来是“共用一把锁、一次性挑完”的语义;丢锁瞬间同核 sibling 的另一个 selection 可以插进来篡改 rq->core_pick、forceidle 计数、core_cookie,导致两类崩溃——读到清空的 core_pick 触发 NULL deref,或者“保留 prev”决策误用旧 flag 留下已 dequeue 的任务继续运行、把机器楔死。

主修复(patch 6)让 core-sched pick 在 dispatch 真的丢过锁时返回 RETRY_TASK,使一次 selection 只提交在“连续持锁”期间做出的 picks。

旧流程的问题

  • pick_task_scx() 内调用 balance_one(),里面会从远端 DSQ 拉任务并丢/拿 rq lock。
  • 同时同核上的 sibling 在另一个 sibling 选择的丢锁窗口里完成自己的 selection:core_pick 互相覆写,forceidle accounting 错乱。
  • SCX_RQ_BAL_KEEP 这个 rq-level flag 隐含“dispatch 与 pick 一一对应”——core-sched 把这个假设破坏了,pick 可能消费到过时的 flag。

新流程

  • 引入 lock_drop_seq 计数器(patch 5),dispatch 每次丢锁时自增。
  • core-sched pick(patch 1、2)能识别“lock-dropping pick 返回 RETRY_TASK”,整体回退到统一 restart: 标签,只提交连续持锁期间的 picks。
  • 把“保留 prev”决策从 rq flag 改成 dispatch 返回的 verdict 枚举(patch 3),让判断与产生它的 dispatch 一起走。
  • dispatch 路径上的 kfunc 用 scx_locked_rq() 取代 this_rq()(patch 4),因为 dispatch 现在可能在另一个 CPU 上为 sibling rq 执行。
  • patch 6 的 dispatch_core_pick() 检测到 lock_drop_seq 改变即返回 SCX_DSP_RETRY,在 do_pick_task_scx() 里转成 RETRY_TASK 上交给上层调度循环。

Patch 概览

#路径作用
1sched/core引入统一 restart: 标签,repair need_sync/fi_before 的 latch 与 clock validity
2sched/core新增 rq->core_pick_in_flight__sched_core_flip() 等 in-flight selection 结束
3sched_ext移除 SCX_RQ_BAL_KEEP,改用 verdict 枚举;同步清理 tools autogen
4sched_extdispatch kfunc 改用 scx_locked_rq(),避免跨 sibling 错锁
5sched_ext新增 lock_drop_seq 计数器,调度点上报锁释放
6sched_ext新增 dispatch_core_pick():丢锁即 SCX_DSP_RETRY

关键实现

patch 1 的核心是把原本 per-loop 的 restart_single / restart_multi 合到顶部 restart:,并在 pick 返回 RETRY_TASK 时清 core_clock_updated,必要时 update_rq_clock(rq)

restart:
        need_sync |= !!rq->core->core_cookie;
        ...
        if (unlikely(next == RETRY_TASK)) {
                /* rq lock may have been dropped, clocks invalidated */
                core_clock_updated = false;
                if (!(rq->clock_update_flags & RQCF_UPDATED))
                        update_rq_clock(rq);
                goto restart;
        }

patch 6 的核心:dispatch_core_pick() 采样前后 lock_drop_seq,丢锁就 SCX_DSP_RETRY;同 rq 已经有 in-flight dispatch 时返回 SCX_DSP_NONE,避免抢它需要的锁:

seq = rq->scx.lock_drop_seq;
rq_unpin_lock(rq, rf);
verdict = balance_one(rq, prev);
...
rq_repin_lock(rq, rf);
if (rq->scx.lock_drop_seq != seq)
        return SCX_DSP_RETRY;

patch 4 的关键:dispatch 路径上的 kfunc 必须分辨“当前 ops 上下文锁住的 rq”和“当前 CPU 的 rq”。core-sched 下二者经常不同:

locked_rq = scx_locked_rq();
if (locked_rq) {
        if (locked_rq != src_rq)
                switch_rq_lock(locked_rq, src_rq);
        p_rq = src_rq;
} else {
        p_rq = this_rq();
        raw_spin_rq_lock(p_rq);
}

patch 2 的关键:在 leader 的 rq 上加 core_pick_in_flight 计数,__sched_core_flip() 看到非零就解锁自旋重试;sched_core_cpu_deactivate() 必须 move 而非 copy,否则旧 leader 后代自己的 leader 会留下偏移的计数。

patch 3 引入:

enum scx_dsp_verdict {
        SCX_DSP_NONE,   /* nothing to run */
        SCX_DSP_LOCAL,  /* local DSQ has tasks */
        SCX_DSP_PREV,   /* keep running @prev */
};

并删掉 SCX_RQ_BAL_KEEP,让决策与 dispatch 在调用栈里一起返回。

类比

core-sched selection 像一次“家庭账本会议”:主持人拿着共享账本,一锤定音谁看电视谁洗碗。fold 之后变成主持人翻账到一半去给孩子找遥控器——这当口另一位家长冲进来把“频道=体育”划掉改成“动画”,第一份账单直接被覆盖。patch 1+6 干的就是:账本被翻开过几次都记下来(lock_drop_seq),中途撒手就把整个流程走回开头重新来,并且只承认从头到尾没松手的决定。patch 2 的 core_pick_in_flight 则像门口的“请勿打扰”挂牌——有 selection 在进行就别在中间换锁。

Highlight:风险与注意点

  1. patch 2 spin-wait 的活锁风险:Peter 明确点名 __sched_core_flip() 的 while 自旋会“饿死 / 永远转下去”。在 sibling 高频交错选择时,flip 可能长时间抢不到锁。需要持续关注。
  2. SCX_DSQ_LOCAL 语义变化:patch 4 之后,dispatch kfunc 看到的 local DSQ 是“op 所在 rq”的,不一定是 current CPU 的;旧 BPF scheduler 假设 this_rq() == locked_rq 的要重审。
  3. stable 回退点扩张:patch 1/2 的 Fixes 从 4c95380701f5 回退到 539f65125d20(v5.14 起),backport 矩阵变大,需要在 v5.14+ 多个 stable tree 上重新跑回归。
  4. local DSQ 暂存任务的 reenqueue 不及时:8 月 19 日 Peter 提出 CPU0 把任务放进 local DSQ 后、还没写回 global DSQ 就被 sibling 选中的 FIFO 任务抢走,Tejun 承认存在并标记为 “less severe than oops”,需要后续跟进解决。
  5. Balance callback 上下文绑定:dispatch 跨 rq 时必须直接 run_deferred(),不能 queue_balance_callback(),后者依赖发起者 CPU 上下文。
  6. tools autogen 清理易漏:patch 3 v2 才补齐 tools/sched_ext/include/scx/{enum_defs.autogen.h, enums.autogen.bpf.h, enums.autogen.h},ABI 移除要完整检查用户态工具。
PATCHSET:  sched_ext: Fix core scheduling            Tejun Heo
============================================================
patch sequence                      files        +/-   focus
------------------------------------------------------------
0001 sched/core  restart: label        core.c   +19/-8  restart whole pick
0002 sched/core  core_pick_in_flight   core.c    +22/-0  flip waits
                                          sched.h   +1/-0
0003 sched_ext   SCX_DSP_* verdict     ext.c   +73/-52  kill rq-level flag
                                          sched.h   +0/-1
                                          tools/*   +0/-3
0004 sched_ext   scx_locked_rq()       ext.c   +35/-23  kfuncs use op rq
0005 sched_ext   lock_drop_seq         ext.c   +39/-2   release counter
                                          sched.h   +3/-0
0006 sched_ext   dispatch_core_pick    ext.c   +78/-8   retry on drop
------------------------------------------------------------
total ext.c: 265 insertions(+), 91 deletions(-)
core-wide pick on CPU0 / CPU1 (fold since 4c95380701f5)
--------------------------------------------------------
lock -- continuous under core sched -- lock      <-- desired
        \                          /
         X drop during dispatch   X              <-- actual

       sibling selection can interleave here:

CPU0  [pick 1]----drop---->[pick 2] core_pick = P2
CPU1          [pick A]----drop--->[pick B] core_pick = PB
   result:  CPU0 commits uncookied pick, CPU1 keeps stale prev
                                                --> oops / hang
lock_drop_seq accounting (patch 5)
=========================================================
   t0  seq=0
       balance_one() start
          unlink_dsq_and_lock_src_rq()  --> scx_rq_lock_drop()  seq=1
          ...
          scx_rq_unlock_irq(rq)         --> scx_rq_lock_drop()  seq=2
          raw_spin_rq_lock(rq)                              seq=2
       balance_one() end
   t1  rq->scx.lock_drop_seq != seq --> SCX_DSP_RETRY
       -> RETRY_TASK -> restart: with fresh state
=========================================================

版本变化

Patchv1 → v2 关键改动
3/6同步从 tools/sched_ext/include/scx/{enum_defs.autogen.h, enums.autogen.bpf.h, enums.autogen.h} 移除 SCX_RQ_BAL_KEEP 导出,采纳 Andrea 建议

整体(非版本号意义上)的修订:

  • patch 1/2 的 Fixes:4c95380701f5 回退到 539f65125d20,stable target 由 v6.19+ 改为 v5.14+(Peter 评论指出核心问题可追溯到 core-sched 首次引入);
  • patch 1/2 加上了 Acked-by: Peter Zijlstra (Intel)
  • 整套已经由 Tejun push 到 sched_ext/for-7.2-fixes

一句话总结

fold balance_scx()pick_task_scx() 后,sched_ext 的 dispatch 会从 core-sched selection 中途丢 rq 锁,让兄弟核的选择互相覆写 state 并秒崩;这一组补丁通过给 lock-drop 计数、把 decision 跟 dispatch 一起返回、让 core-sched pick 在丢锁时整段重试,把 core scheduling 支持重新修通。