0/1 已展开

LLM 分析

sched/numa:失败重建时释放部分构建的 NUMA 拓扑

系列概况

-标题:[PATCH] sched/numa: Free partial topology after a failed rebuild

  • 作者:Guopeng Zhang <zhangguopeng@kylinos.cn>
  • 版本:v1(单 patch,无 -vN 后缀)
  • 规模:1 file, +39 / −30,共 69 行变动
  • 修改文件:kernel/sched/topology.c
  • 代码统计:新增一个 static helper sched_free_numa_masks()sched_init_numa() 全面改造错误路径;sched_reset_numa() 复用 helper
  • Message-ID:20260718095053.1256186-1-guopeng.zhang@linux.dev
  • 完整性:邮件仅含一个 patch,body 文本与 diff 完整可解析,无后续回复

补丁目的

sched_init_numa() 在 NUMA 拓扑重建过程中分多步分配对象:距离数组、层指针表、每层 cpumask 数组、每个节点内的 cpumask。只要中间任何一次 kzalloc 失败,函数就 return 走人,前几步已经分配出来的对象和 tl(topology level)全部泄漏

这个重建路径每次 CPU 热插拔把首个 CPU 加进 node、或把最后一个 CPU 拿走时都会触发。长时间运行 + 高频热插拔 + 内存压力 三者叠加,会把内核 slab 一点点吃光。

补丁目标:把所有处于“半成品”状态的对象都放进局部变量,等所有分配全部成功再通过 rcu_assign_pointer 发布到全局;任一分配失败,统一 goto free 走清理分支。

旧流程的问题

旧流程里关键时序是:

  1. 一进来先把 sched_domains_numa_levels = 0,告诉别人“我重建中,旧拓扑先别用”。
  2. 分配 masks[]、每个 masks[i]、每个节点 mask、tl、填表。
  3. 任意一步失败就 return不释放已分配的部分
  4. 末尾成功路径才把 sched_domains_numa_levels = nr_levels,并发布 sched_numa_node_levels / sched_max_numa_distance / sched_domains_numa_masks
old flow+---------------------------------------+
| enter sched_init_numa()               |
|  sched_domains_numa_levels = 0 |
|  masks = kzalloc(...)                 |
|   fail -> return   (leak nothing yet) |
|  for i in [0..nr_levels):             |
|    masks[i] = kzalloc(...)            |
|     fail -> return  (leak masks)      |
|    per-node mask loop |
|     fail -> return  (leak masks[i] |
|                       + earlier rows) |
|  tl = kzalloc(...)                    |
|   fail -> return  (leak all masks)    |
|  fill tl, set level count, publish    |
+---------------------------------------+

更要命的是,旧代码在失败路径里没有释放 domain_distances / distances,并且把 sched_numa_node_levels 的发布放在距离表发布之前,并发读者可能看到一个 levels >0sched_numa_node_distance == NULL 的瞬时不一致窗口

新流程

补丁引入 helper:

static void sched_free_numa_masks(struct cpumask ***masks, int nr_levels)
{
    int i, j;

    if (!masks)
        return;

    for (i = 0; i < nr_levels; i++) {
        if (!masks[i])
            continue;
        for_each_node(j)
            kfree(masks[i][j]);
        kfree(masks[i]);
    }
    kfree(masks);
}

sched_init_numa() 重写后流程:

new flow
+-----------------------------------------------+
| enter sched_init_numa()                       |
|  distances / domain_distances allocated |
|  sched_domains_numa_levels = 0                |
|  masks = NULL (local, not published yet)      |
|  masks = kzalloc(...) -> fail? goto free     |
|  loop: allocate masks[i] and per-node masks   |
|        any fail? goto free                    |
|  tl = kzalloc(...) -> fail? goto free         |
|  fill tl                                       |
|  publish distances + levels + masks atomically|
|  return                                        |
| |
| free: |
|  sched_free_numa_masks(masks, nr_levels)      |
|  if (domain_distances != distances)           |
|      kfree(domain_distances)                  |
|  kfree(distances)                             |
+-----------------------------------------------+

sched_reset_numa() 把原本就地展开的两层循环释放替换成一行 sched_free_numa_masks(masks, nr_levels),重复代码消除。

Patch 概览

主要修改分布在 kernel/sched/topology.c 的三个函数里:

function | role
-------------------------------|--------------------------------
sched_free_numa_masks (NEW)    | 释放三层结构 masks[level][node]
sched_init_numa                | 全部失败路径改成 goto free
 | 全局发布延后到所有分配成功之后
sched_reset_numa               | 调用新 helper,删除内联释放循环

关键实现

错误路径集中:

void sched_init_numa(int offline_node)
{
    struct cpumask ***masks = NULL;
    /* ... 距离数组分配 ... */
    sched_domains_numa_levels = 0;

    masks = kzalloc(sizeof(void *) * nr_levels, GFP_KERNEL);
    if (!masks)
        goto free;

    for (i = 0; i < nr_levels; i++) {
        masks[i] = kzalloc(nr_node_ids * sizeof(void *), GFP_KERNEL);
        if (!masks[i])
            goto free;
        for_each_cpu_node_but(j, offline_node) {
            struct cpumask *mask = kzalloc(cpumask_size(), GFP_KERNEL);
            if (!mask)
                goto free;
            masks[i][j] = mask;
            /* ... fill mask ... */
        }
    }

    tl = kzalloc(...);
    if (!tl)
        goto free;

    /* fill tl ... */

    /* Publish only after every allocation succeeded */
    rcu_assign_pointer(sched_numa_node_distance, distances);
    WRITE_ONCE(sched_max_numa_distance, distances[nr_node_levels - 1]);
    WRITE_ONCE(sched_numa_node_levels, nr_node_levels);
    rcu_assign_pointer(sched_domains_numa_distance, domain_distances);
    rcu_assign_pointer(sched_domains_numa_masks, masks);
    return;

free:
    sched_free_numa_masks(masks, nr_levels);
    if (domain_distances != distances)
        kfree(domain_distances);
    kfree(distances);
}

注意三条发布顺序被刻意放进同一段:

  • rcu_assign_pointer(sched_numa_node_distance, distances)
  • WRITE_ONCE(sched_max_numa_distance, ...)
  • WRITE_ONCE(sched_numa_node_levels, nr_node_levels)
  • rcu_assign_pointer(sched_domains_numa_distance, domain_distances)
  • rcu_assign_pointer(sched_domains_numa_masks, masks)

老代码里 sched_numa_node_levels 的发布早于其他几个发布;新代码把它们聚拢到 return 前,让读者要么看到完整的旧值(因为没发布所以还是0),要么看到完整的新值,没有中间态

类比

想象你搬家:

  • 旧流程 = 你先把客厅堆满纸箱,搬到一半发现卧室的箱子装不下了,就地抛下客厅那一堆已经打包好的东西出门。楼道越堆越乱。
  • 新流程 = 你在仓库门口先把所有箱子摆好、清点;哪只箱子没装好就当场拆掉、扔回收站;只有清点齐了才把“门口”的总开关一次性打开让大家进来取用。全局开门前,所有半成品都不对外可见

sched_free_numa_masks() 就是那个“拆箱回收站”,goto free 是“任何一步出问题就停手清理”的统一出口,rcu_assign_pointer 是“门口总开关”。

Highlight:风险与注意点

  • NULL 安全:helper 用 if (!masks) 守门,sched_init_numa()masks 显式初始化为 NULL,避免 goto 时还没分配就被 free。
  • 距离数组双形态domain_distancesdistancessched_record_numa_dist() 之后可能指向同一块或不同块(SLIT 重排)。补丁用 if (domain_distances != distances) 区分释放,避免 double free。
  • tl 在新流程里仍无显式释放——但 tl 失败时本地 masks 已经通过 helper 被释放,全局指针还未切换,不存在泄漏;这是补丁的隐含保证,需要 review 确认。
  • RCU 读者:原先早发布的 sched_numa_node_levels 可能让 find_next_best_cpu() 等 RCU 读者短暂看到 levels > 0 但拓扑未就绪;新顺序消除了这一窗口。
  • Fixes tag0fb3978b0aac ("sched/numa: Fix NUMA topology for systems with CPU-less nodes") 表明这是该早期补丁引入的回归;stable 选材时应当 backport。
  • 未触及sched_numa_warn() 等诊断路径与本修复无关;本 patch 仅清理失败路径内存,不改变成功路径语义。

一句话总结

sched_init_numa() 从“半成品一旦失败就地抛下”改成“所有分配成功才一次性发布 RCU 全局指针”,并抽出一个共用 helper sched_free_numa_masks() 复用给 sched_reset_numa(),堵住 CPU 热插拔反复重建 NUMA 拓扑时的内存与状态泄漏。