0/2 已展开

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_mutexcfs_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_u64cpu_weight_write_u64cpu_weight_nice_write_s64cpu_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_lockcpu_max_mutex,需留意 CFS bandwidth hotplug 路径上是否反向持锁,避免 AB-BA。
  • SCX 关闭/编译期分支!CONFIG_FAIR_GROUP_SCHEDsched_group_set_shares_locked 是 stub;core 层仍会取锁,需确认没有把不相关的写路径意外串行化。
  • 验证点:用并发工具同时写 cpu.weight,对比 tg->sharestg->scx.weight、BPF dsq 中的 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_mutexcfs_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->sharestg->scx.weight 互不相等的分裂 bug。