0/2 已展开

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.cinclude/linux/sched/ext.h(仅 v2)
  • 代码统计:v1 +7/-0(1 file);v2 +15/-7(2 files)
  • Message-ID20260827082329.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);
}

问题点:

  1. 死内存:内置 DSQ 的 pcpu 永远没被读。schedule_dsq_reenq()ext.c:1130 的唯一读取点,已经被 !(dsq->id & SCX_DSQ_FLAG_BUILTIN) 包住。
  2. 二次方浪费:内置 DSQ 数量 × per-CPU 份数 ≈ nr_cpu_ids²,在 1024 CPU 机器上是百万级实例。
  3. 误导性字段名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);
}

安全性由三件事兜住:

  1. scx_init_dsq() 先 memset 整个 dsq,再设 id/sched,新加的 early return 只跳过 per-CPU 子结构,不会让其他初始化步骤缺位(Zhan 在 v2 review 中明确点出)。
  2. 唯一 reader(ext.c:1130)已被 !SCX_DSQ_FLAG_BUILTIN 保护。
  3. 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.hstruct scx_dispatch_q::pcpupcpu_user
  • kernel/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)。

现在的修法就是:给专用阅览桌把抽屉拆掉、把抽屉的标签从"每桌抽屉"改成"仅普通桌抽屉"(pcpupcpu_user)。1000 张专用桌 × 每桌 1000 份登记表 = 100 万份废纸,一次清掉。

Highlight:风险与注意点

  1. changelog 措辞要精确:v1 写的是 nr_cpu_ids * sizeof(...) 的线性描述,Zhan 立刻指出这是 nr_cpu_ids² 的二次方节省;v2 已改。
  2. Fixes: 标签的边界:v1 加了 Fixes: 30b0515342db,会触发 stable 回移植;v2 移除,因为这是纯内存节省、没有任何正确性影响,不需要进 stable 树。
  3. exit_dsq() 不能省略 NULL 检查per_cpu_ptr(NULL, cpu) 仍会返回一个偏移指针,list_empty() 实际会读它——所以必须有 if (!dsq->pcpu_user) return;,只靠 free_percpu(NULL) 兜不住。
  4. 字段名重命名的语义价值:仅 user DSQ 才持有 per-CPU 块,pcpu_user 让这个不变式在类型层面可读;后续读代码的人不会再误以为"所有 DSQ 都有 pcpu"。
  5. 合入说明的措辞:Tejun 把 scx_init_dsq() 的注释从"not supported for built-in DSQ"改成了"local DSQ does track deferred reenqs, just through sch->pcpu"——更准确,避免读者以为"内置 DSQ 一概不支持 reenq"。

版本变化

维度v1v2
修改文件kernel/sched/ext/ext.c+ include/linux/sched/ext.h
代码行+7/-0+15/-7
字段名dsq->pcpudsq->pcpu_user
Fixes: tag有(指向 30b0515342db)移除(无 correctness 影响)
Changelog 措辞线性描述浪费改为二次方描述
scx_init_dsq 注释"only supported for user DSQs"进一步说明 LOCAL 仍走 sch->pcpu
Review tagReviewed-by: Zhan Xusheng
状态待 review已合入 sched_ext/for-7.4

一句话总结

为 sched_ext 中所有 built-in DSQ 跳过 pcpu_user 分配,把 nr_cpu_ids² 级别的死内存释放掉,并把字段名改为更直白的 pcpu_user