sched-ext discussion
[PATCH] sched_ext: Skip per-CPU data allocation for built-in DSQs
LLM 分析
sched_ext:跳过内置 DSQ 的 per-CPU 数据分配
系列概况
- 标题:
[PATCH] sched_ext: Skip per-CPU data allocation for built-in DSQs - 作者:Qiurong Fang fangqiurong@kylinos.cn
- 版本:v1(2026-08-27)、v2(2026-09-01),最终被 Tejun Heo 合入 sched_ext/for-7.4
- 规模:单 patch、两个版本;涉及 1~2 个文件
- 修改文件:
kernel/sched/ext/ext.c、include/linux/sched/ext.h(仅 v2) - 代码统计:v1
+7/-0(1 file);v2+15/-7(2 files) - Message-ID:
20260827082329.3368255-1-fangqiurong@kylinos.cn - 完整性:完整,包含 v1、两轮 review、v2、Reviewed-by、应用确认
补丁目的
sed_ext 的 DSQ(dispatch queue)在 scx_init_dsq() 中无条件为每个 DSQ 分配 nr_cpu_ids * sizeof(struct scx_dsq_pcpu) 字节的 per-CPU 内存,用于支撑 schedule_dsq_reenq() 的 deferred reenqueue 跟踪。但这条路径只对 user DSQ 有意义——LOCAL DSQ 走的是 sch->pcpu->deferred_reenq_local,其他内置 DSQ(SCX_DSQ_BYPASS/LOCAL/REJECT/RESCUE)甚至会被 schedule_dsq_reenq() 显式拒绝。
更糟的是:除了 SCX_DSQ_GLOBAL 是单例外,其他四个内置 DSQ 大多在 for_each_possible_cpu() 循环里初始化(ext.c:7172/8681/8683、sub.c 中的 RESCUE 是 per-rq)。换句话说:
- 每个内置 DSQ 都按
nr_cpu_ids份分配 per-CPU 数据 - 内置 DSQ 本身的个数也按
nr_cpu_ids增长 - 总体浪费是 O(nr_cpu_ids²) 级别
补丁的目标就是把这块永远不会读到的死内存省掉,并在 v2 中把字段名从 pcpu 改成 pcpu_user,让"per-CPU 数据只属于 user DSQ"这件事在代码里更直白。
旧流程的问题
s32 scx_init_dsq(struct scx_dispatch_q *dsq, u64 dsq_id, struct scx_sched *sch)
{
dsq->id = dsq_id;
dsq->sched = sch;
dsq->pcpu = alloc_percpu(struct scx_dsq_pcpu); // 不论是否内置都分配
if (!dsq->pcpu)
return -ENOMEM;
for_each_possible_cpu(cpu) {
struct scx_dsq_pcpu *pcpu = per_cpu_ptr(dsq->pcpu, cpu);
struct scx_deferred_reenq_user *dru = &pcpu->deferred_reenq_user;
...
}
}
static void exit_dsq(struct scx_dispatch_q *dsq)
{
for_each_possible_cpu(cpu) { // 内置 DSQ 这里空跑
struct scx_dsq_pcpu *pcpu = per_cpu_ptr(dsq->pcpu, cpu);
...
}
free_percpu(dsq->pcpu);
}
问题点:
- 死内存:内置 DSQ 的
pcpu永远没被读。schedule_dsq_reenq()在ext.c:1130的唯一读取点,已经被!(dsq->id & SCX_DSQ_FLAG_BUILTIN)包住。 - 二次方浪费:内置 DSQ 数量 × per-CPU 份数 ≈
nr_cpu_ids²,在 1024 CPU 机器上是百万级实例。 - 误导性字段名:
pcpu让人误以为所有 DSQ 都真有 per-CPU 数据,但实际只有 user DSQ 有。
新流程
s32 scx_init_dsq(struct scx_dispatch_q *dsq, u64 dsq_id, struct scx_sched *sch)
{
/* Deferred reenqueue tracking is only supported for user DSQs */
if (dsq_id & SCX_DSQ_FLAG_BUILTIN) // 内置直接跳过
return 0;
dsq->sched = sch;
dsq->pcpu_user = alloc_percpu(struct scx_dsq_pcpu); // 改名 + 仅 user DSQ 分配
...
}
static void exit_dsq(struct scx_dispatch_q *dsq)
{
if (!dsq->pcpu_user) // 内置 DSQ 没有 pcpu
return;
...
free_percpu(dsq->pcpu_user);
}
安全性由三件事兜住:
scx_init_dsq()先 memset 整个 dsq,再设id/sched,新加的 early return 只跳过 per-CPU 子结构,不会让其他初始化步骤缺位(Zhan 在 v2 review 中明确点出)。- 唯一 reader(
ext.c:1130)已被!SCX_DSQ_FLAG_BUILTIN保护。 exit_dsq()必须显式判!dsq->pcpu_user再走,否则per_cpu_ptr(NULL, cpu)会交给list_empty()读,越野指针。
Patch 概览
v1 仅修改 kernel/sched/ext/ext.c,加两段 early return;v2 额外:
include/linux/sched/ext.h:struct scx_dispatch_q::pcpu→pcpu_userkernel/sched/ext/ext.c:所有引用点同步改名(schedule_dsq_reenq的 reader、scx_init_dsq的分配、exit_dsq的遍历与释放)
关键实现
/* ext.c scx_init_dsq() 新增 early return */
if (dsq_id & SCX_DSQ_FLAG_BUILTIN)
return 0;
/* ext.c exit_dsq() 新增早返 */
if (!dsq->pcpu_user)
return;
/* ext.h 字段重命名 */
struct scx_dsq_pcpu __percpu *pcpu_user;
逻辑骨架用 ASCII 表达:
scx_init_dsq(dsq, dsq_id, sch)
|
dsq_id & SCX_DSQ_FLAG_BUILTIN ?
/ \
yes no
| |
return 0 alloc pcpu_user
(no per-CPU for_each_cpu init dru
data) |
v
return 0 (success)
exit_dsq(dsq)
|
dsq->pcpu_user ?
/ \
NULL not NULL
| |
return; walk pcpu_user, free_percpu()
DSQ 类型与分配关系:
+--------------------+--------------------------+------------------+
| DSQ id | init site | pcpu_user alloc? |
+--------------------+--------------------------+------------------+
| SCX_DSQ_GLOBAL | single instance | no (BUILTIN) |
| SCX_DSQ_LOCAL | for_each_possible_cpu | no (BUILTIN) |
| SCX_DSQ_BYPASS | for_each_possible_cpu | no (BUILTIN) |
| SCX_DSQ_REJECT | for_each_possible_cpu | no (BUILTIN) |
| SCX_DSQ_RESCUE | per rq, scx_rescue_init | no (BUILTIN) |
| user DSQs | BPF / userland | YES |
+--------------------+--------------------------+------------------+
total saved ~= (nr_builtin_ids_per_cpu) * nr_cpu_ids * sizeof(scx_dsq_pcpu)
~= O(nr_cpu_ids^2) * sizeof(scx_dsq_pcpu)
读取路径与本补丁的耦合:
schedule_dsq_reenq(sch, dsq, rq)
|
dsq->id & SCX_DSQ_FLAG_BUILTIN ?
/ \
yes no
| |
return; (or use sch->pcpu dsq_pcpu = per_cpu_ptr(dsq->pcpu_user, cpu_of(rq))
for LOCAL only) use deferred_reenq_user
类比
把 DSQ 类比成图书馆里的一排桌子:每张桌子原本都自带一个小抽屉,用来放"延期归还登记本"(deferred reenqueue)。但专用阅览桌(built-in DSQ)从来不用这个抽屉——它们有自己统一的登记台(sch->pcpu->deferred_reenq_local)。
现在的修法就是:给专用阅览桌把抽屉拆掉、把抽屉的标签从"每桌抽屉"改成"仅普通桌抽屉"(pcpu → pcpu_user)。1000 张专用桌 × 每桌 1000 份登记表 = 100 万份废纸,一次清掉。
Highlight:风险与注意点
- changelog 措辞要精确:v1 写的是
nr_cpu_ids * sizeof(...)的线性描述,Zhan 立刻指出这是nr_cpu_ids²的二次方节省;v2 已改。 Fixes:标签的边界:v1 加了Fixes: 30b0515342db,会触发 stable 回移植;v2 移除,因为这是纯内存节省、没有任何正确性影响,不需要进 stable 树。exit_dsq()不能省略 NULL 检查:per_cpu_ptr(NULL, cpu)仍会返回一个偏移指针,list_empty()实际会读它——所以必须有if (!dsq->pcpu_user) return;,只靠free_percpu(NULL)兜不住。- 字段名重命名的语义价值:仅 user DSQ 才持有 per-CPU 块,
pcpu_user让这个不变式在类型层面可读;后续读代码的人不会再误以为"所有 DSQ 都有 pcpu"。 - 合入说明的措辞:Tejun 把
scx_init_dsq()的注释从"not supported for built-in DSQ"改成了"local DSQ does track deferred reenqs, just through sch->pcpu"——更准确,避免读者以为"内置 DSQ 一概不支持 reenq"。
版本变化
| 维度 | v1 | v2 |
|---|---|---|
| 修改文件 | kernel/sched/ext/ext.c | + include/linux/sched/ext.h |
| 代码行 | +7/-0 | +15/-7 |
| 字段名 | dsq->pcpu | dsq->pcpu_user |
Fixes: tag | 有(指向 30b0515342db) | 移除(无 correctness 影响) |
| Changelog 措辞 | 线性描述浪费 | 改为二次方描述 |
scx_init_dsq 注释 | "only supported for user DSQs" | 进一步说明 LOCAL 仍走 sch->pcpu |
| Review tag | 无 | Reviewed-by: Zhan Xusheng |
| 状态 | 待 review | 已合入 sched_ext/for-7.4 |
一句话总结
为 sched_ext 中所有 built-in DSQ 跳过 pcpu_user 分配,把 nr_cpu_ids² 级别的死内存释放掉,并把字段名改为更直白的 pcpu_user。