0/4 已展开

LLM 分析

sched_ext:放行 cgroup_set_bandwidth 为可阻塞回调

系列概况

  • 标题[PATCH] sched_ext: allow ops.cgroup_set_bandwidth() to be sleepable
  • 作者:Changwoo Min changwoo@igalia.com(Igalia)
  • 版本:v1(单封 patch,series 未标注版本号)
  • 规模:3 个文件,+12 行 / -1 行
  • 修改文件kernel/sched/ext/ext.ckernel/sched/ext/ext.hkernel/sched/ext/internal.h
  • Message-ID20260817170941.668571-1-changwoo@igalia.com
  • 完整性:完整——1 封 patch + 3 封回复,覆盖原作者、Sashiko AI 自动审阅、maintainer Tejun Heo 以及作者再回复

补丁目的

让 BPF scheduler 在为 sched_ext_ops.cgroup_set_bandwidth 注册程序时,可以把它声明为**可阻塞(sleepable)**的 BPF 程序,从而在 cgroup 的 cpu.max 被运行时改写的事件中按需申请内存,不再被迫提前预留。

同时新增一个 BTF marker 函数 scx_cgroup_set_bandwidth_may_sleep(),让 userspace(libbpf / scx 加载器)通过查询内核 BTF 来判断"这版内核已经允许 cgroup_set_bandwidth 睡了"。

旧流程的问题

tg_set_bandwidth() 在调用 scx_group_set_bandwidth() 时:

  • cpus_read_lockcfs_constraints_mutex 已被 tg_set_cfs_bandwidth() 释放;
  • 只持有 percpu_down_read(&scx_cgroup_ops_rwsem),读端可睡眠。

也就是说调用现场在睡眠语义上是安全的,但 BPF 验证器 bpf_scx_check_member() 没有把 cgroup_set_bandwidth 列入 sleepable allow-list,强制把它当非睡眠程序处理。结果:BPF scheduler 必须预先预留内存,不能在回调里调用 bpf_*_alloc 系列。

新流程

bpf_scx_check_member() 增加 case 放行 cgroup_set_bandwidthinternal.h 把文档改写为"This operation may block.";新增 BTF-only marker 函数让 userspace 通过符号是否存在来识别能力。

cgroup cpu.max write
        |
        v
 tg_set_bandwidth()              <-- process context
        |
        v
 scx_group_set_bandwidth(tg, ...)
        |  (percpu_down_read held; sleepable)
        v
 ops.cgroup_set_bandwidth(...)    <-- BPF program, sleepable
        |
        v
   may call bpf_*_alloc() OK <-- now allowed

Patch 概览

  • kernel/sched/ext/ext.c:声明 marker 函数;在 bpf_scx_check_member()cgroup_set_bandwidth 加 case。
  • kernel/sched/ext/ext.h:导出 marker 函数原型。
  • kernel/sched/ext/internal.h:把 sched_ext_ops.cgroup_set_bandwidth 的注释改成 "This operation may block."。

关键实现

A. 验证器侧的"放行"

case offsetof(struct sched_ext_ops, cgroup_init):
case offsetof(struct sched_ext_ops, cgroup_exit):
case offsetof(struct sched_ext_ops, cgroup_prep_move):
#endif
case offsetof(struct sched_ext_ops, cpu_online):
case offsetof(struct sched_ext_ops, cpu_offline):
+case offsetof(struct sched_ext_ops, cgroup_set_bandwidth):

放行之后,BPF verifier 允许挂在这个字段上的程序调用 bpf_kmallocbpf_local_storage_*_alloc 等可睡眠原语。

B. BTF capability marker

+__used void scx_cgroup_set_bandwidth_may_sleep(void) {}

这个函数纯粹作为 BTF 符号存在:userspace 查到它,就认为这版内核允许把它声明为 sleepable;查不到,就走旧的预分配路径。__used 用来阻止编译器优化丢掉。

C. 调用路径上的锁约束

pre : tg_set_cfs_bandwidth() 持有 cpus_read_lock + cfs_constraints_mutex
post: 上面两把锁都已释放
held : percpu_down_read(&scx_cgroup_ops_rwsem)   <-- read-side, may sleep
=> 整条调用路径处于可睡眠上下文

类比

想象酒店礼宾台。

  • 旧规则:客人临时办业务,礼宾台只能当场掏已经摆好的抽屉——预分配好的物料柜,用完就拒绝。
  • 新规则:礼宾台后面打通了一扇门,可以打电话叫仓库开小推车把物料送过来;车在路上会堵车(sleep),那就让 BPF sleepable 走这条慢车道。
  • 门口的告示牌:大堂挂了一块小牌子"礼宾台现可代叫仓库",这就是 scx_cgroup_set_bandwidth_may_sleep() BTF marker。客人看牌子就知道今晚能办,不必提前电话预约。
  • 告示牌的脆弱性:牌子如果是大风一吹就收走的那种(只有 __used),那就只能靠编译器不吃掉它;想钉稳在墙上得用钉子——__retain
  • 牌子越来越多:将来会冒出"健身房现在 24h"、"泳池可以预约"等若干块,新规则应该统一前缀、集中挂在告示栏里,不再每层楼各贴各的。

Highlight:风险与注意点

  1. __used 不够:Tejun Heo 指出 CONFIG_LD_DEAD_CODE_DATA_ELIMINATION=y 时链接器会 GC 掉无 caller 的 section,仅 __used 保不住 BTF 中的符号;需要参考 __bpf_kfunc__retain
  2. 能力 marker 会累积:Tejun 建议统一前缀(如 scx_compat_marker_*),集中放在 ext.c 末尾靠近模块初始化代码的位置,避免全文件点缀。
  3. Sashiko AI 的 High 级已存问题:并发写 cpu.max 时 CFS 与 SCX 带宽状态会 race、BPF 回调被乱序调用。本 patch 没去碰这条线,需要另外追补丁。
  4. 可睡眠回调的语义边界:allow-list 多开一条,就有新的信任面交到 BPF scheduler 作者手里——他们必须在 sleepable 程序里规避 RCU read-side、raw spinlock 等不适睡的临界区,否则会出 hard-to-debug 的栈。
  5. BTF-only ABI 的隐藏约定:marker 函数既无 caller 也无副作用,等价于把"ABI 信号"嵌进 BTF。任何对该 BTF 符号的重命名、删除都会破坏 userspace 检测,维护上要慎重。
  6. 文档同步:本 patch 修了 internal.h 注释,但 Documentation/scheduler/sched-ext.rst 与 helpers(如 scx_group_set_bandwidth 上方注释)如果也描述"may block"会更一致,需要跟 review 确认。

版本变化

v1 -> v2(作者预告,尚未发出):

  • 给 marker 函数补 __retain
  • 重命名为 scx_compat_marker_* 统一前缀;
  • 把所有 capability marker 集中在 ext.c 末尾,靠近模块初始化代码位置;
  • cpu.max 并发 race 不在 v2 范围,作者计划另起一处处理。

一句话总结

本 v1 把 sched_ext_ops.cgroup_set_bandwidth 放行成 sleepable BPF 程序、靠 BTF marker 函数让 userspace 探测该能力,但 __usedCONFIG_LD_DEAD_CODE_DATA_ELIMINATION 下仍会被链接器 GC,v2 需要补 __retain 并把后续越来越多的 capability marker 统一成 scx_compat_marker_* 前缀、集中放在 ext.c 模块初始化附近。