0/5 已展开

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.ckernel/sched/fair.ckernel/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;v2 20260821140818.1559100-1-michalblk@google.com
  • 完整性:包含 v1 patch、Tejun 与 Peter 的两条评审、v2 patch、Tejun 对 v2 的命名建议;缺 v3 投稿,线程仍在修订中

补丁目的

修复一个并发漏洞:当用户同时向 cgroup 控制文件(cpu.sharescpu.weightcpu.maxcpu.idlecpu.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_u64tg_set_bandwidthcpu_idle_write_s64cpu_weight_write_u64cpu_weight_nice_write_s64)统一 guard(mutex) 持锁;调用从 sched_group_set_shares() 改为 sched_group_set_shares_locked()cfs_constraints_mutexcpus_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_mutexextern 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.sharescpu.weightcpu.weight.nicecpu.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_mutexcpu_max_mutex,更贴近实际语义;v3 需要回应这一命名建议。
  • cfs_constraints_mutex 的位置:v2 将其从 tg_set_cfs_bandwidth 顶部移到 tg_set_bandwidth,意味着 cgroup 写上下文外不再持有,行为差异需要在 changelog 中明确说明。
  • 回归测试面:建议在 selftest/sched_ext 中新增并发写 cpu.sharescpu.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。