sched-ext discussion
[PATCH] selftests/sched_ext: Validate select_cpu_and mask constraints
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.c、tools/testing/selftests/sched_ext/allowed_cpus.c |
| Message-ID | 20260906144029.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 内,也没断言边界错误码。本补丁把测试从"打印"升级为"契约校验":
- 给 BPF 程序新增
custom_cpu字段,允许用户态指定要放进自定义 mask 的 CPU; - 用户态覆盖三档场景——空 mask、单点 mask、与亲性不交的 mask;
- 把
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:
- empty mask:
input->custom_cpu = -1,BPF 端不设置任何 bit → 必须返回-EBUSY。 - legal singleton:把 runner 亲和性里的第一个 CPU 填进 mask → 期望返回这个 CPU,但允许
-EBUSY(合法候选也可能被抢)。 - 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_bpfland、scx_rusty、scx_layered等如果之前在 mask 边界上侥幸能跑,现在会暴露出来,回归测试会更稳健。
一句话总结
把 sched_ext 现有的 select_cpu_and 自测从"打印 CPU 号"升级为"对 mask 与亲性的契约断言",新增空 mask、单点 mask、不相交 mask 三档用例,并完成亲和性保存/恢复,是一次典型的 selftest cleanup。