sched-ext discussion
[PATCHSET v2 sched_ext/for-7.3] sched_ext: Sub-scheduler and cid fixes
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-ID | 20260721223118.2268529-1-tj@kernel.org |
| 来源 | sched-ext |
| 目标分支 | sched_ext/for-7.3 |
明确目的
本系列修复 sched_ext 子调度器(sub-scheduler)和 cid(core-id)映射表的四个问题:
- Runnable stall 责任归属错误——子调度器 bypass 期间,任务停在其祖先的 bypass DSQ 上,watchdog 却把 stall 归咎于子调度器而非真正负责排空该 DSQ 的祖先,导致退出信息不可操作。
- Bypass 期间无效 CPU 选择——bypass 模式下
select_task_rq_scx()走默认路径调用scx_select_cpu_dfl(),但选出的 CPU 会被 bypass enqueue 丢弃;且自定义 idle tracking 的调度器会让内置 idle cpumask 冻结,选择结果不可靠。 - 删除死代码——
scx_cpumask_to_cmask()无任何调用者。 - 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 突出问题
- cid kfuncs 的 RCU 保护必须全面审查:
scx_bpf_this_cid()原来完全没有 RCU/preempt 保护,scx_bpf_task_cid()只靠KF_RCU(对 sleepable 程序不生效)。patch 已修复这两个,但其他 kfunc 是否也存在类似遗漏需要持续检查。 - NULL-check 后 deref 的一致性:reader 端必须在 RCU read section 内先 NULL-check 再 deref。任何绕过此模式的路径(如 hotplug callback 依赖
cpus_read_lock()而非 RCU)都需要确保不会引入新的竞态。 scx_nr_cid_shards的同步:publish 时才写scx_nr_cid_shards = tbls->nr_shards,若 reader 在 publish 前读到旧的scx_nr_cid_shards与 NULL 表搭配,理论上可能出现不一致;需确认所有 reader 都 gated on scheduler liveness。- Stall 归咎的
READ_ONCE(p->scx.dsq)安全性:task 的 dsq 指针可能在检查后变更,虽然 watchdog 逻辑是近似检测,但理论上可能出现 dsq 刚切换导致归咎不准确的情况。
版本演进
| 版本 | 关键改动 |
|---|---|
| v1 → v2 | v1 的 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 竞态读取未初始化/撕裂数据的风险。