Linux 6.6内核内存管理深度解析(一):物理内存初始化 — memblock 怎么把内存交给 buddy

〇、全景:buddy 建好之前,先用 memblock 顶

内核的内存管理,最终落到 buddy 分配器 (伙伴系统)身上------alloc_page/free_page、order、free_area[] 链表,这套"正式分配器"几乎承载了所有内核内存分配。但 buddy 有一个先有鸡还是先有蛋 的问题:它要给每个物理页 一个 struct page 来记账(空闲链表就挂在 struct page 上),而 struct page 数组(vmemmap)本身要占一大块内存、也得先被分配出来。

所以在 buddy 和 struct page 建好之前,内核需要一个不依赖 struct page 的、只记"物理区间"的粗粒度分配器 先顶着------这就是 memblock。物理内存初始化,本质就是这条"从 memblock 交接给 buddy"的路:
#mermaid-svg-yM8lGeKdV921N1yH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yM8lGeKdV921N1yH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yM8lGeKdV921N1yH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yM8lGeKdV921N1yH .error-icon{fill:#552222;}#mermaid-svg-yM8lGeKdV921N1yH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yM8lGeKdV921N1yH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yM8lGeKdV921N1yH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yM8lGeKdV921N1yH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yM8lGeKdV921N1yH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yM8lGeKdV921N1yH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yM8lGeKdV921N1yH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yM8lGeKdV921N1yH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yM8lGeKdV921N1yH .marker.cross{stroke:#333333;}#mermaid-svg-yM8lGeKdV921N1yH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yM8lGeKdV921N1yH p{margin:0;}#mermaid-svg-yM8lGeKdV921N1yH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yM8lGeKdV921N1yH .cluster-label text{fill:#333;}#mermaid-svg-yM8lGeKdV921N1yH .cluster-label span{color:#333;}#mermaid-svg-yM8lGeKdV921N1yH .cluster-label span p{background-color:transparent;}#mermaid-svg-yM8lGeKdV921N1yH .label text,#mermaid-svg-yM8lGeKdV921N1yH span{fill:#333;color:#333;}#mermaid-svg-yM8lGeKdV921N1yH .node rect,#mermaid-svg-yM8lGeKdV921N1yH .node circle,#mermaid-svg-yM8lGeKdV921N1yH .node ellipse,#mermaid-svg-yM8lGeKdV921N1yH .node polygon,#mermaid-svg-yM8lGeKdV921N1yH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yM8lGeKdV921N1yH .rough-node .label text,#mermaid-svg-yM8lGeKdV921N1yH .node .label text,#mermaid-svg-yM8lGeKdV921N1yH .image-shape .label,#mermaid-svg-yM8lGeKdV921N1yH .icon-shape .label{text-anchor:middle;}#mermaid-svg-yM8lGeKdV921N1yH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yM8lGeKdV921N1yH .rough-node .label,#mermaid-svg-yM8lGeKdV921N1yH .node .label,#mermaid-svg-yM8lGeKdV921N1yH .image-shape .label,#mermaid-svg-yM8lGeKdV921N1yH .icon-shape .label{text-align:center;}#mermaid-svg-yM8lGeKdV921N1yH .node.clickable{cursor:pointer;}#mermaid-svg-yM8lGeKdV921N1yH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yM8lGeKdV921N1yH .arrowheadPath{fill:#333333;}#mermaid-svg-yM8lGeKdV921N1yH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yM8lGeKdV921N1yH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yM8lGeKdV921N1yH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yM8lGeKdV921N1yH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yM8lGeKdV921N1yH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yM8lGeKdV921N1yH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yM8lGeKdV921N1yH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yM8lGeKdV921N1yH .cluster text{fill:#333;}#mermaid-svg-yM8lGeKdV921N1yH .cluster span{color:#333;}#mermaid-svg-yM8lGeKdV921N1yH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yM8lGeKdV921N1yH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yM8lGeKdV921N1yH rect.text{fill:none;stroke-width:0;}#mermaid-svg-yM8lGeKdV921N1yH .icon-shape,#mermaid-svg-yM8lGeKdV921N1yH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yM8lGeKdV921N1yH .icon-shape p,#mermaid-svg-yM8lGeKdV921N1yH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yM8lGeKdV921N1yH .icon-shape .label rect,#mermaid-svg-yM8lGeKdV921N1yH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yM8lGeKdV921N1yH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yM8lGeKdV921N1yH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yM8lGeKdV921N1yH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} e820__memblock_setup

memblock_add RAM
早期分配

memblock_alloc
sparse_init

建 vmemmap
memblock_free_all

free 进 free_area
buddy 接管
e820 探测

固件内存地图
memblock

memory 区间 - reserved 区间
SPARSEMEM

struct page 数组(vmemmap)
buddy

free_area\[\] 空闲链表

一句话主线:物理内存初始化是一条"交接"链路------启动早期 buddy 没建好(它需要 struct page,而 struct page 自己也要内存),先用只记区间的 memblock 顶着;e820 从固件拿到内存地图喂给 memblock(memory/reserved 两个区间集合),早期分配(页表/percpu/initrd)都从 memblock 挖、标成 reserved,SPARSEMEM 建好 struct page(vmemmap)后,mem_init 里 memblock_free_all 把"memory 减去 reserved"剩下的空闲页一次性 free 进 buddy 的 free_area[],buddy 从此接管。


一、为什么需要 memblock:buddy 的"鸡生蛋"问题

先想清楚为什么不能一上来就用 buddy。buddy 分配器的工作方式(下一篇细讲):把物理内存按 2^order 个页组织成空闲链表,每个空闲块的元信息存在对应的 struct page 里(复用 page->lru/page->buddy_list 等字段做链表指针)。这意味着:

  1. buddy 要工作,必须先有 struct page 数组 ------每个物理页一个 struct page(x86_64 上几十字节),整个数组(vmemmap)占物理内存约 1%~2% 量级。
  2. struct page 数组本身要分配内存,而且内核镜像、页表、percpu 区、initrd......这些早期结构都要在"正式分配器"之前就位。

于是陷入循环:buddy 需要 struct page → struct page 需要内存 → 分配内存需要分配器 → 但分配器(buddy)还没就位。破局的办法就是先上一个"粗粒度"的临时分配器:

  • memblock 不依赖 struct page ,它只记两个"物理地址区间"的集合(见 §三)------memory(哪些区间是可用的 RAM)和 reserved(哪些被占用了)。分配 = 在 memory 里挖一块、标进 reserved,完全不碰 per-page 记账。
  • 等 struct page 建好、buddy 建好,memblock 把剩下的空闲页一次性交接给 buddy,自己功成身退。

这就是"鸡生蛋"的解法:用 memblock(粗粒度、无依赖)先顶过启动早期,再切到 buddy(细粒度、依赖 struct page)。


二、e820:从固件拿到物理内存地图

第一步是搞清楚这台机器有多少内存、哪些地址能用 。x86 靠固件提供的 e820 表 ------一张"物理地址区间 → 类型"的表,类型有:RAM(可用内存)、RESERVED、ACPI(ACPI 表/数据)、NVS(非易失存储)、SOFT_RESERVED 等。

setup_arch 里先解析它:

c 复制代码
// arch/x86/kernel/setup.c (v6.6, line 944) ------ 实现于 e820.c:1298
e820__memory_setup();   // 从固件读 e820 表,打印 "BIOS-provided physical RAM map"

e820__memory_setup(e820.c:1298)核心就是调 x86_init.resources.memory_setup() 去读表,然后 e820__print_table 把整张地图打出来。之后 e820__memblock_setup(e820.c:1314,调用点 setup.c:1117)把 e820 表翻译成 memblock 的区间:

c 复制代码
// arch/x86/kernel/e820.c (v6.6, line 1314)
void __init e820__memblock_setup(void)
{
	int i;
	u64 end;

	memblock_allow_resize();                       // EFI 可能给超过 128 条,允许扩容

	for (i = 0; i < e820_table->nr_entries; i++) {
		struct e820_entry *entry = &e820_table->entries[i];

		end = entry->addr + entry->size;
		if (end != (resource_size_t)end)
			continue;

		if (entry->type == E820_TYPE_SOFT_RESERVED)
			memblock_reserve(entry->addr, entry->size);

		if (entry->type != E820_TYPE_RAM && entry->type != E820_TYPE_RESERVED_KERN)
			continue;

		memblock_add(entry->addr, entry->size);   // 只有 RAM 进入可用内存池
	}

	memblock_trim_memory(PAGE_SIZE);               // 丢掉不满一页的零头
	memblock_dump_all();
}

关键:只有 E820_TYPE_RAM(和内核保留的 RESERVED_KERN)会 memblock_add 进可用内存池 ,SOFT_RESERVED 会被 memblock_reserve 保护起来,其余(ACPI/NVS/普通 RESERVED)直接跳过------这些是设备/固件占用的地址,不能当普通内存用。


三、memblock:memory - reserved = 可分配

memblock 的数据结构极其简单,就是两个"区间集合":

c 复制代码
// include/linux/memblock.h (v6.6, line 59 / 76 / 91)
struct memblock_region {         // 一条区间
	phys_addr_t base;
	phys_addr_t size;
	enum memblock_flags flags;
#ifdef CONFIG_NUMA
	int nid;
#endif
};

struct memblock_type {           // 一类区间的集合
	unsigned long cnt;       // 有多少条
	unsigned long max;       // 数组容量
	phys_addr_t total_size;  // 总大小
	struct memblock_region *regions;
	char *name;
};

struct memblock {                // 全局单例
	bool bottom_up;
	phys_addr_t current_limit;   // 分配上限
	struct memblock_type memory;    // 可用内存
	struct memblock_type reserved;  // 已占用
};

于是 memblock 的语义一目了然:

  • memblock.memory:从 e820 喂进来的可用 RAM 区间。
  • memblock.reserved:已经"名花有主"的区间(内核镜像、页表、percpu、initrd、ACPI......)。
  • 可分配内存 = memory 减去 reserved。

分配(memblock_phys_alloc,memblock.h:406)就是在 memory 里找一块够大、且不和 reserved 重叠的区间,切出来、标进 reserved:

c 复制代码
// include/linux/memblock.h (v6.6, line 406) ------ 分配物理内存
static __always_inline phys_addr_t memblock_phys_alloc(phys_addr_t size, phys_addr_t align)
{
	return memblock_phys_alloc_range(size, align, 0, MEMBLOCK_ALLOC_ACCESSIBLE);
}
// memblock_add  (memblock.c:713)  →  memblock_add_range(&memblock.memory, ...)
// memblock_reserve (memblock.c:871) →  memblock_add_range(&memblock.reserved, ...)

早期所有"正式分配器还没就位"的分配都走它:页表、setup_per_cpu_areas 的 percpu 区、initrd......(这就是上一篇 CPU #8 里 setup_per_cpu_areas 底层用的分配器)。

一句话:memblock 不记账 per-page,只记账"区间";分配 = 从 memory 挖一块、挪进 reserved。 这是它能"无依赖地先顶着"的根本原因。


四、SPARSEMEM:给每个页建 struct page(vmemmap)

memblock 只记区间,但 buddy 需要 per-page 的 struct page。所以交接之前,必须先为每个物理页建好 struct page 数组 。这一步由内存模型负责,x86 64 位默认 SPARSEMEM + vmemmap。

  • 内存模型分三种:FLATMEM(平坦,整个物理内存当一条连续数组)、SPARSEMEM(稀疏,把内存切成 section,只给有内存的 section 建数组)、DISCONTIGMEM(已废弃)。x86 64 位用 SPARSEMEM。
  • vmemmap 是 SPARSEMEM 的关键优化 :把 struct page 数组映射到一段连续的虚拟地址 上,于是 pfn_to_page 就是一条加法:
c 复制代码
// include/asm-generic/memory_model.h (v6.6, line 37) ------ SPARSEMEM_VMEMMAP 模式
#define __pfn_to_page(pfn)	(vmemmap + (pfn))
#define __page_to_pfn(page)	(unsigned long)((page) - vmemmap)

pfn_to_page(n) = vmemmap 基址 + n,无需任何查表------这就是为什么 vmemmap 让 SPARSEMEM 和 FLATMEM 一样快。

建立 vmemmap 的是 sparse_init:

c 复制代码
// mm/sparse.c (v6.6, line 558)
void __init sparse_init(void)
{
	// ... 遍历有内存的 section(memory_section),为每个 section:
	//     __populate_section_memmap(pfn, PAGES_PER_SECTION, ...)  ------ sparse.c:428
	//     建这个 section 的 struct page 数组,映射进 vmemmap
}

一个 section 覆盖多大内存(PAGES_PER_SECTION)由 SECTION_SIZE_BITS 决定------x86_64 是 SECTION_SIZE_BITS=27(128MB),x86_32 是 26/29(64MB/512MB)。sparse_init 只给"确实有内存"的 section 建 struct page,空洞的地址区间不浪费 vmemmap------这正是 SPARSEMEM 相对 FLATMEM 的意义。


五、交接:memblock_free_all → buddy

struct page 建好、buddy 的记账结构(free_area[])也初始化好了,最后一步就是交接 。入口是 mem_init 里的 memblock_free_all:

c 复制代码
// arch/x86/mm/init_64.c (v6.6, line 1331)
void __init mem_init(void)
{
	pci_iommu_alloc();

	/* this will put all memory onto the freelists */
	memblock_free_all();   // ← 交接点:把空闲内存全部 free 进 buddy
	after_bootmem = 1;
	// ...
}

调用链(start_kernel :870 → mm_core_init :928 → mem_init)串起来是这样的:

c 复制代码
// init/main.c (v6.6, line 928) → 定义于 mm/mm_init.c:2762
mm_core_init();
//   ├─ build_all_zonelists(NULL)   (mm_init.c:2765) 建 zone 回退链
//   ├─ mem_init()                  (mm_init.c:2778 → x86 的 init_64.c:1331)
//   │     └─ memblock_free_all()   (init_64.c:1338)
//   └─ kmem_cache_init()           slab 也要等 buddy 就绪

memblock_free_all 做的事,就是把"memory 减去 reserved"剩下的每一个空闲页,free 进 buddy:

c 复制代码
// mm/memblock.c (v6.6, line 2174)
void __init memblock_free_all(void)
{
	unsigned long pages;

	free_unused_memmap();
	reset_all_zones_managed_pages();

	pages = free_low_memory_core_early();   // ← 真正的释放
	totalram_pages_add(pages);
}
c 复制代码
// mm/memblock.c (v6.6, line 2126)
static unsigned long __init free_low_memory_core_early(void)
{
	unsigned long count = 0;
	phys_addr_t start, end;
	u64 i;

	memmap_init_reserved_pages();           // 先给 reserved 区也初始化 struct page

	for_each_free_mem_range(i, NUMA_NO_NODE, MEMBLOCK_NONE, &start, &end, NULL)
		count += __free_memory_core(start, end);   // 每个空闲区间 free 进 buddy
	return count;
}

for_each_free_mem_range 遍历的正是"memory 里、没被 reserved 盖住的区间",__free_memory_core(memblock.c:2080)把这段区间换算成 pfn 范围,__free_pages_memory 逐页 free 进 buddy 的 free_area[]。交接完成后 after_bootmem = 1,memblock 退居二线,buddy 正式接管所有空闲内存。


六、为什么这样设计(Why 层)

6.1 为什么要有 memblock 这个"临时分配器",而不是直接上 buddy

因为 buddy 有硬依赖:struct page 。buddy 的空闲链表元信息挂在 struct page 上,而 struct page 数组(vmemmap)占内存、要分配。所以"分配内存"这件事在 buddy 就绪之前就必须有一个不依赖 struct page 的分配器 来做。memblock 只记"区间"(base/size 两个数),天然无依赖,是启动早期唯一可行的选择。这是"鸡生蛋"问题的标准解法:先用粗粒度的顶,再切细粒度的。

6.2 为什么 memblock 用"memory - reserved"两个集合,而不是一张链表

因为启动早期要频繁 reserve 各种固件/内核占用的地址 (ACPI、initrd、percpu、内核镜像......),而这些 reserve 往往是"任意物理区间"。用两个区间集合(memory 存可用、reserved 存占用)表达"哪些地址空着",比维护一张精细的 per-page 位图简单得多------而且此时 struct page 还没有,per-page 记账无从谈起。"区间"是启动早期唯一能廉价表达的东西。

6.3 为什么 x86 用 SPARSEMEM,而不是 FLATMEM

FLATMEM 假设物理内存是连续的一段 ,struct page 数组从 0 一路排到最大 pfn。但 x86 服务器的物理地址空间充满空洞(设备映射、NUMA 节点之间的 gap),FLATMEM 会给这些空洞也白白建 struct page。SPARSEMEM 把内存切成 section,只给有内存的 section 建 struct page ,省下空洞的浪费;再靠 vmemmap(连续虚拟映射)让 pfn_to_page 保持一条加法、不失 FLATMEM 的速度。"稀疏存储 + 连续虚拟映射"是这套模型的两个要点。

6.4 为什么交接点是 memblock_free_all,而不是逐步释放

因为 memblock 和 buddy 是两套记账体系(区间 vs per-page),让它们长期并存会很乱。memblock 的使命就是"顶到 buddy 就绪",所以一次性 把空闲页全部倒进 buddy、然后 after_bootmem = 1 干净退出,比"边用边搬"简单得多。这也是为什么 memblock_free_all 之后,大部分 memblock 的分配 API 就不再使用了。


七、与已有博客的交叉引用

已有博客 本篇覆盖的关系
CPU #1 BSP 拉起 setup_arch 里的 e820__memory_setup(setup.c:944)/e820__memblock_setup(:1117)就是本篇 §二的入口;#1 讲启动链路,本篇展开其中的"内存初始化"这一段
CPU #8 percpu setup_per_cpu_areas 分配 percpu 区,底层就是 memblock_alloc(本篇 §三的早期分配);percpu 区也是 memblock.reserved 里的一笔
内存 #2(下一篇)buddy 本篇 §五交接给 buddy,下一篇讲 buddy 到底怎么管理 free_area[]、alloc_page 怎么分页

附:本篇关键源码索引

符号 位置
e820__memory_setup(读 e820 表) arch/x86/kernel/e820.c:1298(调用点 setup.c:944)
e820__memblock_setup(喂入 memblock) arch/x86/kernel/e820.c:1314(调用点 setup.c:1117)
struct memblock_region / memblock_type / memblock include/linux/memblock.h:59 / :76 / :91
memblock_add / memblock_reserve mm/memblock.c:713 / :871
memblock_phys_alloc(早期分配) include/linux/memblock.h:406
sparse_init(建 vmemmap) mm/sparse.c:558
__populate_section_memmap mm/sparse.c:428
pfn_to_page(vmemmap + pfn) include/asm-generic/memory_model.h:37
mm_core_init / build_all_zonelists mm/mm_init.c:2762 / :2765
free_area_init / free_area_init_core mm/mm_init.c:1792 / :1548
mem_init(x86,含 memblock_free_all) arch/x86/mm/init_64.c:1331 / :1338
memblock_free_all / free_low_memory_core_early / __free_memory_core mm/memblock.c:2174 / :2126 / :2080
相关推荐
2401_868534781 小时前
无线网络规划设计
服务器·网络
库拉镜像AI牛牛1 小时前
短剧内容自动化生产:知漫剧工作室落地教程
大数据·服务器·前端·人工智能·语音识别
天蓝蓝的本我1 小时前
conda日常用到的命令积累1-登录linux,安装pycharm
linux·pycharm·conda
Android系统攻城狮1 小时前
Linux Gstreamer深度解析之gst_audio_encoder_get_frame_max调用流程与实战(五十九)
linux·运维·服务器·gstreamer音视频·音视频进阶·gstreamer音视频进阶
l1t2 小时前
DeepSeek总结的一个不含数据的 DuckDB 数据库
服务器·数据库·duckdb
忆挽篱笙歌2 小时前
makefil
linux·运维·服务器
菜_小_白3 小时前
codex
linux·vscode·ai
程序员老赵3 小时前
Docker 部署 Dolibarr:轻松搭建开源 ERP/CRM 平台
运维·前端·后端
zhangrelay3 小时前
用ROS2Lyrical实验报告册思路重做蓝桥云课ROS1实验并改进
linux·笔记·学习·ubuntu·机器人