0/5 已展开

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-ID20260803060930.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 完全启用之后才开始。

这导致两个问题:

  1. 误报 busy CPU 为 idle:在 ops.init() 到首次 idle transition 之间,忙碌的 CPU 会被错误地广告为 idle。
  2. 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 突出问题

  1. 初始化延迟:mask 初始为空意味着在 bypass 移除、re-notification 完成之前,真正 idle 的 CPU 不会被选到。这段时间内调度器可能做出次优的 idle selection。虽然收敛很快,但在高 CPU 数系统上值得观察。
  2. NUMA per-node mask:patch 对 per-node idle mask 也做了同样的 clear 操作,但 per-node 的 re-notification 填充路径是否同样可靠,需要确认。
  3. selftest 不变量的范围:新不变量只检查"本地非 idle 上下文不能被标记为 idle",但不检查远端 CPU 的 idle 状态正确性。如果远端 CPU 被误标为 idle,这个 test 无法捕获。
  4. 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。