sched-ext discussion
[PATCH sched_ext/for-7.3] selftests/sched_ext: Make numa idle validation race-free
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 中」。问题是:
scx_bpf_pick_idle_cpu_node()返回的 CPU 可能在 BPF 程序校验它之前,被另一次 idle 到 idle re-pick 重新放回 idle cpumask。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_idle是curr && (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。