0/10 已展开

LLM 分析

sched_ext: Sub-scheduler 和 cid 修复

系列基线信息

字段
标题sched_ext: Sub-scheduler and cid fixes
作者Tejun Heo
版本v2 → v3(patch 4/4 单独升版本)
规模4 patches,5 个文件,+298/-166 行
Message-ID20260721223118.2268529-1-tj@kernel.org
来源sched-ext
目标分支sched_ext/for-7.3

明确目的

本系列修复 sched_ext 子调度器(sub-scheduler)和 cid(core-id)映射表的四个问题:

  1. Runnable stall 责任归属错误——子调度器 bypass 期间,任务停在其祖先的 bypass DSQ 上,watchdog 却把 stall 归咎于子调度器而非真正负责排空该 DSQ 的祖先,导致退出信息不可操作。
  2. Bypass 期间无效 CPU 选择——bypass 模式下 select_task_rq_scx() 走默认路径调用 scx_select_cpu_dfl(),但选出的 CPU 会被 bypass enqueue 丢弃;且自定义 idle tracking 的调度器会让内置 idle cpumask 冻结,选择结果不可靠。
  3. 删除死代码——scx_cpumask_to_cmask() 无任何调用者。
  4. cid 表竞争读取——全局 cid 表在填充和原地重写期间对 BPF kfunc 可见:首次 enable 先发布指针再填充、ops.init_cids() 原地覆盖、re-enable 原地重建。竞态的 TRACING/SYSCALL 程序可读到未填充条目、未初始化内存或撕裂的 topo 更新。

遍历代码

Patch 1/4:DSQ stall 责任归属

check_rq_for_timeouts() 原来把 runnable stall 归咎于任务所属的调度器(task->scx.dsq->sched 的 owner)。

新增逻辑:如果任务所在的 DSQ 有明确的 sched 字 owning scheduler,且该 DSQ 不是 SCX_DSQ_LOCAL(local DSQ 由 CPU 自己消费,责任仍归 owner),则把 stall 归咎于 DSQ 的 owning scheduler。

struct scx_dispatch_q *dsq = READ_ONCE(p->scx.dsq);
if (dsq && dsq->sched && dsq->id != SCX_DSQ_LOCAL)
    sch = dsq->sched;

效果:bypass 子调度器的任务停在祖先 bypass DSQ 上时,watchdog 归咎祖先而非子调度器。

Patch 2/4:跳过 bypass 期间的默认 CPU 选择

select_task_rq_scx() 在 bypass 时原来仍走默认路径。现在提前返回 prev_cpu

if (bypassing) {
    __scx_add_event(sch, SCX_EV_BYPASS_DISPATCH, 1);
    p->scx.selected_cpu = prev_cpu;
    return prev_cpu;
}

效果:bypass enqueue 不再浪费 CPU 选择开销,也不会用过期的 idle mask。

Patch 3/4:删除 scx_cpumask_to_cmask()

纯删除,无调用者,22 行减掉。

Patch 4/4(v3):cid 表的 RCU 发布模型

这是最关键的改动,从"分配一次、原地填充"变为"每次 enable 私有构建 → RCU 发布 → disable RCU 回收"。

核心数据结构变更

// 旧:裸指针,全局可见
s16 *scx_cid_to_cpu_tbl;
// 新:__rcu 指针,发布后才可读
s16 __rcu *scx_cid_to_cpu_tbl;

// 新增:包表集合
static struct scx_cid_tables *scx_cid_tables; /* 仅 alloc/free 期间使用 */

生命周期

  • Enable 阶段scx_cid_alloc_tables() 分配 struct scx_cid_tables 及 6 个子表,scx_cid_init() 私有填充(所有写入走 tbls->xxx),ops.init_cids() 完成后调用 scx_cid_publish_tables() 通过 rcu_assign_pointer() 逐个发布到 __rcu 全局指针。
  • Disable 阶段scx_cid_retire_tables()scx_enable_mutex + cpus_read_lock() 保护下将所有 __rcu 指针置 NULL,然后 call_rcu() 回收整组表。
  • Reader 端:必须在 RCU read section 内 NULL-check,非 NULL 即为完整且稳定的表。scx_bpf_this_cid()scx_bpf_task_cid() 两个 kfunc 原来缺少 RCU 保护,现已修复。

v3 vs v2 差异:commit message 增加了一段说明——cid kfuncs 对 cid-form 和 cpu-form 调度器都可用(允许渐进迁移),因此每个 root 都构建并发布默认映射。


ASCII 流程图

┌─────────────────────────────────────────────────┐
│            cid 表生命周期 (patch 4/4)           │
├─────────────────────────────────────────────────┤
│                                                 │
│  ENABLE                                         │
│  ┌──────────────┐                               │
│  │ scx_cid_alloc│──► private tbls (不可见)      │
│  │  _tables()   │                               │
│  └──────┬───────┘                               │
│         │ 填充 cid_to_cpu, cpu_to_cid, ...      │
│         │ (全部写入 tbls->xxx, 不碰全局指针)    │
│         ▼                                       │
│  ┌──────────────┐                               │
│  │ops.init_cids │──► 可覆盖默认映射             │
│  └──────┬───────┘                               │
│         │ layout final                           │
│         ▼                                       │
│  ┌──────────────┐                               │
│  │scx_cid_publish│──► rcu_assign_pointer()       │
│  │  _tables()   │     全局 __rcu 指针 ← tbls   │
│  └──────┬───────┘     读者从此可见              │
│         ▼  ═════════════════════════             │
│         ║  RCU grace period barrier ║            │
│         ▼  ═════════════════════════             │
│  ┌──────────────┐                               │
│  │   正常运行   │  reader: rcu_read_lock()      │
│  │              │    → NULL-check → deref        │
│  └──────┬───────┘                               │
│         │ DISABLE                               │
│         ▼                                       │
│  ┌──────────────┐                               │
│  │scx_cid_retire │──► RCU_INIT_POINTER(NULL)    │
│  │  _tables()   │     call_rcu(tbls->rcu, free) │
│  └──────┴───────┘                               │
│                                                 │
└─────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────┐
│        Stall 归咎逻辑 (patch 1/4)               │
├─────────────────────────────────────────────────┤
│                                                 │
│  Task T (owner = Sub-S)                         │
│    stuck on DSQ_D (sched = Ancestor-S)          │
│                                                 │
│  旧: blame T->owner = Sub-S  (错误!)           │
│  新: blame DSQ->sched = Ancestor-S (正确)       │
│                                                 │
│  例外: SCX_DSQ_LOCAL → blame owner              │
│        (local DSQ 由本 CPU 自己消费)            │
│                                                 │
│  Sub-S bypass ─► task routed to                 │
│    Ancestor bypass DSQ ─► Ancestor 应排空       │
│    ─► stall 归咎 Ancestor                       │
│                                                 │
└─────────────────────────────────────────────────┘

概念类比

cid 表竞争 → RCU 发布:想象一家餐厅每天换菜单。旧做法是:厨师直接在墙上公开的菜单板上一边擦一边写,客人路过可能看到半写半擦的混乱内容(未填充条目、撕裂更新)。新做法是:厨师在厨房里(私有 tbls)写好完整的新菜单,确认无误后一次性挂上墙(rcu_assign_pointer),客人从此看到的就是完整的菜单;换菜单时,先从墙上取下(置 NULL),等所有正在看旧菜单的客人看完(RCU grace period)再扔掉旧菜单板。

Stall 归咎:一栋大楼的电梯坏了,住户被困。旧逻辑是:谁住这层就怪谁。但实际负责维修的是大楼物业(祖先 bypass DSQ 的 draining scheduler),而不是住这层的小公司(子调度器)。新逻辑正确地归咎物业。

Bypass CPU 选择:你在绕行模式下开车,导航还在建议"前方路口左转",但你已经被强制分流到绕行专用道了,左转指令完全无用——甚至地图数据可能已过期。新逻辑让导航在绕行模式下直接保持当前路线,不再浪费计算。


Highlight 突出问题

  1. cid kfuncs 的 RCU 保护必须全面审查scx_bpf_this_cid() 原来完全没有 RCU/preempt 保护,scx_bpf_task_cid() 只靠 KF_RCU(对 sleepable 程序不生效)。patch 已修复这两个,但其他 kfunc 是否也存在类似遗漏需要持续检查。
  2. NULL-check 后 deref 的一致性:reader 端必须在 RCU read section 内先 NULL-check 再 deref。任何绕过此模式的路径(如 hotplug callback 依赖 cpus_read_lock() 而非 RCU)都需要确保不会引入新的竞态。
  3. scx_nr_cid_shards 的同步:publish 时才写 scx_nr_cid_shards = tbls->nr_shards,若 reader 在 publish 前读到旧的 scx_nr_cid_shards 与 NULL 表搭配,理论上可能出现不一致;需确认所有 reader 都 gated on scheduler liveness。
  4. Stall 归咎的 READ_ONCE(p->scx.dsq) 安全性:task 的 dsq 指针可能在检查后变更,虽然 watchdog 逻辑是近似检测,但理论上可能出现 dsq 刚切换导致归咎不准确的情况。

版本演进

版本关键改动
v1 → v2v1 的 0003 只加 NULL check,Andrea 和 sashiko-bot 指出 cid 表在填充/原地重写期间仍可被读取。v2 用全新 0004 替换:私有构建 + RCU 发布模型。原 0003 变为死代码删除。0001-0002 不变。
v2 → v3(仅 patch 4)commit message 补充说明:cid kfuncs 对 cid-form 和 cpu-form 调度器都可用(允许渐进迁移),因此每个 root 都构建默认映射。代码逻辑不变。

关联系列

  • Andrea Righi 的原始报告(al3tLtPZZkFjMveK@gpd4)直接催生了 patch 4/4 的重写。
  • 本系列基于 sched_ext/for-7.3 分支,与 sub-scheduler 功能系列紧密相关——stall 归咎和 bypass CPU 选择问题都只在 sub-scheduler 层级场景下才会暴露。

一句话总结

修复 sched_ext 子调度器层级下的 stall 归咎错误和 bypass 期间无效 CPU 选择,并将 cid 映射表从"原地填充全局可见"改为"私有构建 + RCU 发布",消除 BPF kfunc 竞态读取未初始化/撕裂数据的风险。