0/2 已展开

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.c
    • tools/testing/selftests/sched_ext/ddsp_vtimelocal_fail.bpf.c
  • 代码统计:纯测试端 BPF 代码调整,未触碰内核核心路径

  • Message-ID20260811141358.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 ID 0xcafef00d 做 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。

危害有两层:

  1. 假绿灯:CI 显示 PASS,但实际没验证任何东西;一旦内核真的回归(比如误允许 vtime 派发到 local DSQ),测试也不会红。
  2. 不可复现:同样的代码在空闲机器上跑一次是真测试,在满载机器上跑一次是空跑,表现随负载抖动,正是典型 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);

三个要点:

  1. 控制流由"条件执行 + 双出口"改成"兜底 + 单出口",insert_vtime()pick_idle_cpu() 的结果彻底解耦。

  2. prev_cpu 由内核在调用 select_cpu() 时传入,一定是合法 CPU,因此兜底安全;测试不需要它空闲。

  3. 返回值语义收敛为 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 假绿灯。