sched-ext discussion
[PATCH] selftests/sched_ext: Fix flaky ddsp failure tests on busy systems
LLM 分析
selftests/sched_ext:修复 ddsp 失败测试在繁忙系统上的 flaky 问题
系列概况
-
标题:[PATCH] selftests/sched_ext: Fix flaky ddsp failure tests on busy systems
-
作者:Michal Blaszczyk
<michalblk@google.com> -
版本:单封 PATCH,无 vN 编号,无 cover letter
-
规模:2 files changed, 15 insertions(+), 18 deletions(-)
-
修改文件:
tools/testing/selftests/sched_ext/ddsp_bogus_dsq_fail.bpf.ctools/testing/selftests/sched_ext/ddsp_vtimelocal_fail.bpf.c
-
代码统计:纯测试端 BPF 代码调整,未触碰内核核心路径
-
Message-ID:
20260811141358.2767765-1-michalblk@google.com -
完整性:完整。包含 Subject、commit body、
Fixes: a5db7817af78 ("sched_ext: Add selftests")、Signed-off-by,并有 maintainer Tejun Heo 的 Applied 回复(sched_ext/for-7.3)
补丁目的
sched_ext selftest 里有两个"失败注入"测试:
ddsp_bogus_dsq_fail:故意往一个不存在的 DSQ ID0xcafef00d做 vtime dispatch,验证内核能优雅地回退 / 报错。ddsp_vtimelocal_fail:故意对 builtin 的SCX_DSQ_LOCAL做 vtime dispatch,验证内核拒绝这种非法组合。
这两个测试的核心断言,都建立在"非法 dispatch 真的被调用过"这个前提上。但原代码把这次调用包在 if (cpu >= 0) 里——只有 scx_bpf_pick_idle_cpu() 成功挑到空闲 CPU 才执行。
于是在繁忙机器(CI 主机、共享构建机)上,pick_idle_cpu() 返回负错误码,测试直接 return prev_cpu 走人,核心断言一次都没跑,却仍然报"通过"。本补丁就是要消灭这种自欺欺人的 flaky。
旧流程的问题
select_cpu() 的控制流把"资源可用性"和"测试是否执行"耦合在了一起:
- 成功路径(cpu ≥ 0):执行非法 dispatch →
return cpu→ 内核错误路径被触发 → 断言有效。 - 失败路径(cpu < 0):
return prev_cpu→ 非法 dispatch 从未发生 → 测试退化为 no-op。
危害有两层:
- 假绿灯:CI 显示 PASS,但实际没验证任何东西;一旦内核真的回归(比如误允许 vtime 派发到 local DSQ),测试也不会红。
- 不可复现:同样的代码在空闲机器上跑一次是真测试,在满载机器上跑一次是空跑,表现随负载抖动,正是典型 flaky。
新流程
补丁把"挑 CPU"降级为尽力而为,把"执行非法 dispatch"提升为无条件:
- 先
pick_idle_cpu();失败就把cpu兜底成prev_cpu(task 上次运行的 CPU,永远有效)。 - 然后无条件调用
scx_bpf_dsq_insert_vtime()。 - 最后统一
return cpu,保证返回值始终是合法 CPU。
关键洞察:这两个测试根本不关心目标 CPU 是否空闲,只关心 dispatch 动作有没有被内核拦截。因此空闲性完全没必要当 guard。
OLD (flaky) NEW (deterministic)
----------- -------------------
cpu = pick_idle_cpu() cpu = pick_idle_cpu()
| |
+----+----+ +----+----+
| | | |
cpu>=0 cpu<0 cpu>=0 cpu<0
| | | |
v v | cpu = prev_cpu
insert_ return | |
vtime() prev_cpu +----+----+
| | <-- ASSERT SKIPPED |
v v (silent PASS) v
return (test no-op) insert_vtime() <-- ALWAYS
cpu |
v
return cpu
Effect on a loaded CI box:
------------------------------------------------------------
load | old behaviour | new behaviour
-----------+--------------------------+---------------------
idle host | dispatch tested [OK] | dispatch tested [OK]
busy host | dispatch skipped [??] | dispatch tested [OK]
关键实现
两个文件的改动同构,可以抽象成同一段伪 C:
s32 BPF_STRUCT_OPS(..._select_cpu, struct task_struct *p,
s32 prev_cpu, u64 wake_flags)
{
s32 cpu = scx_bpf_pick_idle_cpu(p->cpus_ptr, 0);
/* NEW: 失败时兜底,解除对 idle CPU 的依赖 */
if (cpu < 0)
cpu = prev_cpu;
/*
* OLD:
* if (cpu >= 0) {
* scx_bpf_dsq_insert_vtime(...);
* return cpu;
* }
* return prev_cpu; <-- 断言在此被跳过
*
* NEW: 无条件执行,这才是测试的目的
*/
scx_bpf_dsq_insert_vtime(p, DSQ_TARGET, SCX_SLICE_DFL,
p->scx.dsq_vtime, 0);
return cpu;
}
各文件的 DSQ_TARGET 不同:
/* ddsp_bogus_dsq_fail.bpf.c —— 不存在的 DSQ ID */
scx_bpf_dsq_insert_vtime(p, 0xcafef00d, SCX_SLICE_DFL,
p->scx.dsq_vtime, 0);
/* ddsp_vtimelocal_fail.bpf.c —— builtin local DSQ,vtime 派发非法 */
scx_bpf_dsq_insert_vtime(p, SCX_DSQ_LOCAL, SCX_SLICE_DFL,
p->scx.dsq_vtime, 0);
三个要点:
-
控制流由"条件执行 + 双出口"改成"兜底 + 单出口",
insert_vtime()与pick_idle_cpu()的结果彻底解耦。 -
prev_cpu由内核在调用select_cpu()时传入,一定是合法 CPU,因此兜底安全;测试不需要它空闲。 -
返回值语义收敛为
return cpu一条路径,删掉了 18 行、加回 15 行,净减少代码,可读性也更好。
类比
保险丝检测台:产线上要确认"短路时保险丝确实会熔断"。旧写法像是"如果货架上恰好有一支全新的测试探针,就通电测一次;没有就跳过,直接盖章合格"。忙的时候探针总被别人借走,于是一整批保险丝都是"未测试却合格"。新写法是"探针有没有都得通电"——哪怕用一支旧探针(prev_cpu),也必须把电送进去,让保险丝有机会真的烧断。
消防演练:旧版是"如果今天消防员有空就拉一次警报",忙季警报从不响,演练记录却照样打勾。新版是"警报每次都必须拉响",消防员在不在场是另一回事——重点是验证警铃本身没坏。
Highlight:风险与注意点
-
最容易误解的点:这不是"让调度更聪明",而是"让测试不再偷懒"。补丁完全在 selftest 的 BPF 侧,
scx_bpf_pick_idle_cpu()和内核调度路径一行未改,不存在生产行为回归风险。 -
返回值语义被 maintainer 纠正:commit message 原文写
scx_bpf_pick_idle_cpu()"returns -1",Tejun 合并时改成"returns a negative error code"。任何< 0都是失败,代码里的if (cpu < 0)判断是对的,只是描述不够精确。后续引用这段历史时应以修正后的措辞为准。 -
Fixes 指向测试引入之初:
Fixes: a5db7817af78 ("sched_ext: Add selftests")说明这个"条件 guard 吞掉断言"的设计缺陷自测试诞生日就存在,意味着此前所有在繁忙机器上的绿灯都可能是假的——评估历史 CI 结果时需打折扣。 -
通用教训:测试用例中凡是"取资源失败就 return"的写法,都要问一句——这个资源是断言的前提,还是只是顺手拿的?若不是前提,就不该当 guard。
-
后续观察点:改动后
select_cpu()在满载时会把任务返回到prev_cpu并同时做非法 dispatch,需确认内核对该组合的错误处理路径(回退 global DSQ / ops_error)在两种 CPU 来源下表现一致;若将来这两个测试再度 flaky,应先怀疑内核侧错误处理而非测试逻辑。 -
无 backport 说明:补丁只标了
Fixes,未见Cc: stable;实际落到sched_ext/for-7.3,是否回移到旧的 selftest 分支未在本线程讨论。
版本变化
本线程只有单一版本的 PATCH,没有 v1→v2 迭代。唯一的"变化"发生在合并阶段:
submitted (v1) applied to sched_ext/for-7.3
-------------- ----------------------------
code: if (cpu < 0) cpu = prev_cpu; code: unchanged
desc: "...returns -1" --> desc: "...returns a negative
error code"
reviewer action: none requested maintainer edit on commit msg only
一句话总结
用一行 if (cpu < 0) cpu = prev_cpu; 把非法 dispatch 从"有空闲 CPU 才测"改成"无条件必测",消除了 sched_ext 两个失败注入 selftest 在繁忙系统上悄悄空跑的 flaky 假绿灯。