sched-ext discussion
[PATCHSET v3 sched_ext/for-7.3] sched_ext: Fix idle CPU state initialization and validation
LLM 分析
sched_ext: Fix idle CPU state initialization and validation
系列基线信息
| 字段 | 值 |
|---|---|
| 标题 | [PATCHSET v3 sched_ext/for-7.3] sched_ext: Fix idle CPU state initialization and validation |
| 作者 | Andrea Righi (NVIDIA) |
| 版本 | v3 (v1 → v2 → v3) |
| 规模 | 2 patches, 3 files, +78 / -11 |
| Message-ID | 20260731182406.3166853-1-arighi@nvidia.com |
| 来源 | sched-ext |
明确目的
sched_ext 的内置 idle mask 在初始化时将所有 online CPU 标记为 idle,但 idle 状态追踪只有在调度器完全启用后才启动。
这导致两个问题:
- ops.init() 看到错误状态:busy CPU 被错误地报告为 idle,直到该 CPU 下一次 idle 转换才纠正。
- allowed_cpus 自测存在竞态:远程选中的 CPU 可能在 idle-to-idle re-pick 之后被重新标记为 idle,导致验证检查天然不可靠。
本系列通过在 ops.init() 之前启用 idle 追踪、刷新每个 online CPU 的状态来修复第一个问题;通过将自测改为检查本地 CPU 的稳定不变量来修复第二个问题。
遍历代码
Patch 1/2: 初始化 idle mask 在 ops.init() 之前
关键改动一:导出 static key
ext.h中将scx_builtin_idle_enabled从static改为DECLARE_STATIC_KEY_FALSE,使 idle.c 之外也能引用。- 在
__scx_update_idle()的调用条件中增加static_branch_unlikely(&scx_builtin_idle_enabled),使得即使调度器尚未完全启用,idle 追踪也能生效。
关键改动二:__scx_update_idle() 提前返回
void __scx_update_idle(struct rq *rq, bool idle, bool do_notify)
{
/* Idle tracking starts before the scheduler is enabled ... */
if (!scx_enabled())
return; // 不发 ops.update_idle() 通知,但 update_builtin_idle() 已执行
...
}
在 update_builtin_idle() 之后、ops.update_idle() 通知之前插入 if (!scx_enabled()) return;。这样内置 mask 得到更新,但 BPF 侧回调被抑制。
关键改动三:refresh_idle_masks()
static void refresh_idle_masks(void)
{
int cpu;
for_each_online_cpu(cpu) {
struct rq *rq = cpu_rq(cpu);
scoped_guard(rq_lock_irqsave, rq)
update_builtin_idle(cpu, rq->curr == rq->idle);
}
}
在 scx_idle_enable() 中,如果 static key 已启用,调用此函数。逐个 CPU 持 rq lock 检查当前进程是否为 idle,纠正初始的"全 idle"状态。
关键改动四:reset_idle_masks() 注释更新
将"Should converge to the actual state quickly"改为"Seed all online CPUs as idle. refresh_idle_masks() below corrects their state before ops.init() runs.",明确语义。
Patch 2/2: allowed_cpus 自测消除竞态
旧逻辑:validate_idle_cpu() 对远程选中的 CPU 调用 scx_bpf_test_and_clear_cpu_idle(),检查它是否不在 idle mask 中。但 idle-to-idle re-pick 可能在检查前重新将 CPU 标记为 idle。
新逻辑:
validate_local_idle_state():检查本地 CPU 的不变量——如果当前进程不是 idle 进程,但 idle mask 仍标记该 CPU 为 idle,则报错。validate_selected_cpu():仅检查选中 CPU 是否在 allowed domain 和 task affinity 中,不再检查 idle mask。- 在
ops.select_cpu()和ops.enqueue()入口都调用validate_local_idle_state()。 - 使用
scx_bpf_get_idle_cpumask()只读检查,不修改 mask。
ASCII 流程图
旧流程 (v1 之前)
┌──────────────────────────────────────────────┐
│ reset_idle_masks(): all online CPUs = idle │
│ ↓ │
│ ops.init() ← 看到错误: busy CPU 在 idle mask│
│ ↓ │
│ scheduler fully enabled │
│ ↓ │
│ idle tracking starts (首次 idle transition │
│ 才纠正状态) │
└──────────────────────────────────────────────┘
新流程 (v3)
┌──────────────────────────────────────────────┐
│ reset_idle_masks(): seed all online CPUs=idle│
│ ↓ │
│ scx_builtin_idle_enabled = true │
│ ↓ │
│ refresh_idle_masks(): 逐 CPU 持 rq lock │
│ rq->curr == rq->idle ? idle : busy │
│ ↓ │
│ ops.init() ← idle mask 现在准确 │
│ ↓ │
│ scheduler fully enabled │
│ __scx_update_idle() 不再提前 return │
│ ops.update_idle() 通知开始正常发出 │
└──────────────────────────────────────────────┘
allowed_cpus 自测对比
旧 (racy): 新 (stable):
┌─────────────────────┐ ┌─────────────────────┐
│ select_cpu() 选中 CPU │ │ select_cpu() / │
│ ↓ │ │ enqueue() 入口 │
│ test_and_clear_idle()│ │ ↓ │
│ ↓ │ │ validate_local_ │
│ ← re-pick 可重设 idle │ │ idle_state() │
│ → 竞态! │ │ 本地CPU: !idle进程 │
└─────────────────────┘ │ 但 mask=idle → err │
│ (本地CPU不会re-pick) │
└─────────────────────┘
概念类比
想象一个酒店前台系统:酒店开业时,系统默认所有房间都是"空闲"的(reset_idle_masks 全标 idle)。但实际有些房间已经有客人入住了。如果前台在系统正式上线前(ops.init())查看房间状态,会误以为有客人入住的房间是空闲的,导致错误地给新客人分配已有客人的房间。
修复方案:在系统正式上线前,先派工作人员逐个房间敲门确认(refresh_idle_masks() 持 rq lock 检查实际状态),纠正系统中的错误状态。同时,系统正式上线前不给前台发送"房间状态变更通知"(ops.update_idle() 被抑制),等正式上线后通知才正常发出。
自测的修复则是:以前是检查"被选中的房间是否还显示空闲"(远程检查,可能被其他前台抢先更新),改为检查"我现在正在前台值班,如果我不是空闲人员,系统就不应该显示我空闲"(本地不变量,不会被竞态干扰)。
Highlight 突出问题
- Tejun 的替代方案:Tejun 提出可以简单地在加载时清空所有 idle bit(而非设为 idle),等 bypass 模式结束后自然恢复。Andrea 回复认为这也可行,使初始状态保守化。这个方向可能比当前方案更简洁,值得跟进后续讨论。
refresh_idle_masks()的开销:需要逐 CPU 持 rq lock,在 CPU 数量很多的系统上可能引入延迟。不过这只在 scheduler 启用时执行一次,影响有限。- static key 的语义变化:
scx_builtin_idle_enabled原来仅在调度器完全启用后才为 true,现在提前启用。任何依赖此 key 与scx_enabled()等价的代码可能需要审查。 - 自测的覆盖范围:新的本地不变量只验证"正在运行的 CPU 不应被标记为 idle",但未验证远程 CPU idle 状态的正确性。Patch 1 修复了核心问题,但自测并未直接验证远程 CPU 的 idle 准确性。
版本演进
| 版本 | 关键改动 |
|---|---|
| v1 | 将 idle mask 初始化移到 selftest 中;引入专用 idle-tracking static key |
| v2 | 将 idle mask 初始化从 selftest 移入 sched_ext core;重做 allowed_cpus 自测使用本地不变量 |
| v3 | 复用已有 scx_builtin_idle_enabled static key(不再引入新 key);在 ops.select_cpu() 和 ops.enqueue() 都检查本地不变量;只读检查 idle mask,不修改 |
与其他相关 patch 系列的关联
- 本系列依赖 Kuba Piecuch 的设计建议(Suggested-by),v2 和 v3 的核心改动均来自 Kuba 的反馈。
- 与 sched_ext bypass mode 机制紧密相关:bypass 模式退出时通过 RENOTIFY + kick 机制更新 idle 状态,这是 Tejun 提出替代方案的基础。
一句话总结
在 sched_ext 启用前提前启用 idle 追踪并刷新每个 CPU 的实际状态,修复 idle mask 初始值不准确的问题,同时将 allowed_cpus 自测从竞态的远程检查改为稳定的本地不变量。