0/5 已展开

LLM 分析

sched/isolation:把 memblock cpumask 的释放推迟到 initcall

系列概况

  • 标题: [PATCH v4] sched/isolation: Defer freeing of cpumask memblock memory to initcall
  • 作者: Waiman Long longman@redhat.com
  • 版本: v4(v4 的改动为「Collect tags and update commit log」)
  • 规模: 单补丁(非系列),线程共 5 封邮件(1 封 patch + 3 封作者 ping/重发回复 + 1 封 tip-bot 合并通知)
  • 修改文件: kernel/sched/isolation.c
  • 代码统计: 1 file changed, 15 insertions(+), 1 deletion(-)
  • Message-ID: 20260701195810.477326-1-longman@redhat.com
  • 完整性: 完整。已被 Peter Zijlstra 合入 tip:sched/core,Commit-ID 2b58c749b8c5244e259a0230bc57b10b010dc545(CommitterDate 2026-08-13)

补丁目的

在 linux-next 上带 nohz_full= 启动参数时,housekeeping_init() 里对 memblock 内存调用 memblock_free() 触发了新加的告警(来自 commit 59bd1d914bb5,"memblock: warn when freeing reserved memory before memory map is initialized"):

Cannot free reserved memory because of deferred initialization of the memory map
WARNING: mm/memblock.c:904 at __free_reserved_area+0xde/0xf0, CPU#0: swapper/0/0
  memblock_phys_free+0xcb/0x100
  housekeeping_init+0x14c/0x170
  start_kernel+0x207/0x450

根因:housekeeping_init()start_kernel() 早期执行,此时 memory map 还没被 deferred_init_memmap() 完全初始化,把 reserved 内存还回去是不安全的。

目标:保留原有「用完就释放临时 cpumask」的语义,但把释放动作推迟到 initcall 阶段。

旧流程的问题

27c3a5967f05("sched/isolation: Convert housekeeping cpumasks to rcu pointers")之后,housekeeping_init() 会把早期用 memblock 分配的旧 cpumask(omask)复制到新 mask 并挂成 RCU 指针,然后立刻 memblock_free(omask, cpumask_size())。这个「立刻」发生在 memory map 尚未就绪的窗口内,于是踩中新告警。

新流程

不改分配路径,只把待释放的 omask 串成一个链表,等到 pure_initcall 时一次性 free。

       start_kernel()
            |
            v
    housekeeping_init()            [memory map 未就绪]
            |
            |  copy omask -> nmask, RCU_INIT_POINTER
            |
            +--> __llist_add(omask) --> memblock_freelist
                                          |  omask1 -> omask2 -> ... -> NULL
                                          |
            ... 内核继续启动 ...           |
    deferred_init_memmap() 完成           |
            |                             |
            v                             v
     pure_initcall 阶段  ----> housekeeping_late_init()
                                  __llist_del_all()
                                  for each node: memblock_free(node, cpumask_size())

关键实现

#include <linux/llist.h>

static __initdata LLIST_HEAD(memblock_freelist);

/* housekeeping_init(): 不再立刻释放 */
- memblock_free(omask, cpumask_size());
+ __llist_add((struct llist_node *)omask, &memblock_freelist);

static int __init housekeeping_late_init(void)
{
	struct llist_node *llnode, *pos, *t;

	/* Free allocated memblock memory, if any */
	llnode = __llist_del_all(&memblock_freelist);
	llist_for_each_safe(pos, t, llnode)
		memblock_free(pos, cpumask_size());
	return 0;
}
pure_initcall(housekeeping_late_init);

三个设计要点:

  1. 复用待释放内存本身当链表节点omask 已经不再被读,直接把它 cast 成 struct llist_nodellist_node 只有一个 next 指针(一个 long),而 cpumask 的最小分配尺寸也正好是一个 long,所以不会越界。
  2. 用非原子版 llist API__llist_add / __llist_del_all):启动早期单线程,没有并发争用,省掉 cmpxchg。
  3. pure_initcall + __initdata:链表头本身是 initdata,跑完 initcall 后连表头都被回收;pure_initcall 是最早一档 initcall,此时 memory map 已就绪,但又尽早还内存。

依赖说明:commit message 明确写了本补丁依赖 7c2eee9c1367("memblock: don't touch memblock arrays when memblock_free() is called late"),否则晚调用 memblock_free() 会被 KASAN 报 UAF。

Fixes: 指向 27c3a5967f05,Reviewed-by 来自 Frederic Weisbecker 和 Phil Auld。

类比

想象你在装修房子的第一天(早期启动),工人临时用几块木板搭了个操作台(memblock 分配的 cpumask)。活干完了,你想立刻把木板扔进小区垃圾站——但垃圾站还没建好、门牌号还没登记(memory map 未初始化),保安会拦下你并贴一张警告条。

这个补丁的做法是:把这几块木板先靠墙码在门口,用一根绳子串起来(llist,绳子直接绑在木板上,不额外占地方),等垃圾站正式开张的第一天(pure_initcall)再一次性推过去扔掉。既没提前违规,也没把木板永远留在屋里。

线程本身的另一层「类比」:作者在 7/13、7/29、8/13 三次 ping 这封补丁——就像把包裹放在收发室后每两周去问一次,直到 8/13 维护者终于签收(tip-bot 合并通知)。

Highlight:风险与注意点

  • 强依赖 7c2eee9c1367:如果补丁被 backport 到没有该 memblock 修复的树上,延后的 memblock_free() 会触碰已经失效的 memblock 数组,KASAN 会报 use-after-free。合并顺序必须保证。
  • cast 的隐含假设(struct llist_node *)omask 依赖「cpumask 最小分配 ≥ sizeof(long)」。如果将来 memblock 分配路径或 cpumask 尺寸约定变化,这个 cast 会静默越界写。commit log 已解释,但代码里没有 BUILD_BUG_ON 之类的静态检查,是可以跟进加固的点。
  • 非原子 llist 的前提__llist_add/__llist_del_all 无锁保护,正确性完全建立在「housekeeping_init 与 late_init 都是单线程、无并发」上。若以后有人在 SMP 上下文里复用这条链表,会静默出错。
  • 只是把窗口移后,不是不释放pure_initcall 是最早一档 initcall,需要确认 deferred memmap 初始化确实已完成;这也是选 pure_initcall 而非更晚 initcall 时值得复核的地方(当前无人在线程中提出异议)。
  • 触发条件窄:只有带 nohz_full/isolcpus 之类导致 housekeeping cpumask 被分配的启动参数才会命中,默认配置下看不到告警,容易被误认为「无关紧要」。

版本变化

  • v4:收集 Reviewed-by tags(Frederic Weisbecker、Phil Auld)并更新 commit log;实现相对 v3 无功能改动。
  • 线程中 v1(20260505051821.1107133-1-longman@redhat.com)被反复引用作为上下文来源。

与其他相关 patch 的关联

  • 59bd1d914bb5(memblock: warn when freeing reserved memory...)——告警的引入者,是本补丁的触发源。
  • 7c2eee9c1367(memblock: don't touch memblock arrays when memblock_free() is called late)——硬依赖。
  • 27c3a5967f05(sched/isolation: Convert housekeeping cpumasks to rcu pointers)——Fixes: 目标,引入了这段早期 free 逻辑。

一句话总结

housekeeping_init() 里过早的 memblock_free() 改成「用待释放内存自身做 llist 节点先挂起、到 pure_initcall 再统一释放」,以 15 行代码消除了 nohz_full 启动下的 memblock 告警,已合入 tip:sched/core。