0/2 已展开

LLM 分析

sched_ext selftest:让 NUMA idle 校验摆脱 race

系列概况

  • 标题: [PATCH sched_ext/for-7.3] selftests/sched_ext: Make numa idle validation race-free
  • 作者: Cheng-Yang Chou yphbchou0911@gmail.com
  • 版本: 单封 PATCH,目标分支 sched_ext/for-7.3
  • 规模: 1 个文件,+21 / -7
  • 修改文件: tools/testing/selftests/sched_ext/numa.bpf.c
  • 代码统计: 新增 validate_local_idle_state(),删除旧 is_cpu_idle()、旧 scx_bpf_error("CPU %d should be marked as busy", cpu) 一行
  • Message-ID: 20260811153524.6616-1-yphbchou0911@gmail.com(patch)+ acc89c8d6db9e09ca91a4cf8c0534a5b@kernel.org(Tejun Heo 回复)
  • 完整性: 包含 diff --stat、commit body、Signed-off-by,回信表示已合入 sched_ext/for-7.3

补丁目的

本补丁是 tools/testing/selftests/sched_ext/numa.bpf.c 的 race-fix。在 NUMA 拓扑下,BPF sched_ext 调度器的 numa_select_cpu 会优先用 scx_bpf_pick_idle_cpu_node() 选一个目标节点内的 idle CPU;旧代码对返回值再做断言「该 CPU 不能出现在该节点的 idle cpumask 中」。问题是:

  1. scx_bpf_pick_idle_cpu_node() 返回的 CPU 可能在 BPF 程序校验它之前,被另一次 idle 到 idle re-pick 重新放回 idle cpumask。
  2. scx_bpf_pick_any_cpu_node() fallback 完全不占用该 CPU,也无所谓「是否仍处于 idle」。

旧断言因此存在窗口期 —— 即便调度器行为正确,也会偶发报错,不是真正的不变式。

补丁借用了 allowed_cpus 那次修复(commit 12da4723b679)的思路:改成本地 CPU 上不变式的校验:本 CPU 跑在非 idle 调度上下文中时,它不应该出现在自身节点的 idle cpumask 中。这个断言只考察本地、不需要远程原子观测,因此天然 race-free。

旧流程的问题

numa_select_cpu(task)
  |
  +-- cpu = scx_bpf_pick_idle_cpu_node(node)
  |      // After pick, the CPU may still be in idle_cpumask:
  |      //   - another idle task re-picks idle -> idle (re-pick)
  |      //   - pick_any fallback does not claim it at all
  |
  +-- is_cpu_idle(cpu, node) == true ?!?   <-- sees idle_cpumask still set
  |
  +-- scx_bpf_error("CPU %d should be marked as busy", cpu)   false positive

新流程

numa_select_cpu(task)
  |
  +-- cpu = scx_bpf_pick_idle_cpu_node(node)
  |      (or pick_any_cpu_node() fallback)
  |
  +-- validate_local_idle_state()         <-- new function
  |     - take curr on local CPU (RCU read lock)
  |     - curr_is_idle = curr && (curr->flags & PF_IDLE)
  |     - cpu_is_idle  = idle_cpumask contains local CPU?
  |     - if (!curr_is_idle && cpu_is_idle)
  |           scx_bpf_error("running CPU %d should be marked as busy", cpu)
  |
  +-- keep node-membership check (stable, kept)
  +-- if task_cpu not in allowed cpus / wrong node: error

validate_local_idle_state() 的语义是:当前 CPU 上跑的 BPF ops 本身在非 idle task context 里,那么该 CPU 就不应该再被上层标记为空闲。被观测的是执行 BPF 这件事,逻辑上是稳定的。

关键实现

static void validate_local_idle_state(void)
{
    struct task_struct *curr;
    s32 cpu = bpf_get_smp_processor_id();
    int node = __COMPAT_scx_bpf_cpu_node(cpu);
    bool cpu_is_idle, curr_is_idle;

    bpf_rcu_read_lock();
    curr = scx_bpf_cpu_curr(cpu);
    curr_is_idle = curr && (curr->flags & PF_IDLE);
    bpf_rcu_read_unlock();

    cpu_is_idle = bpf_cpumask_test_cpu(cpu, idle_cpumask);

    if (!curr_is_idle && cpu_is_idle)
        scx_bpf_error("running CPU %d should be marked as busy", cpu);
}

要点:

  • scx_bpf_cpu_curr(cpu) 拿当前任务,并借助 bpf_rcu_read_lock 保护。
  • curr_is_idlecurr && (curr->flags & PF_IDLE):只有 curr 存在且自身是 idle task,才算 idle。
  • 只在「既不是 idle task context同时又仍被登记为 idle」时报警 —— 这是真正的不变式违例。
  • 把函数挂在 numa_select_cpu 入口处,紧跟着 node 计算与 cpu 选择。

线程中的第二封邮件是 Tejun Heo 的 apply 回复:「Applied to sched_ext/for-7.3.」表示合入。

关键流程图

                 +-----------------------------+
                 | BPF: numa_select_cpu(task)  |
                 +-------------+---------------+
                               |
                               v
                 +-----------------------------+
                 |  cpu = pick_idle_cpu_node() |
                 |   (or pick_any_cpu_node())  |
                 +-------------+---------------+
                               |
                               v
       +------------------------------------------+
       | validate_local_idle_state()             |
       |   curr   = scx_bpf_cpu_curr(my_cpu)     |
       |   idle1  = curr && (curr->flags&PF_IDLE)|
       |   idle2  = cpumask[my_cpu] in idle_mask |
       |   if (!idle1 && idle2) -> scx_bpf_error |
       +--------------------+---------------------+
                            |
                            v
                  +---------------------+
                  | continue dispatch   |
                  +---------------------+

类比

把「CPU 是不是 idle」想象成电影院入口的「空座位标牌」:

  • 旧断言 = 「你刚坐进去时,门口电子牌不应再显示这排有空座位」。问题是门口电子牌是公共信息,其他观众(其他 idle task)随时可以重写它,你坐下来到抬头看牌这段时间里,牌子早就被人翻回「空」了。
  • 新断言 = 「你自己正坐在非空闲任务的位置上,所以和你同行的本地检票口(本 CPU 的 curr)不应该在外屏写你是闲的。」 这是关于你自己 + 本地窗口的不变式,其他人怎么改外屏都跟这条规则相容。

Highlight:风险与注意点

  • race 已消除,但语义收紧:原来检查的是「被选中的远端 CPU 在 idle cpumask 中」,新检查只看「当前自己」。如果未来有人写出别的 sched_ext ops 在 idle cpumask 与远端 CPU 之间产生类似不一致,这里发现不了 —— 后续可能需要类似的 __pick_idle_cpu 系列补丁继续跟进。
  • bpf_rcu_read_lock/unlock 范围:仅保护 scx_bpf_cpu_curr,没有包到 bpf_cpumask_test_cpu。当前实现依赖 idle cpumask 自身在 BPF 侧的不变式;后续若 BPF helper 的 sleepable / RCU 行为有调整需要重新评估。
  • scx_bpf_pick_any_cpu_node() 不 claim CPU:补丁明确承认这一点,因此未对 fallback 路径再加 busy 断言,这是有意设计,不是遗漏。
  • 验证点:CI 跑 selftests/sched_ext/numa.bpf.c 时若旧版偶发错误 CPU N should be marked as busy,升级到此版本应能消除;如仍出现,需要再确认是否有更新的 idle cpumask 假设被改。
  • signed-off-by 与目标树:补丁用 for-7.3 命名空间,意味着只在 7.3 周期内 cherry-pick,需要的人记得追一下 upstream 是否已经 cherry-pick 回去。

与其他相关 patch 系列的关联

补丁 commit message 显式引用了 12da4723b679 ("selftests/sched_ext: Make allowed_cpus idle validation race-free"),做法完全一致 —— 只是把同样的不变式搬到 NUMA 场景。可以理解成同一作者修同一类 race 的第二次迭代:先固化允许位(allowed_cpus)方向的不变式,再固化 idle 方向的不变式。

一句话总结

把 NUMA idle 校验从「远端 CPU 是否还躺在 idle cpumask」改为「本地执行上下文与本地 idle cpumask 的不变式」,与 allowed_cpus 修复同源思路,Tejun Heo 已合入 sched_ext/for-7.3