0/1 已展开

LLM 分析

selftests/sched_ext:强化 select_cpu_and 掩码约束验证

系列概况

字段内容
标题[PATCH] selftests/sched_ext: Validate select_cpu_and mask constraints
作者Tianyi Chen hi@tychen.cc
版本v1(单 patch,patch header 无版本号)
规模2 文件改动,86 行新增 / 15 行删除
修改文件tools/testing/selftests/sched_ext/allowed_cpus.bpf.ctools/testing/selftests/sched_ext/allowed_cpus.c
Message-ID20260906144029.848978-1-hi@tychen.cc
完整性commit message、Assisted-by、Signed-off-by、diff stats 完整可解析;diff 中插入点无破损

补丁目的

scx_bpf_select_cpu_and 是 sched_ext 子系统暴露给 BPF 调度器的"挑 CPU"入口,承诺的契约是:返回的 CPU 必须同时落在调用方传入的自定义 cpumask 任务的 cpus_ptr 内。

现有 selftest 只调用一次 BPF_PROG_TEST_RUN、把返回 CPU 号打印一下就结束,既没断言返回值是否真的落在 mask 内,也没断言边界错误码。本补丁把测试从"打印"升级为"契约校验":

  1. 给 BPF 程序新增 custom_cpu 字段,允许用户态指定要放进自定义 mask 的 CPU;
  2. 用户态覆盖三档场景——空 mask、单点 mask、与亲性不交的 mask;
  3. attr.retval 当作有符号 32 位解释,强制 select_cpu_and 返回的 CPU 必须落在自定义 mask 与任务亲和 mask 内,否则返回 -ERANGE

旧流程的问题

旧 selftest 主体大致是:

args[0] = getpid();
/* BPF_PROG_TEST_RUN ... */
fprintf(stderr, "%s: CPU %d\n", __func__, attr.retval);

问题点:

  • 单一 happy path("挑一个 idle CPU"),没有任何错误注入或边界用例。
  • mask 完全是配角:BPF 端调用 scx_bpf_select_cpu_and 时传的 p->cpus_ptr 与自定义 mask 没有区别,也没有对返回值做 mask 校验。
  • 没有动任务 cpus_ptr/用户态亲和性,无法覆盖"任务亲和与自定义 mask 不相交"这一关键契约。
  • attr.retval 类型是 __u64,但 BPF 可能返回负 errno;旧代码用 %d 打印能凑合看到负数,却没有断言错误码本身。

新流程

新流程在 run() 里依次跑三档用例,跑完统一恢复亲和性、检查 uei.kind

  1. empty maskinput->custom_cpu = -1,BPF 端不设置任何 bit → 必须返回 -EBUSY
  2. legal singleton:把 runner 亲和性里的第一个 CPU 填进 mask → 期望返回这个 CPU,但允许 -EBUSY(合法候选也可能被抢)。
  3. disjoint masks:把当前任务绑到 first,让 BPF 去选 second → 任务亲和与自定义 mask 不相交,必须返回 -EBUSY
+------------------+
|   attach ops     |
|   sleep(1)       |
+------------------+
        |
        v
+------------------+   expect: -EBUSY
|  empty mask      | ----------------------+
|  custom_cpu = -1 |                       |
+------------------+                       |
        |                                  |
        v                                  |
+--------------------+   allow busy or     |
| legal singleton    | -- expect_cpu ----> |
| custom_cpu = first |                      |
+--------------------+                      |
        |                                  |
        v                                  |
+-----------------------+ expect: -EBUSY   |
| disjoint masks        | ------------------+
| pin to first,         |
| custom_cpu = second   |
+-----------------------+
        |
        v
+----------------------+   EXIT_KIND(NONE) ? -> PASS
| restore affinity     | ----------------------+
| check uei.kind       |                        |
+----------------------+                        |
        |                                       |
        v                                       v
+----------------------+                +----------------------+
|  PASS / FAIL         | <-------------- | SCX_TEST_PASS/FAIL   |
+----------------------+                +----------------------+

Patch 概览

只动 selftests 目录的 2 个文件,kernel 侧代码完全不动:

  • allowed_cpus.bpf.c:扩展 syscall handler,新增 cpumask 构造 + 返回值 mask 校验。
  • allowed_cpus.c:把单次 happy path 重构成"三档断言 + 亲和性保存/恢复",并把 retval 当作有符号解释。

关键实现

BPF 侧:构造自定义 mask 并断言返回

mask = bpf_cpumask_create();
if (!mask) {
    bpf_task_release(p);
    return -ENOMEM;
}

/* A negative custom_cpu leaves the custom mask empty. */
if (input->custom_cpu >= 0)
    bpf_cpumask_set_cpu(input->custom_cpu, mask);

cpu = scx_bpf_select_cpu_and(p, bpf_get_smp_processor_id(), 0,
                             cast_mask(mask), 0);
if (cpu >= 0 &&
    (!bpf_cpumask_test_cpu(cpu, cast_mask(mask)) ||
     !bpf_cpumask_test_cpu(cpu, p->cpus_ptr)))
    cpu = -ERANGE;

bpf_cpumask_release(mask);
return cpu;

要点:

  • bpf_cpumask_create() 在 BPF 上下文里拿到 RCU 安全的 cpumask;失败直接 -ENOMEM 并释放 task。
  • custom_cpu = -1 是"显式空 mask"的约定,把"用户忘了填"与"故意留空"区分开。
  • 返回值二次校验:必须同时在 mask 与 cpus_ptr 里,否则改写为 -ERANGE,把契约用代码钉死。

用户态:三档断言 + 亲和性管理

if (test_select_cpu_from_user(skel, "empty mask", -1, true))
    goto out;

/* A legal candidate may be busy; selection need not succeed. */
if (test_select_cpu_from_user(skel, "legal candidate", first, false))
    goto out;

if (second >= 0) {
    CPU_ZERO(&pinned);
    CPU_SET(first, &pinned);
    if (sched_setaffinity(0, sizeof(pinned), &pinned)) { ... }
    if (test_select_cpu_from_user(skel, "disjoint masks", second, true))
        goto restore;
} else {
    fprintf(stderr, "Skipping disjoint masks: need two allowed CPUs\n");
}

断言函数里把 attr.retval 强转 __s32,按 expect_busy 决定期望:

cpu = (__s32)attr.retval;
if ((expect_busy && cpu != -EBUSY) ||
    (!expect_busy && cpu != -EBUSY && cpu != custom_cpu)) {
    SCX_ERR("%s: unexpected CPU selection result %d", name, cpu);
    return -EINVAL;
}

restore: 标签里再 sched_setaffinity 把亲和性还原,最后比对 uei.kind == EXIT_KIND(SCX_EXIT_NONE) 才算 PASS。

类比

把它想成餐厅选座:BPF 程序是领位员,scx_bpf_select_cpu_and 是"请把这位客人领到这张桌"的请求。旧测试只请领位员领一次,把桌号抄下来就算完事。新测试发三种刁钻任务:

  • 桌子清单是空的 → 领位员必须说"无座可领"(-EBUSY)。
  • 清单只有 3 号桌,但客人恰好被别的服务员占了 → 允许领位员说"忙"或者真的把 3 号桌让出来。
  • 清单只有 5 号桌,可客人限定只能在 3 号 → 必须说"无座可领"(mask 与亲和不交)。

而且测试自己会先把 runner 原本能在哪些桌坐记下来,跑完再恢复,免得影响下一次运行。

Highlight:风险与注意点

  • p->cpus_ptr 必须存在bpf_cpumask_test_cpu(cpu, p->cpus_ptr) 假定非空;如果某条调度路径上 cpus_ptr 临时为 NULL(例如 hotplug 窗口),会触发 verifier 报错或运行期 panic,需要 kernel 侧加 NULL 检查或在 BPF 端用 bpf_core_field_exists 探测。
  • bpf_cpumask_create() 失败被静默:目前 ENOMEM 只 print errno 不退出,可能让后面的用例无声失败,建议补 SCX_FAIL_IF
  • RCU 临界区scx_bpf_select_cpu_and 内部会 rcu_read_lock,bpf_cpumask_release(mask) 必须在所有 rcu 临界区外;当前顺序调用安全,但若未来并发使用同一 mask 要小心 release 顺序。
  • 单 CPU 机器 / hotunplug:disjoint 用例已通过 if (second < 0) 跳过,但 first < 0 必须存在,否则直接 FAIL——这一点是测试应有的硬约束,符合预期。
  • 不动 kernel 实际逻辑:本补丁不改 select_cpu_and 的实现,只是把它的契约用测试钉死。如果有 BPF 调度器依赖了"不被 mask 校验保护的边角行为",会被这里卡住,这是符合预期的"约束收紧",应在 release notes 点一下。
  • 对 sched-ext 生态的影响:selftests 现在更严,scx_bpflandscx_rustyscx_layered 等如果之前在 mask 边界上侥幸能跑,现在会暴露出来,回归测试会更稳健。

一句话总结

把 sched_ext 现有的 select_cpu_and 自测从"打印 CPU 号"升级为"对 mask 与亲性的契约断言",新增空 mask、单点 mask、不相交 mask 三档用例,并完成亲和性保存/恢复,是一次典型的 selftest cleanup。