0/1 已展开

LLM 分析

sched_ext:序列化 cgroup knob 更新

系列概况

  • 标题[PATCH sched_ext/for-7.3-fixes] sched_ext: Serialize cgroup knob updates
  • 作者:Andrea Righi <arighi@nvidia.com>
  • 版本:单封 patch,目标分支 sched_ext/for-7.3-fixes
  • 规模:修改 4 个文件,+38 / -10
  • 修改文件include/linux/sched/ext.hkernel/sched/core.ckernel/sched/ext/ext.ckernel/sched/ext/ext.h
  • Message-ID20260825092153.2602809-1-arighi@nvidia.com
  • 完整性:单封 patch 完整,可独立 review;回信讨论替代方案

补丁目的

并发写 cgroup knob(weight / idle / bandwidth)时,核心调度器在自己的内部锁里更新 task_group,但 释放锁之后 才把这次更新通知给 sched_ext。两个并发的写各自完成核心调度器更新,但它们的 sched_ext 通知可能乱序完成。结果就是:核心调度器里 tg->shares 已经更新到 B 值,BPF scheduler 看到的却是 A 值(或反之),三者状态错位。补丁的目标是把"核心调度器更新"和"sched_ext 通知"绑成一个原子事务,确保同一 task_group 的连续写严格按顺序落地。

旧流程的问题

  CPU A: cgroup_lock --> sched_group_set_shares(100) --> cgroup_unlock |
 v (delay before BPF call)
                          scx_group_set_weight(100) <- notify A
  CPU B: cgroup_lock --> sched_group_set_shares(200) --> cgroup_unlock
 v (delay)
                                                  scx_group_set_weight(200)   <- notify B (arrives first)
 Problem: notify B reaches BPF scheduler before notify A
           core scheduler says 200, BPF cache says 100 -> mismatch

sched_ext 已经有一把 scx_cgroup_ops_rwsem(percpu rwsem),但它只保护 BPF ops 调用本身,覆盖核心调度器那段更新,所以串不起来。

新流程

给每个 task_group 加一把 knob_mutex,把核心调度器更新和 sched_ext 通知都包在同一把锁里:

  CPU A: knob_mutex.lock --> sched_group_set_shares(100) --> scx_group_set_weight(100) --> knob_mutex.unlock
  CPU B:                                                                 |
  CPU B:                                                                                       v (blocked)
  CPU B: knob_mutex.lock --> sched_group_set_shares(200) --> scx_group_set_weight(200) --> knob_mutex.unlock
  Result: notifications follow core update order strictly, all three views consistent

Patch 概览

四处改动:

  1. include/linux/sched/ext.h:在 struct scx_task_group 增加 struct mutex knob_mutex;
  2. kernel/sched/ext/ext.cscx_tg_init()mutex_init(&tg->scx.knob_mutex);
  3. kernel/sched/ext/ext.h:定义 lock / unlock 函数,并用 DEFINE_GUARD(scx_group_knob, ...) 包成作用域锁;当 CONFIG_EXT_GROUP_SCHED 关闭时退化为空函数。
  4. kernel/sched/core.c:在 tg_weightcpu_idle_write_s64cpu_weight_write_u64cpu_weight_nice_write_u64tg_set_bandwidth 等写路径加入 guard(scx_group_knob)(tg);,并把 css_tg(css) 提取到局部变量以减少重复求值。

关键实现

锁封装代码(来自 kernel/sched/ext/ext.h):

static inline void scx_group_knob_lock(struct task_group *tg)
{
	mutex_lock(&tg->scx.knob_mutex);
}

static inline void scx_group_knob_unlock(struct task_group *tg)
{
	mutex_unlock(&tg->scx.knob_mutex);
}

DEFINE_GUARD(scx_group_knob, struct task_group *,
 scx_group_knob_lock(_T), scx_group_knob_unlock(_T));

/* CONFIG_EXT_GROUP_SCHED disabled stubs */
static inline void scx_group_knob_lock(struct task_group *tg) {}
static inline void scx_group_knob_unlock(struct task_group *tg) {}

调用点改写(来自 kernel/sched/core.c):

static int cpu_weight_write_u64(struct cgroup_subsys_state *css,
				struct cftype *cftype, u64 cgrp_weight)
{
	struct task_group *tg = css_tg(css);
	unsigned long weight;
	int ret;

	weight = sched_weight_from_cgroup(cgrp_weight);

	guard(scx_group_knob)(tg);
	ret = sched_group_set_shares(tg, scale_load(weight));
	scx_group_set_weight(tg, cgrp_weight);

	return ret;
}

guard() 是内核作用域锁惯用法,函数返回时自动 unlock,避免漏写 unlock 或提前 return 留下死锁。锁作用域关系见下图:

  +-----------------------------+
  | cgroup write handler entry |
  +-------------+---------------+
                |
                v
  +-----------------------------+
  | guard(scx_group_knob)(tg)   |  <- acquires knob_mutex
  +-------------+---------------+
                |
                v
  +-----------------------------+
  | sched_group_set_shares(tg)  |  <- core scheduler update
  +-------------+---------------+
                |
                v
  +-----------------------------+
  | scx_group_set_weight(tg)    |  <- sched_ext notify (BPF call)
  +-------------+---------------+
                |
                v
  +-----------------------------+
  | function return / guard exit|  <- knob_mutex auto-released
  +-----------------------------+

类比

把每个 cgroup 想象成银行的一个 VIP 客户账户,并发写就像两个柜员同时改这个账户的余额:

  • 旧流程:柜员 A 在 A 窗口把余额改成 100,柜员 B 在 B 窗口把余额改成 200,但 A 的"短信通知"发得比 B 晚。客户看到的短信顺序是 200 -> 100,跟卡里真实余额对不上,账单对账失败。
  • 新流程:给每个 VIP 账户发一把"业务章"(knob_mutex),任何柜员改这个账户都要先盖章,改完余额立刻发短信,最后才能解章。两个柜员不可能同时改同一个账户,短信和真实余额就一定对齐。

更进一步,Tejun 推荐的 Michal 方案相当于直接把"短信通道"挪进银行核心系统,让改余额和发通知在同一笔业务里完成,连那把额外的章都不用发——更"治本"。

Highlight:风险与注意点

  1. 维护者倾向 Michal 的方案:Tejun 明确说"I'm inclined to go with that one",Michal 把 fair 的锁提升到 core 写 handler,共用一把锁,省掉 knob_mutex 这一层嵌套。Andrea 这版大概率会被弃用或演化成 v2。
  2. 嵌套锁风险knob_mutex 只在 cgroup 写路径获取,没有和 rq lock / cgroup 自旋锁嵌套,但 tg_set_bandwidth 里还有 cfs_bandwidth 相关锁,新加的 guard 是否落在合理位置需要细看 diff 上下文。
  3. CONFIG 开关覆盖cpu_weight_writeCONFIG_GROUP_SCHED_WEIGHTcpu_idle_writeCONFIG_CFS_BANDWIDTH,guard 调用要确保对应编译路径下 scx_group_knob_lock 桩存在;#else 分支需要空桩版本(本 patch 已提供)。
  4. scx_cgroup_ops_rwsem 重叠:已存在的 percpu rwsem 是为 BPF ops 串行化而设;knob_mutex 范围更宽、且非 percpu,要留意未来是否会引入不必要的全局争用。
  5. 回归验证点:每个写 handler 都加锁后,需跑 cgroup v1/v2 的 weight / cpu.idle / cpu.max 写压测,确认不会出现死锁、优先级反转或 BPF 程序被饿死。

版本变化

单封 patch,没有 v2/v3。

与其他 patch 系列的关联

Tejun 在回信中点名了 Michal Koutný 的同主题 patch:

  • http://lkml.kernel.org/r/20260824074913.2468177-1-michalblk@google.com

两版是同一问题的两种修法:

  • Andrea:scx 侧加 per-tg mutex,core 侧用 guard 加锁。
  • Michal:把 fair 写 handler 的锁提升,使 core 更新和 sched_ext 通知在同一把锁下串行。

维护者倾向 Michal 的"提升到 core"做法。

一句话总结

并发写 cgroup knob 时核心调度器更新和 sched_ext 通知会乱序,导致 core / BPF / sched_ext 缓存三者值错位——补丁在每个 task_group 上加一把 knob_mutex 把两次更新绑成原子事务,但维护者更偏好另一种"把 fair 锁提到写 handler 里"的同主题方案。