sched discussion
[PATCH] sched/numa: Free partial topology after a failed rebuild
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 走清理分支。
旧流程的问题
旧流程里关键时序是:
- 一进来先把
sched_domains_numa_levels = 0,告诉别人“我重建中,旧拓扑先别用”。 - 分配
masks[]、每个masks[i]、每个节点 mask、tl、填表。 - 任意一步失败就
return,不释放已分配的部分。 - 末尾成功路径才把
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 >0 但 sched_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_distances与distances在sched_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 tag:
0fb3978b0aac ("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 拓扑时的内存与状态泄漏。