sched discussion
[PATCH] cpufreq: schedutil: Publish util hooks only after all sg_cpu are initialized
LLM 分析
cpufreq/schedutil: 仅在所有 sg_cpu 初始化完成后才发布 util hook
系列概况
- 标题: [PATCH] cpufreq: schedutil: Publish util hooks only after all sg_cpu are initialized
- 作者: Zhongqiu Han zhongqiu.han@oss.qualcomm.com
- 版本: 单封 PATCH,无 vN 序号,非系列
- 规模: 1 file changed, 11 insertions(+), 0 deletions(-)
- 修改文件:
kernel/sched/cpufreq_schedutil.c - 代码统计: +11 / -0;新增一个
for_each_cpu循环专做cpufreq_add_update_util_hook(),原循环保留 memset 初始化部分 - Message-ID: 20260716115159.848403-1-zhongqiu.han@oss.qualcomm.com
- 完整性: 提交信息完整,含 Fixes tag(指向 16a03c71bba0)、Cc stable、Signed-off-by。线程内还有 Christian Loehle(Arm)与 Rafael J. Wysocki(Intel)各一封回复,正文均为对补丁 commit message 的全文或部分引用,在 Lore 上看起来都截断于引文末尾,未观察到明显新增的实质评论。
补丁目的
schedutil 是把 scheduler 的 util 信号转成 CPU 频率决策的 governor。对于 shared cpufreq policy(多 CPU 共享同一频率域,例如大核簇),多个 CPU 共享一个 sg_policy,并各自维护一份 sugov_cpu(per-CPU 数据)。sugov_start() 启动 governor 时需先初始化每个 per-CPU sugov_cpu,再向 scheduler 注册每个 CPU 的 util update hook。
该补丁修复一个回归:commit 16a03c71bba0 把"逐 CPU 初始化 + 注册 hook"合并到一个循环里,看似简洁,却无意中重新踩到了 ab2f7cf141aa 当年已经修过的那条 race。
旧流程的问题
旧实现把初始化和 hook 注册放在同一个 for_each_cpu 循环里:
for_each_cpu(cpu, policy->cpus) {
struct sugov_cpu *sg_cpu = &per_cpu(sugov_cpu, cpu);
memset(sg_cpu, 0, sizeof(*sg_cpu));
sg_cpu->cpu = cpu;
sg_cpu->sg_policy = sg_policy;
cpufreq_add_update_util_hook(cpu, &sg_cpu->update_util, uu);
}
问题链条:
cpufreq_add_update_util_hook()一旦对某个 CPU 注册成功,scheduler 在 RCU-sched 域立即可触发sugov_update_shared()。sugov_start()持有policy->rwsem;但 scheduler 的 util 路径根本不取这把锁,rwsem 拦不住更新端。- 更新端持
sg_policy->update_lock;初始化端持policy->rwsem。两把锁没有公共部分,互不序列化。 - 当首个 sibling 的 hook 已发布,循环还在给其他 sibling 做
memset(0)时,sugov_next_freq_shared()会遍历整个 shared group,并发读/写其它 sibling 的iowait_boost、util、bw_min等标量字段,踩到尚未填好的结构上。
后果:访问都是标量,今天不会立刻 crash,只会读到 stale 或 0 值,调频决策偏移、tracepoint 错乱;但这是真正的 data race,未来一旦在这条路径里解引用任何指针(例如 sg_cpu->sg_policy),就立刻变 latent crash。
新流程
恢复两阶段:
/* Phase 1: 初始化所有 sg_cpu,但不暴露给 scheduler */
for_each_cpu(cpu, policy->cpus) {
struct sugov_cpu *sg_cpu = &per_cpu(sugov_cpu, cpu);
memset(sg_cpu, 0, sizeof(*sg_cpu));
sg_cpu->cpu = cpu;
sg_cpu->sg_policy = sg_policy;
}
/*
* Publish the hooks only after all per-CPU data is initialized, so a
* shared policy's sugov_update_shared() never reads an uninitialized
* sibling sugov_cpu.
*/
for_each_cpu(cpu, policy->cpus) {
struct sugov_cpu *sg_cpu = &per_cpu(sugov_cpu, cpu);
cpufreq_add_update_util_hook(cpu, &sg_cpu->update_util, uu);
}
第一阶段先把所有 sugov_cpu 内存清零并填好 cpu/sg_policy,确保结构完整、内部一致;第二阶段才发布 hook。第一个 hook 暴露时,所有 sibling 结构已就绪,scheduler 即时触发的 sugov_update_shared() 遍历到的每个 sugov_cpu 都已构造完成。
shared policy (e.g. CPU4..CPU7)
+----------------------------------------+
| sg_policy -> update_lock : unique |
| policy->rwsem : init-side only |
+----------------------------------------+
Old (bad) flow New (good) flow
---------------- -----------------
for cpu in cpus: Phase 1: memset all sg_cpu
memset(sg_cpu) for cpu in cpus:
fill cpu/sg_policy memset(sg_cpu)
add_hook(cpu) fill cpu/sg_policy
|
v
update path wakes up Phase 2: publish hooks
immediately on first for cpu in cpus:
published hook, sees other add_hook(cpu)
siblings still half-zero |
v
update path only sees
fully-initialized sg_cpu
关键实现
补丁只动一处:在原循环后再插入一个 for_each_cpu 循环专做 cpufreq_add_update_util_hook(),原循环保留 memset 初始化。关键约束:
- 没有新增任何锁;两阶段靠 happens-before 发布语义(hook 注册本身就是 RCU 侧的发布动作)打破 race,不是靠锁。
policy->cpus在两个循环期间不会变(仍受policy->rwsem保护),所以两个循环看到的 CPU 集合完全一致。- 纯结构重构:对其他 governor 类型无影响,公共调度路径语义不变。
锁与发布关系示意:
sugov_start() sugov_update_shared()
----------- ----------------------
acquire policy->rwsem RCU-sched (no policy->rwsem)
memset(sg_cpu) .............. race . read sg_cpu->iowait_boost
fill cpu/sg_policy sg_cpu->util
add_hook() -- publish ---> (atomic) --> sg_cpu->bw_min
release policy->rwsem acquire sg_policy->update_lock
release sg_policy->update_lock
Patch 概览
| 项 | 内容 |
|---|---|
| 触发 commit | 16a03c71bba0 (Merge init in single loop) |
| 之前修复 | ab2f7cf141aa (Fix sugov_start vs sugov_update_shared race) |
| 改动定位 | kernel/sched/cpufreq_schedutil.c 函数 sugov_start() |
| 改动行数 | 11 insertions(+), 0 deletions(-) |
| 关键标签 | Fixes: 16a03c71bba0、 Cc: stable@vger.kernel.org |
类比
把它想象成剧场的开门放人:
- 老做法(坏):工作人员把检票闸机一个一个地打开,每开一个就放人进去。第一个闸机后面的座位还没摆椅子,后到的观众只能坐在地上看戏——他们以为坐的是"摆好的椅子",其实椅子此刻还在仓库里被工人搬进来。椅子全是硬物、没有指针,所以没人摔骨折,但全场观众坐错位置、看错场次。
- 新做法(好):先把所有椅子都摆好、把场景搭完,最后才统一打开闸机放观众入场。第一个观众进场时,所有座位已经到位,不再出现"坐错椅子"的情况。
- 中间那两把没对齐的锁(
policy->rwsemvssg_policy->update_lock)就像"剧场大门"的钥匙和"仓库门"的钥匙——两把钥匙开的不是同一扇门,保安拿哪一把都拦不住对方进出。真正的解法是把摆椅子的工作全部完成才开门,而不是寄希望于两把钥匙。
Highlight:风险与注意点
- 今天不 crash ≠ 没有 bug:标量写入看起来"安全",但 KCSAN/TSAN 仍会把它标为 data race;它正潜伏等待下一次有人在这函数里解引用指针就立刻引爆。
- 历史已踩过同类问题:
ab2f7cf141aa当年修过同样形态的 race,说明sugov_start()的初始化顺序本来就对 race 高度敏感;16a03c71bba0的"合并循环"看似重构,实则回退了那次修复。 - stable 标注:补丁带
Cc: stable@vger.kernel.org,因为问题在所有受16a03c71bba0影响的 stable 分支上同样存在,回流优先级高。 - 测试覆盖难点:单纯 functional 测试很难发现这条 race,因为读到的"几乎为零"的标量只会让频率略偏,肉眼难察。需要 KCSAN、stress + cpufreq shared policy 的组合测试才能稳定复现。
- 后续关注:Christian Loehle 与 Rafael J. Wysocki 的两封回复正文均为引用 patch 文本,正文未显示实质评论;需要关注后续 v2 是否带
Acked-by/Reviewed-by,以及是否有测试 patch 跟进。
一句话总结
把 sugov_start() 的"初始化 + 注册 hook"重新拆成两阶段,先把每个 sibling 的 sugov_cpu 全部 memset 完整,再统一通过 cpufreq_add_update_util_hook() 暴露给 scheduler,避免 sugov_update_shared() 在 RCU-sched 路径下读到尚未初始化的标量字段,关闭 16a03c71bba0 重新引入的 latent race。