0/6 已展开

LLM 分析

sched_ext/scx_flatcg:让权重变化真正生效

系列概况

  • 标题:[PATCH] sched_ext/scx_flatcg: expire cached hweights on weight changes
  • 作者:Tao Cui cuitao@kylinos.cn
  • 版本:单版本(v1,单 patch)
  • 规模:1 file changed, 3 insertions(+)
  • 修改文件tools/sched_ext/scx_flatcg.bpf.c
  • 代码统计:3 行新增
  • Message-ID20260814144116.2767304-1-cui.tao@linux.dev
  • 完整性:完整 PATCH,含 commit message、SoB、diff 与验证说明;已被 Tejun Heo 拣入 sched_ext/for-7.3

补丁目的

scx_flatcgfcg_cgroup_set_weight() 在更新 cgc->weight 与父节点 child_weight_sum 时,没有让缓存的层级权重(hierarchical weight,hweight)失效。

对于永远没有 0→n 可运行状态跃迁的"持续忙"cgroup,原本只在任务激活时才推进的 hweight_gen 永远不会更新,导致 cgrp_refresh_hweight() 一直沿用旧的层级权重参与调度分配——cpu.weight 的修改对调度完全不可见。

修复方法是在权重写入后显式推进 hweight_gen,让下一次 refresh 用新权重重算 hweight。

旧流程的问题

user writes cgroupfs: cpu.weight 100 -> 800
        |
        v
fcg_cgroup_set_weight()
   - update cgc->weight
   - update parent pcgc->child_weight_sum
   - unlock cgv_tree_lock
        |
        v
hweight_gen unchanged
        |
        v
next cgrp_refresh_hweight() sees gen matches
        |
        v
keeps old hweight; 800:100 ratio never takes effect

只有当某个 task 进入 0→n runnable 跃迁时,激活路径才会推进 hweight_gen。如果 cgroup 内所有任务都已经常驻 CPU 上运行(典型 CPU-bound 负载),这条"逃生通道"永远不被触发,权重改动永远传不到调度器。

新流程

fcg_cgroup_set_weight()
   - update cgc->weight
   - update parent pcgc->child_weight_sum
   - unlock
   + __sync_fetch_and_add(&hweight_gen, 1)   <-- new
        |
        v
next cgrp_refresh_hweight() sees gen mismatch
        |
        v
recompute hweight with new weight
        |
        v
new ratio takes effect immediately

关键实现

void BPF_STRUCT_OPS(fcg_cgroup_set_weight, struct cgroup *cgrp, u32 weight)
{
    ...
    pcgc->child_weight_sum += (s64)weight - cgc->weight;
    cgc->weight = weight;
    bpf_spin_unlock(&cgv_tree_lock);

    + /* expire cached hweights so the new weight propagates */
    + __sync_fetch_and_add(&hweight_gen, 1);
}

关键点:

  • __sync_fetch_and_add 保证 BPF 侧多 CPU 并发推进 hweight_gen 的原子性,与 cgrp_refresh_hweight() 里的比较/写入路径相对应。
  • 推进点放在 bpf_spin_unlock 之后,避免把锁内临界区延长。
  • 这是个 BPF_STRUCT_OPS 回调,每次用户层 cg_set_weight 都会调用一次,开销可忽略。
  • hweight_gen推进后会令所有 cgrp节点的 cached_gen != cur_gen,下一次 refresh 触发整树重算。

ASCII 图:刷新判定

cgrp_refresh_hweight(cgrp)
        |
        +-- read  cgrp->hweight_gen   (cached_gen)
        |
        +-- read  global hweight_gen  (cur_gen)
        |
        +-- if cached_gen == cur_gen:
        |       return cached hweight   <-- old weight kept
        |
        +-- else:
                recompute hweight from new weight_sum
                write back cached_gen = cur_gen
                return new hweight

只有当 hweight_gen 推进,cur_gen != cached_gen 才成立,刷新才会真正发生。

类比

想象一个图书馆有多个阅览室,每个阅览室门口挂着一块写有"接待时长比例"的牌子(hweight),牌子写好后放在那里很久没人更新(缓存)。

平时有读者进进出出(任务 0→n 激活),馆员会顺手把所有牌子刷新一次(gen 推进)。但如果有一个阅览室的读者一直在里面看书、没人出去再进来(持续忙碌的 cgroup),那块牌子就一直挂着旧数字。

新读者来问"我该去哪个阅览室",馆员只看牌子,就会把读者塞进老牌子上写的那个小房间。

补丁相当于:馆长直接下令"阅览室比例改了,所有牌子全部作废,请重写"。即使没人进出,下一次问路的读者也能拿到按新比例分配的阅览室。

Highlight:风险与注意点

  1. __sync_fetch_and_add 的 BPF 兼容性:scx 示例已经使用,惯例保持一致即可;后续若换 helper,需重新校验。
  2. 推进时机:放在 bpf_spin_unlock 之后,避免持锁推进;若有人后续把它移到锁内,要小心递归取 cgv_tree_lock 的死锁。
  3. Tejun 在回复中顺手指出的"另一只虫"fcg_dispatch() 的 true-up(__sync_fetch_and_add(&cgc->cvtime_delta, ...))看起来也有问题,属于另一个 bug 的伏笔,需要后续补丁。
  4. 持久忙 cgroup 之外的场景:补丁只补了"完全无跃迁"的极端情况;非极端场景下原本就能被任务激活路径覆盖,效果一致。
  5. 示例调度器定位scx_flatcgtools/sched_ext/ 下的示例,并非主调度路径,影响面有限,但作为用户/开发者的参考实现,正确性同样重要。

Patch 概览

[PATCH 1/1] sched_ext/scx_flatcg: expire cached hweights on weight changes
   file: tools/sched_ext/scx_flatcg.bpf.c
   +3 lines in fcg_cgroup_set_weight()
   function: after writing weight bump hweight_gen,
             so cached hierarchical weight expires with write

版本变化

本系列只有 v1,无 v1→vN+1 演进。被 Tejun Heo 直接拣入 sched_ext/for-7.3,故 v1 即为最终版本。

与其他 patch 系列的关联

  • 与 Tejun 在本帖第 5 封邮件里提到的 fcg_dispatch() true-up 问题属于同一文件的两处独立 bug,预计会有后续 patch 跟进修复 cvtime_delta 计算。
  • sched_ext 整体的缓存一致性机制(hweight_gen 单调推进)保持同一思路,可视为同一治理模式的延伸。

一句话总结

scx_flatcg 在改写 cgroup 权重时漏掉了"作废 hweight 缓存"的步骤,对持续忙碌的 cgroup 来说权重改动永远传不到调度器;只需在 fcg_cgroup_set_weight() 末尾原子推进 hweight_gen,下一次刷新就会按新权重重算。