sched-ext discussion
[PATCHSET v4 sched_ext/for-7.3] sched_ext: Fix idle CPU state initialization and validation
LLM 分析
sched_ext: 修复 idle CPU 状态初始化与验证
系列基线信息
| 字段 | 内容 |
|---|---|
| 标题 | sched_ext: Fix idle CPU state initialization and validation |
| 作者 | Andrea Righi (NVIDIA) |
| 版本 | v4 |
| 规模 | 2 patches, 2 files, +50/-15 |
| Message-ID | 20260803060930.4111746-1-arighi@nvidia.com |
| 来源 | sched-ext |
| 状态 | 已由 Tejun Heo 合入 sched_ext/for-7.3 |
明确目的
sched_ext 的内置 idle mask 在 scheduler 启用时将所有 online CPU 标记为 idle,但 idle tracking 实际上在 scheduler 完全启用之后才开始。
这导致两个问题:
- 误报 busy CPU 为 idle:在
ops.init()到首次 idle transition 之间,忙碌的 CPU 会被错误地广告为 idle。 - selftest 竞态:
allowed_cpus自测检查远端选中的 CPU 是否从 idle mask 中消失,但 idle-to-idle re-pick 可能在检查之前重新将该 CPU 标记为 idle,导致验证本身是竞态的。
修复方案:将 idle mask 初始化为空(保守策略),并重构 selftest 使用稳定的本地不变量。
遍历代码
Patch 1/2: kernel/sched/ext/idle.c — Initialize idle masks as busy
核心改动在 reset_idle_masks() 函数:
改动前:用 cpumask_copy() 将 idle mask 设为所有 online CPU —— 即默认所有 CPU 都是 idle。
// 旧代码
cpumask_copy(idle_cpumask(NUMA_NO_NODE)->cpu, cpu_online_mask);
cpumask_copy(idle_cpumask(NUMA_NO_NODE)->smt, cpu_online_mask);
// per-node 同理
cpumask_and(idle_cpumask(node)->cpu, cpu_online_mask, node_mask);
改动后:用 cpumask_clear() 将 idle mask 清空 —— 即默认所有 CPU 都是 busy。
// 新代码
cpumask_clear(idle_cpumask(NUMA_NO_NODE)->cpu);
cpumask_clear(idle_cpumask(NUMA_NO_NODE)->smt);
// per-node 同理
cpumask_clear(idle_cpumask(node)->cpu);
填充机制:当 bypass 被移除后,现有 idle re-notification 路径会重新调度每个 online CPU。idle-to-idle re-pick 将真正 idle 的 CPU 填入 mask。后续的 idle transition 持续保持 mask 更新。
Patch 2/2: tools/testing/selftests/sched_ext/allowed_cpus.bpf.c — Race-free validation
旧逻辑:validate_idle_cpu() 检查远端选中的 CPU 是否已从 idle mask 中移除(scx_bpf_test_and_clear_cpu_idle())。这是竞态的 —— idle-to-idle re-pick 可能在检查前重新标记该 CPU 为 idle。
新逻辑:引入 validate_local_idle_state(),检查本地不变量:
static void validate_local_idle_state(void)
{
s32 cpu = bpf_get_smp_processor_id();
bool cpu_is_idle, curr_is_idle;
// 获取当前 CPU 上运行的 task,判断是否为 idle task
bpf_rcu_read_lock();
curr = scx_bpf_cpu_curr(cpu);
curr_is_idle = curr && (curr->flags & PF_IDLE);
bpf_rcu_read_unlock();
// 读取 idle mask(不清除)
idle = scx_bpf_get_idle_cpumask();
cpu_is_idle = bpf_cpumask_test_cpu(cpu, idle);
scx_bpf_put_idle_cpumask(idle);
// 不变量:非 idle 调度上下文 → 不能被标记为 idle
if (!curr_is_idle && cpu_is_idle)
scx_bpf_error("running CPU %d should be marked as busy", cpu);
}
关键差异:
- 只读 idle mask,不修改(
scx_bpf_get_idle_cpumask+bpf_cpumask_test_cpu,而非scx_bpf_test_and_clear_cpu_idle) - 检查本地 CPU,不检查远端 CPU —— 本地 CPU 在回调执行期间不会经历 idle re-pick
- 在
ops.select_cpu()和ops.enqueue()两个回调中都调用
同时 validate_selected_cpu() 简化为只检查 selected CPU 是否在 allowed domain 和 task affinity 内。
ASCII 流程图
旧流程 (v1-v3) 新流程 (v4)
┌──────────────────────┐ ┌──────────────────────┐
│ reset_idle_masks() │ │ reset_idle_masks() │
│ mask = all_online │ │ mask = empty (busy) │
└──────────┬───────────┘ └──────────┬───────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ ops.init() 运行 │ │ ops.init() 运行 │
│ mask 含 busy CPU ❌ │ │ mask 为空,无误报 ✅ │
└──────────┬───────────┘ └──────────┬───────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ bypass 移除 │ │ bypass 移除 │
│ idle transition 更新 │ │ re-notification │
│ mask 逐个收敛 │ │ idle re-pick 填充 │
└──────────────────────┘ │ 实际 idle CPU ✅ │
└──────────────────────┘
selftest 旧验证 (竞态) selftest 新验证 (稳定)
┌─────────────────────┐ ┌─────────────────────────┐
│ select_cpu → 远端CPU │ │ select_cpu / enqueue │
│ 检查远端CPU不在idle │ │ 检查本地CPU不变量 │
│ ↓ │ │ ↓ │
│ idle re-pick 可能 │ │ 本地CPU在回调中运行 │
│ 重新标记为idle ❌ │ │ 不会经历re-pick ✅ │
└─────────────────────┘ └─────────────────────────┘
概念类比
想象一个酒店前台系统:
- 旧方案:每天早上开门时,前台系统把所有房间标记为"空闲"(all online = idle)。但实际有些房间已经有客人住了(busy CPU),系统却错误地显示为空闲,导致新客人被分配到已有客人的房间。
- 新方案:每天早上开门时,前台系统把所有房间标记为"占用"(empty = busy)。这是保守但安全的 —— 不会把已有客人的房间错误分配。当保洁人员确认房间确实空了(idle re-pick / re-notification),才将状态更新为空闲。虽然一开始可能漏掉一些真正空闲的房间,但很快会收敛到正确状态。
Highlight 突出问题
- 初始化延迟:mask 初始为空意味着在 bypass 移除、re-notification 完成之前,真正 idle 的 CPU 不会被选到。这段时间内调度器可能做出次优的 idle selection。虽然收敛很快,但在高 CPU 数系统上值得观察。
- NUMA per-node mask:patch 对 per-node idle mask 也做了同样的 clear 操作,但 per-node 的 re-notification 填充路径是否同样可靠,需要确认。
- selftest 不变量的范围:新不变量只检查"本地非 idle 上下文不能被标记为 idle",但不检查远端 CPU 的 idle 状态正确性。如果远端 CPU 被误标为 idle,这个 test 无法捕获。
- v3→v4 策略变化:v3 是在
ops.init()之前就启用 idle tracking 并在 rq lock 下刷新每个 CPU;v4 改为空初始化 + 依赖 re-notification。后者更简洁,但依赖 bypass 机制的正确性。
版本演进
| 版本 | 关键改动 |
|---|---|
| v1 | 将 idle mask 初始化从 selftest 移到 sched_ext core;引入 dedicated idle-tracking static key;重构 allowed_cpus selftest 使用本地不变量 |
| v2 | (链接中未详述,主要基于 Kuba Piecuch 反馈调整) |
| v3 | 复用 built-in idle-selection static key 而非引入专用 key;从 ops.select_cpu() 和 ops.enqueue() 两处检查本地 CPU-idle 不变量;只读 idle mask 不修改;在 scx_bpf_select_cpu_and() 之前检查 |
| v4 | 核心策略变化:idle mask 初始化为空(而非 v3 的提前启用 idle tracking + rq lock 下刷新);依赖现有 bypass idle re-notification 路径填充 mask(Tejun Heo 建议) |
与其他相关 patch 系列的关联
- 本系列直接修复 sched_ext built-in idle selection 的正确性问题,与
scx_bpf_select_cpu_and()、scx_bpf_test_and_clear_cpu_idle()等 BPF API 的语义紧密相关。 - Kuba Piecuch 在 v1→v3 的 review 中深度参与,提出了多个改进建议。
- Tejun Heo 的 v4 建议推动了核心策略转变(从"提前启用 tracking"到"空初始化 + re-notification")。
一句话总结
将 sched_ext idle mask 初始化为空以避免 busy CPU 被误报为 idle,并重构 selftest 使用稳定的本地不变量消除竞态,已被合入 for-7.3。