sched-ext discussion
[PATCH v3] sched: Lift cgroup update locking to core to prevent CFS/SCX divergence
LLM 分析
sched/cgroup:把 cgroup 更新锁抬升到 core 层,避免 CFS/SCX 状态分裂
系列概况
- 标题:[PATCH v3] sched: Lift cgroup update locking to core to prevent CFS/SCX divergence
- 作者:Michal Blaszczyk michalblk@google.com
- 版本:v3(单 patch)
- 规模:3 files changed, 43 insertions(+), 24 deletions(-)
- 修改文件:kernel/sched/core.c、kernel/sched/fair.c、kernel/sched/sched.h
- 代码统计:core.c +21/-12,fair.c +13/-12,sched.h +7/-0(依据 diffstat)
- Message-ID(首发):20260824074913.2468177-1-michalblk@google.com
- 完整性:标题、Fixes tag、SoB、diffstat 齐全;本帖含首发 patch + 1 条 AC 回复 + 1 条 maintainer Ack 并请 Peter 接收
补丁目的
修复并发写 cgroup 接口(如 cpu.shares / cpu.weight)时,CFS 与 sched_ext(SCX)两条调度路径出现状态分裂的 bug。
具体场景:
- CFS 路径的
sched_group_set_shares()由 fair.c 内部的shares_mutex串行化,但函数返回后这个锁就释放了。 - 之后才会执行
scx_group_set_weight(),后者只持有scx_cgroup_ops_rwsem的读锁,并发写之间无法互斥。 - 结果:
tg->shares、SCX 内部tg->scx.weight、BPF 调度器所见的 weight 出现两两不等的值(CFS=A, SCX 内部=B, BPF=C)。 - 同类竞态也存在于
tg_set_bandwidth()、cpu_idle_write_s64()、cpu_weight_write_u64()、cpu_weight_nice_write_u64()。
旧流程的问题
cgroup write handler
|
v
+---------------------------------------------+
| mutex_lock(&shares_mutex) |
| sched_group_set_shares(...) <-- only CFS |
| mutex_unlock(&shares_mutex) |
+---------------------------------------------+
|
v
read_lock(&scx_cgroup_ops_rwsem) <-- many readers can pass
|
v
scx_group_set_weight(...)
|
v
BPF scheduler ops (dsq weight, etc.)
Race interleaving (CFS vs SCX update are NOT atomic):
T1: lock shares; CFS=A ; unlock ; rlock scx ; SCX=A ; runlock
T2: lock shares; CFS=B; unlock; rlock scx; SCX=B; runlock
-> CFS may end at B while SCX still at A (or vice versa)
-> tg->shares, tg->scx.weight, BPF state can all differ
问题根源:CFS 持锁窗口与 SCX 持锁窗口之间存在空档,且 scx_cgroup_ops_rwsem 是读信号量,无法互斥多个并发写。
新流程
把锁抬升到 core 层,写 handler 在持锁状态下先后调用 CFS 与 SCX callback,让两者在同一个临界区里原子完成。
cgroup write handler (kernel/sched/core.c)
|
v
+-----------------------------------------------------------+
| guard(mutex)(&cpu_weight_mutex) |
| |
| sched_group_set_shares_locked(css_tg(css), shareval) | -> CFS
| |
| scx_group_set_weight(css_tg(css), | -> SCX
| sched_weight_to_cgroup(shareval)) |
| |
| (guard auto-release on return) |
+-----------------------------------------------------------+
|
v
BPF scheduler sees consistent weight
Atomicity guarantee:
Writer1: lock ; CFS=A ; SCX=A ; unlock
Writer2: lock ; CFS=B ; SCX=B ; unlock
-> No interleaving possible between CFS and SCX for the same write
锁的重命名:shares_mutex -> cpu_weight_mutex,cfs_constraints_mutex -> cpu_max_mutex,让锁名与它保护的 cgroup cpu 接口(weight / max)语义对齐。
Patch 概览
- kernel/sched/core.c
- 新增
DEFINE_MUTEX(cpu_weight_mutex)、DEFINE_MUTEX(cpu_max_mutex)。 - 在
cpu_shares_write_u64、cpu_weight_write_u64、cpu_weight_nice_write_s64、cpu_idle_write_s64入口加guard(mutex)(&cpu_weight_mutex),包住 CFS 与 SCX 两段调用。 tg_set_cfs_bandwidth()中将cfs_constraints_mutex替换为cpu_max_mutex,并把cpus_read_lock+ 注释从tg_set_cfs_bandwidth移到tg_set_bandwidth。
- 新增
- kernel/sched/fair.c
- 删除文件内
DEFINE_MUTEX(shares_mutex)。 sched_group_set_shares()拆成「带锁的 wrapper」 + 「_locked版本」。__sched_group_set_shares/sched_group_set_idle内所有mutex_lock/unlock替换为lockdep_assert_held(&cpu_weight_mutex)。
- 删除文件内
- kernel/sched/sched.h
- 导出
extern struct mutex cpu_weight_mutex;与extern int sched_group_set_shares_locked(...);。 - 在
!CONFIG_FAIR_GROUP_SCHED分支提供同名 stub。
- 导出
关键实现
_locked 版本的契约:
/* kernel/sched/fair.c */
int sched_group_set_shares_locked(struct task_group *tg, unsigned long shares)
{
lockdep_assert_held(&cpu_weight_mutex);
/* ... actual CFS weight update ... */
}
int sched_group_set_shares(struct task_group *tg, unsigned long shares)
{
guard(mutex)(&cpu_weight_mutex);
return sched_group_set_shares_locked(tg, shares);
}
core 层写 handler 的统一模板:
/* kernel/sched/core.c */
static int cpu_weight_write_u64(struct cgroup_subsys_state *css, u64 weight)
{
guard(mutex)(&cpu_weight_mutex);
ret = sched_group_set_shares_locked(css_tg(css), scale_load(weight));
if (!ret)
scx_group_set_weight(css_tg(css), sched_weight_to_cgroup(weight));
return ret;
}
tg_set_bandwidth() 同时持 cpus_read_lock() 与 cpu_max_mutex,把原本散落在 tg_set_cfs_bandwidth() 里的 hotplug-vs-runtime 互斥连同 cgroup 接口一起罩住。
类比
把 CFS 和 SCX 想象成同一本家庭账本上的「收入栏」和「支出栏」:原来父亲只在记「收入」时按住账本,记完就松手;母亲再来记「支出」,两人之间存在一个「谁都能翻账本」的窗口,结果月底对账发现一笔钱既被记成收入又被记成支出。修复办法是让其中一人自始至终把整本账本按在桌上(cpu_weight_mutex),从头到尾一次性把 CFS、SCX、BPF 三栏都记完,其他家庭成员只能等他放下账本再动——这样三栏的状态永远一致,不可能再出现「两栏不一致的账」。
Highlight:风险与注意点
_locked函数的契约:sched_group_set_shares_locked()只用lockdep_assert_held做 debug 检查,运行时不会阻止违规调用。任何新增调用点都必须确认外层真的持锁。- 临界区扩大后的延迟:现在所有
cpu.shares/cpu.weight/cpu.idle写都串行化在cpu_weight_mutex上。若 BPF cgroup 钩子里执行较慢,会拖慢全部并发写,需要复测 cgroup 写密集场景(stress-ng --cgroup等)。 - 与
cpus_read_lock()的锁序:tg_set_bandwidth()同时取cpus_read_lock与cpu_max_mutex,需留意 CFS bandwidth hotplug 路径上是否反向持锁,避免 AB-BA。 - SCX 关闭/编译期分支:
!CONFIG_FAIR_GROUP_SCHED下sched_group_set_shares_locked是 stub;core 层仍会取锁,需确认没有把不相关的写路径意外串行化。 - 验证点:用并发工具同时写
cpu.weight,对比tg->shares、tg->scx.weight、BPFdsq中的 weight 是否始终两两相等;并跑tools/testing/selftests/sched_ext回归。 - 后续可观察:CFS/SCX 路径上是否还有别的 cgroup 接口(uclamp、idle、bwfraction 等)走
scx_cgroup_ops_rwsem而未受cpu_weight_mutex/cpu_max_mutex保护。
版本变化
本帖只到 v3,相对前一版只声明了一处改动:「Renamed the shares and cfs_constraints mutexes」,把 fair.c 内部的 shares_mutex 重命名为 cpu_weight_mutex、cfs_constraints_mutex 重命名为 cpu_max_mutex,与 core 层语义对齐。前几版具体改动未在本帖披露,无法追溯。
一句话总结
把 CFS 内部的 weight 锁抬升到 kernel/sched/core.c,让 cpu.shares / cpu.weight / cpu.idle / cpu.max 等 cgroup 写路径在 cpu_weight_mutex / cpu_max_mutex 保护下串行化 CFS 与 SCX callback,避免并发写造成 tg->shares 与 tg->scx.weight 互不相等的分裂 bug。