sched-ext discussion
[PATCH] sched: Serialize cgroup updates to prevent CFS/SCX state divergence
LLM 分析
sched:cgroup 写锁提升到 core 层消除 CFS/SCX 状态发散
系列概况
- 标题:
[PATCH v2] sched: Lift cgroup update locking to core to prevent CFS/SCX divergence(v1 标题为[PATCH] sched: Serialize cgroup updates to prevent CFS/SCX state divergence) - 作者:Michal Blaszczyk michalblk@google.com
- 版本:v2(v1 到 v2 之间经 Tejun Heo 与 Peter Zijlstra 评审改写)
- 规模:v1 单文件 +21/-0;v2 涉及 3 个文件 +42/-23
- 修改文件:
kernel/sched/core.c、kernel/sched/fair.c、kernel/sched/sched.h - 代码统计:v2 在 core.c5 个 cgroup 写回调上加
guard(mutex)(&shares_mutex),新增sched_group_set_shares_locked();fair.c 删除static DEFINE_MUTEX(shares_mutex),把sched_group_set_shares()改成 guard(mutex) 包装;sched.h 导出 mutex 与_locked声明,并补!CONFIG_FAIR_GROUP_SCHED下的 stub - Message-ID:v1
20260820160956.910663-1-michalblk@google.com;v220260821140818.1559100-1-michalblk@google.com - 完整性:包含 v1 patch、Tejun 与 Peter 的两条评审、v2 patch、Tejun 对 v2 的命名建议;缺 v3 投稿,线程仍在修订中
补丁目的
修复一个并发漏洞:当用户同时向 cgroup 控制文件(cpu.shares、cpu.weight、cpu.max、cpu.idle、cpu.weight.nice)写入时,CFS 内部记录的权重、SCX 内部记账(如 tg->scx.weight)、BPF 调度器自身看到的参数三者会出现两两不一致。
具体路径:cpu_shares_write_u64() 中 sched_group_set_shares() 内部用 shares_mutex 串行化 CFS 写,但 shares_mutex 在返回前就被释放;紧接其后的 scx_group_set_weight() 只对 scx_cgroup_ops_rwsem 加读锁,多个线程因此能交错执行。三条数据通路看到的是完全不同的权重值。
Fixes tag 指向 819513666966 ("sched_ext: Add cgroup support"),说明 bug 随 cgroup 支持一起引入。
旧流程的问题
Thread A: write cpu.shares=100 ---+
+--> sched_group_set_shares()
Thread B: write cpu.shares=200 ---+ |
+-- lock(shares_mutex) [fair-only]
+-- CFS update [fair-only]
+-- unlock(shares_mutex) [LOCK GONE]
|
+-- read_lock(scx_cgroup_ops_rwsem)
+-- scx_group_set_weight() [concurrent!]
A sees: CFS=100, SCX=200
B sees: CFS=200, SCX=100
Result: pairwise divergent state across CFS / SCX / BPF
要点:
shares_mutex是 fair 层局部锁,作用域只覆盖 fair 内部;scx_cgroup_ops_rwsem是读信号量,并发读者不被互斥;- CFS 写与 SCX 写之间存在一个无锁时间窗,允许交叉。
新流程
v2 方案:把 shares_mutex 从 fair.c 提升到 core.c,让 cgroup 写回调先持有锁再调 CFS、再调 SCX;同时把 sched_group_set_shares() 拆成对外的自动上锁包装与内部的 _locked() 版本,避免内部调用再次尝试上锁。
core.c: cpu_shares_write_u64()
|
+-- guard(mutex)(&shares_mutex) <-- core-layer lock
|
+-- sched_group_set_shares_locked() <-- no double lock
| |
| +-- __sched_group_set_shares() <-- CFS update
|
+-- scx_group_set_weight() <-- SCX update, same lock |
+-- (guard scope ends -> unlock)
Patch 概览
kernel/sched/core.c:新增DEFINE_MUTEX(shares_mutex);5 个写回调(cpu_shares_write_u64、tg_set_bandwidth、cpu_idle_write_s64、cpu_weight_write_u64、cpu_weight_nice_write_s64)统一guard(mutex)持锁;调用从sched_group_set_shares()改为sched_group_set_shares_locked()。cfs_constraints_mutex与cpus_read_lock同步下沉到tg_set_bandwidth()内部。kernel/sched/fair.c:删除static DEFINE_MUTEX(shares_mutex);把__sched_group_set_shares暴露为sched_group_set_shares_locked()并加lockdep_assert_held(&shares_mutex);sched_group_set_shares()改成 guard(mutex) 包装;sched_group_set_idle()内的多余mutex_unlock清理。kernel/sched/sched.h:导出extern struct mutex shares_mutex与extern int sched_group_set_shares_locked(...);!CONFIG_FAIR_GROUP_SCHED分支补充内联 stub。
关键实现
/* kernel/sched/core.c */
DEFINE_MUTEX(shares_mutex);
static int cpu_shares_write_u64(struct cgroup_subsys_state *css,
struct cftype *cftype, u64 shareval)
{
guard(mutex)(&shares_mutex);
ret = sched_group_set_shares_locked(css_tg(css),
scale_load(shareval));
/* same lock held when scx_group_set_weight() is called below */
...
}
/* kernel/sched/fair.c */
int sched_group_set_shares_locked(struct task_group *tg,
unsigned long shares)
{
lockdep_assert_held(&shares_mutex);
...
}
int sched_group_set_shares(struct task_group *tg, unsigned long shares)
{
guard(mutex)(&shares_mutex);
return sched_group_set_shares_locked(tg, shares);
}
sched_group_set_idle() 内原先残留的 mutex_unlock(&shares_mutex) 在 v2 中被删除,因为锁已经在 core 层管。
类比
把 CFS 与 SCX 看作一家公司里两本独立的账本:财务(CFS)每次记账前先把账本锁起来,记完立刻放回抽屉;运营(SCX)只是把账本摊开在桌面供多人围观(读信号量),谁都可以随手改一行。两边没有共同的上锁—改账—放锁流程,于是 A 改了 100、B 改了 200,最后财务账本、运营登记、对外公告三者各说各话。v2 的修复相当于在部门门口加了一把总钥匙,进门后两本账一次性改完再出门,从此三方永远一致。
Highlight:风险与注意点
- 共享一把锁的代价:
shares_mutex同时保护cpu.shares、cpu.weight、cpu.weight.nice、cpu.idle四类写路径,写并发被强制串行;但所有这些都是 cgroup 文件写,开销可控。 - fair.c 内仍有自动上锁版本:
sched_group_set_shares()(包装sched_group_set_shares_locked())必须确保内部调用一律走_locked变体,否则guard(mutex)会触发嵌套上锁告警。v2 通过lockdep_assert_held在_locked中自检。 - 命名争议:Tejun 在第 5 封邮件建议改名为
cpu_weight_mutex与cpu_max_mutex,更贴近实际语义;v3 需要回应这一命名建议。 - cfs_constraints_mutex 的位置:v2 将其从
tg_set_cfs_bandwidth顶部移到tg_set_bandwidth,意味着 cgroup 写上下文外不再持有,行为差异需要在 changelog 中明确说明。 - 回归测试面:建议在 selftest/sched_ext 中新增并发写
cpu.shares与cpu.weight的回归用例,覆盖CONFIG_FAIR_GROUP_SCHED/CONFIG_EXT_GROUP_SCHED启用与禁用的编译组合。
版本变化
- v1 到 v2:放弃新增
scx_cgroup_mutex,改为把 fair 层已有的shares_mutex上提到 core 层,并新增sched_group_set_shares_locked()让内部调用复用同一锁;同步清理sched_group_set_idle()中的多余mutex_unlock,把cfs_constraints_mutex的获取下沉到带宽设置函数内部;标题从 "Serialize cgroup updates" 改为 "Lift cgroup update locking to core"。
一句话总结
通过把 fair 层 shares_mutex 提升到 cgroup 写回调入口,确保 CFS 与 SCX 在同一把锁下看到一致的权重,从而消除 819513666966 引入的并发写状态发散;v3 还需回应命名建议并补 selftest。