sched-ext discussion
[PATCHSET sched_ext/for-7.2-fixes] sched_ext: Fix core scheduling
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.c、kernel/sched/ext/ext.c、kernel/sched/sched.h,以及tools/sched_ext/include/scx/下三个 autogen 头 - 代码统计:265 insertions(+),91 deletions(-)
- Message-ID:
20260807210221.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 概览
| # | 路径 | 作用 |
|---|---|---|
| 1 | sched/core | 引入统一 restart: 标签,repair need_sync/fi_before 的 latch 与 clock validity |
| 2 | sched/core | 新增 rq->core_pick_in_flight,__sched_core_flip() 等 in-flight selection 结束 |
| 3 | sched_ext | 移除 SCX_RQ_BAL_KEEP,改用 verdict 枚举;同步清理 tools autogen |
| 4 | sched_ext | dispatch kfunc 改用 scx_locked_rq(),避免跨 sibling 错锁 |
| 5 | sched_ext | 新增 lock_drop_seq 计数器,调度点上报锁释放 |
| 6 | sched_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:风险与注意点
- patch 2 spin-wait 的活锁风险:Peter 明确点名
__sched_core_flip()的 while 自旋会“饿死 / 永远转下去”。在 sibling 高频交错选择时,flip 可能长时间抢不到锁。需要持续关注。 SCX_DSQ_LOCAL语义变化:patch 4 之后,dispatch kfunc 看到的 local DSQ 是“op 所在 rq”的,不一定是 current CPU 的;旧 BPF scheduler 假设this_rq() == locked_rq的要重审。- stable 回退点扩张:patch 1/2 的 Fixes 从
4c95380701f5回退到539f65125d20(v5.14 起),backport 矩阵变大,需要在 v5.14+ 多个 stable tree 上重新跑回归。 - local DSQ 暂存任务的 reenqueue 不及时:8 月 19 日 Peter 提出 CPU0 把任务放进 local DSQ 后、还没写回 global DSQ 就被 sibling 选中的 FIFO 任务抢走,Tejun 承认存在并标记为 “less severe than oops”,需要后续跟进解决。
- Balance callback 上下文绑定:dispatch 跨 rq 时必须直接
run_deferred(),不能queue_balance_callback(),后者依赖发起者 CPU 上下文。 - 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
=========================================================
版本变化
| Patch | v1 → 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 支持重新修通。