sched-ext discussion
[PATCH] sched_ext: Use atomic cpumask_clear_cpu in scx_idle_test_and_clear_cpu()
LLM 分析
sched_ext:原子化 idle_smts 位清零尝试(最终被撤回)
系列概况
- 标题:
[PATCH] sched_ext: Use atomic cpumask_clear_cpu in scx_idle_test_and_clear_cpu() - 作者:Michal Blaszczyk
<michalblk@google.com> - 版本:单封 v1 邮件(无 v2,因为被建议放弃)
- 规模:1 个文件,
kernel/sched/ext/idle.c,1 行删除 / 1 行新增 - 修改文件:
kernel/sched/ext/idle.c - 代码统计:
1 file changed, 1 insertion(+), 1 deletion(-) - Message-ID:
20260901152212.1691696-1-michalblk@google.com - 完整性:thread 完整,包含 patch、Sashiko 自动审查、Andrea Righi 维护者回复、作者本人决定放弃;最终建议放弃。
补丁目的
sched_ext 的 idle 跟踪路径里,idle_smts 是一个 per-node 的共享 cpumask,记录 SMT 兄弟核中"idle"的位。它由多个 CPU 无锁并发地修改。
原代码在 scx_idle_test_and_clear_cpu() 中调用了 __cpumask_clear_cpu():
__cpumask_clear_cpu()是 非原子的 read-modify-write,底层是__clear_bit()。- 当两个 CPU 同时清除同一字(word)内的不同位时,会发生 lost update。
- 补丁把这一行替换成
cpumask_clear_cpu(),底层走clear_bit(),是原子的 bitop。
Fixes: 指向 48849271e661 ("sched_ext: idle: Per-node idle cpumasks"),也就是引入这个共享 mask 的提交。
旧流程的问题
scx_idle_test_and_clear_cpu() 中三处都会读写共享的 idle_smts:
if (cpumask_intersects(smt, idle_smts)) // 非原子读
cpumask_andnot(idle_smts, idle_smts, smt); // 非原子整字 RMW
else if (cpumask_test_cpu(cpu, idle_smts))
__cpumask_clear_cpu(cpu, idle_smts); // 非原子 RMW(patch 改的位置)
问题在于:每条路径上的 RMW 都不是原子的;两个 CPU 并发清/置同一字的不同位时,写者-写者竞争会导致一方的更新被悄悄覆盖。
新流程
本补丁的"新流程"只替换最后一行:
cpumask_clear_cpu(cpu, idle_smts); // 走 clear_bit(),原子
但 Sashiko 自动审查与 Andrea Righi 都指出:
cpumask_andnot()仍然是整字非原子 RMW,会把同字上cpumask_clear_cpu()写入的原子位"原样擦掉"。update_builtin_idle()里的cpumask_or()同样是整字非原子写。- 因此这个 patch 只覆盖了一个症状,没覆盖根本问题:整张 mask 都在无锁并发地做非原子操作。
Andrea 也指出,原作者早在引入这套逻辑时就把它 显式标注为"有竞争、靠自我修复",并非严格 bug,而是 best-effort 优化路径上的妥协。
Patch 概览
补丁只动 scx_idle_test_and_clear_cpu() 末尾的 else if 分支。其它两条路径(cpumask_andnot、外部 cpumask_or)保持原样。
关键实现
static bool scx_idle_test_and_clear_cpu(int cpu)
{
// ... 省略上下文 ...
if (cpumask_intersects(smt, idle_smts))
cpumask_andnot(idle_smts, idle_smts, smt);
else if (cpumask_test_cpu(cpu, idle_smts))
cpumask_clear_cpu(cpu, idle_smts); // 由 __cpumask_clear_cpu 改为原子版本
return cpumask_test_and_clear_cpu(cpu, idle_cpus);
}
cpumask_clear_cpu() 内部走 arch_clear_bit() / clear_bit():带 LOCK prefix 的原子写;同字内不同位可并发。__cpumask_clear_cpu() 走的是 __clear_bit(),不带 LOCK,多 CPU 并发会丢位。
类比
想象候车厅入口有一块电子白板,列着"现在空着的检票口"。多个检票员无锁地各拿一支马克笔上去改写:
cpumask_andnot/cpumask_or像整张白板重新打印:甲先擦掉 3 号窗口,乙接着重新打印,把甲刚写的 5 号窗口状态覆盖回旧的。__cpumask_clear_cpu像单格橡皮擦:两位检票员同时擦相邻格子,可能把对方的笔迹蹭没。cpumask_clear_cpu是带锁的格子橡皮擦:只锁单格,但整张白板被人重印时,原子擦仍然会被重印覆盖——这就是 Sashiko 指出的"原子 bitop + 非原子整字 RMW 混用"问题。
也就是说,patch 改的不是白板规则(规则仍是"谁能用笔谁就用"),只换了一种更稳的笔。要彻底解决,得把整张白板换成电子锁屏版(统一加锁 / 用 per-CPU mask / 用 cpumask_var_t + 序列号)。
Highlight:风险与注意点
- 修复不完整:仅一处替换无法消除 race;
cpumask_andnot/cpumask_or仍然是同等问题源头。 - 隐含语义被打破:原代码刻意走无锁 + 自我修复路径以避免 fast path 上的锁开销;改一处原子化会让维护者误以为"这里已经安全",掩盖真实设计意图。
- commit message 误导:标题与描述都把这次改动说成"fix race",但实际上并不能"fix",只能算 best-effort 改进。
- 作者最终建议放弃:Michal 在第 4 封邮件里明确表示"与其误导性地半修,不如 drop 这封 patch"。
- 后续跟进建议:真正修法要么把
idle_smts改为 per-CPU 局部 mask + 聚合,要么引入轻量锁(如 sequence counter / RCU),要么用cmpxchg风格的整字 RMW 取代非原子cpumask_andnot/or。
版本变化
无 v2。线程最终走向:作者在评审反馈后决定 drop the patch,整条 thread 终止于"撤回"。
+------------------------------+
| v1: 1-line atomic-clear swap |
| Fixes tag: 48849271e661 |
+------------------------------+
|
| (review)
v+----------------------------------------------+
| Sashiko AI |
| - High: cpumask_andnot / cpumask_or remain |
| non-atomic, fix is incomplete |
+----------------------------------------------+
|
v
+----------------------------------------------+
| Andrea Righi (maintainer) |
| - race pre-dates Fixes commit |
| - original design is "racy + self-correcting"|
| - commit msg should say "best-effort" |
+----------------------------------------------+
|
v
+----------------------------------------------+
| Michal (author) |
| - admits misread original intent |
| - no perf data to justify patch |
| - drop the patch, do not send v2 |
+----------------------------------------------+
一句话总结
这个 patch 想用一次"原子位清零"修补 idle_smts 的并发 race,但因同 mask 上其它整字 RMW 仍非原子、维护者认为原始设计本就"racy + 自我纠正",作者最终决定撤回,而不是发 v2。