0/4 已展开

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-ID20260901152212.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:风险与注意点

  1. 修复不完整:仅一处替换无法消除 race;cpumask_andnot / cpumask_or 仍然是同等问题源头。
  2. 隐含语义被打破:原代码刻意走无锁 + 自我修复路径以避免 fast path 上的锁开销;改一处原子化会让维护者误以为"这里已经安全",掩盖真实设计意图。
  3. commit message 误导:标题与描述都把这次改动说成"fix race",但实际上并不能"fix",只能算 best-effort 改进。
  4. 作者最终建议放弃:Michal 在第 4 封邮件里明确表示"与其误导性地半修,不如 drop 这封 patch"。
  5. 后续跟进建议:真正修法要么把 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。