〇、全景: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 等字段做链表指针)。这意味着:
- buddy 要工作,必须先有
struct page数组 ------每个物理页一个struct page(x86_64 上几十字节),整个数组(vmemmap)占物理内存约 1%~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 |