4. 页表分配器初始化:memblock与bootmem阶段页表内存的预留与初始化流程
在Linux内核启动早期,内存管理子系统尚未完全建立,伙伴系统和slab分配器均不可用。此时,内核必须依赖一个自举式内存分配器来完成页表等关键数据结构的预留与初始化。x86-64架构经历了从bootmem到memblock的演进,本章聚焦于这两个阶段中页表内存的分配策略与初始化流程。
4.1 启动阶段的内存视图与页表需求
内核刚进入保护模式或长模式时,CPU 直接使用物理地址。为了开启分页并建立线性映射,内核需要立即分配以下页表内存:
- 页全局目录(PGD / PML4):每个 CPU 一个,用于顶层页表。
- 页上级目录(PUD):覆盖整个线性地址空间。
- 页中间目录(PMD):细粒度映射内核空间。
- 页表(PTE):最终物理页映射。
- 固定映射页表(fixmap):用于早期 I/O 和临时映射。
| 页表层级 | 典型大小(x86-64) | 启动阶段分配时机 | 分配器 |
|---|---|---|---|
| PML4(PGD) | 4KB | start_kernel → setup_arch | memblock / bootmem |
| PUD | 4KB × 数量 | init_mem_mapping | memblock |
| PMD | 4KB × 数量 | init_mem_mapping | memblock |
| PTE | 4KB × 数量 | 分页建立时按需分配 | memblock |
| fixmap PTE | 4KB | early_fixmap_init | memblock |
4.2 bootmem 分配器(传统方案)
bootmem 是一个基于位图的早期分配器,在 NUMA 节点上使用 pglist_data->bdata 管理空闲物理页。其页表内存分配流程如下:
- 位图初始化:在
setup_arch()中调用init_bootmem(),根据 e820 内存映射建立空闲页位图。 - 页表预留:调用
alloc_bootmem_pages()或alloc_bootmem_low_pages()分配连续的物理页用于页表。 - 固定映射预留:通过
reserve_bootmem()锁定 fixmap 区域,防止被其他分配覆盖。 - 映射建立:使用分配的页表页填充
swapper_pg_dir,建立内核线性映射。
4.3 memblock 分配器(现代方案)
memblock 使用区域数组(memblock_type)管理内存,支持 MEMBLOCK_NONE、MEMBLOCK_HOTPLUG 等标志,分配效率远高于 bootmem。其页表内存初始化流程分为三个阶段:
4.3.1 早期预留阶段(early_reserve)
在 start_kernel() → setup_arch() 中,内核通过 memblock_reserve() 预留页表所需物理页:
// arch/x86/kernel/setup.c
void __init setup_arch(char **cmdline_p)
{
// 1. 解析 e820 并初始化 memblock
e820__memory_setup();
memblock_set_current_limit(ISA_END_ADDRESS);
// 2. 预留内核代码段、数据段
memblock_reserve(__pa_symbol(_text),
__pa_symbol(_end) - __pa_symbol(_text));
// 3. 预留页表内存(关键步骤)
early_alloc_pgt_buf();
// 4. 初始化固定映射
early_fixmap_init();
}
early_alloc_pgt_buf() 在 arch/x86/mm/init.c 中实现,它从 memblock 分配一个页表缓冲池:
void __init early_alloc_pgt_buf(void)
{
unsigned long size = EXTRA_PAGE_TABLE_SIZE;
unsigned long start;
// 从 memblock 分配连续物理页
start = memblock_phys_alloc(size, PAGE_SIZE);
if (!start)
panic("Failed to allocate page table buffer\n");
// 记录缓冲池起始与结束地址
pgt_buf_start = start >> PAGE_SHIFT;
pgt_buf_end = pgt_buf_start;
pgt_buf_top = pgt_buf_start + (size >> PAGE_SHIFT);
}
4.3.2 映射建立阶段(init_mem_mapping)
在 init_mem_mapping() 中,内核从页表缓冲池中按需取出页表页,填充各级目录项:
// arch/x86/mm/init.c
void __init init_mem_mapping(void)
{
unsigned long end;
// 获取实际物理内存上限
end = max_pfn << PAGE_SHIFT;
// 建立内核线性映射(使用 2MB 大页或 4KB 小页)
kernel_physical_mapping_init(PAGE_OFFSET, end, 0);
// 加载新页表
load_cr3(swapper_pg_dir);
__flush_tlb_all();
}
其中 kernel_physical_mapping_init() 会调用 alloc_low_page() 从 memblock 缓冲池分配页表页:
static __init void *alloc_low_page(void)
{
unsigned long pfn;
// 从缓冲池取一页
if (pgt_buf_end >= pgt_buf_top)
pgt_buf_alloc_count++;
pfn = pgt_buf_end++;
// 清空页表页
memset((void *)__va(pfn << PAGE_SHIFT), 0, PAGE_SIZE);
return (void *)__va(pfn << PAGE_SHIFT);
}
4.3.3 固定映射初始化(early_fixmap_init)
固定映射(fixmap)是一组预定义的虚拟地址,用于早期 I/O 和临时映射。其页表在 memblock 阶段初始化:
void __init early_fixmap_init(void)
{
pgd_t *pgd;
p4d_t *p4d;
pud_t *pud;
pmd_t *pmd;
pte_t *pte;
// 获取 fixmap 对应的 PGD 项
pgd = pgd_offset_k(FIXADDR_TOP);
p4d = p4d_offset(pgd, FIXADDR_TOP);
pud = pud_offset(p4d, FIXADDR_TOP);
pmd = pmd_offset(pud, FIXADDR_TOP);
// 分配 PMD 页表(若不存在)
if (pmd_none(*pmd)) {
pte = alloc_low_page();
set_pmd(pmd, __pmd(__pa(pte) | _KERNPG_TABLE));
}
}
EXTRA_PAGE_TABLE_SIZE 控制(通常为 4KB × 64 = 256KB)。若系统内存极大或 NUMA 节点数多,可增大该值以减少后续 memblock_alloc 调用次数。
4.4 memblock 与 bootmem 的对比
| 特性 | bootmem | memblock |
|---|---|---|
| 数据结构 | 位图(bitmap) | 区域数组(memblock_type) |
| 分配复杂度 | O(n) 扫描位图 | O(1) 区域查找 |
| 页表预留方式 | alloc_bootmem_pages | memblock_phys_alloc |
| 大页支持 | 不支持 | 支持(通过标志) |
| 碎片回收 | 无 | 支持 memblock_free |
| 当前状态 | 已废弃(仅少数 arch 保留) | 主流方案 |
4.5 页表内存初始化流程总览
下图展示了从 start_kernel 到页表就绪的完整调用链与内存分配关系:
4.6 从 bootmem 到 memblock 的迁移路径
对于仍使用 bootmem 的旧内核或嵌入式架构,迁移至 memblock 可带来显著的页表分配性能提升。迁移步骤包括:
- 替换分配接口:将
alloc_bootmem_pages()替换为memblock_phys_alloc()。 - 移除位图初始化:删除
init_bootmem()调用,改用memblock_add()添加内存区域。 - 调整预留顺序:确保页表缓冲池在
memblock_reserve()之后分配,避免覆盖内核代码。 - 验证对齐:memblock 默认按
SMP_CACHE_BYTES对齐,页表分配需显式指定PAGE_SIZE对齐。
early_alloc_pgt_buf() 中增大 EXTRA_PAGE_TABLE_SIZE 可减少后续 memblock_alloc 调用次数,但会占用更多低端内存。对于 512GB 以上内存的服务器,建议将该值调整为 1MB 以上。
4.7 常见问题与避坑
- 页表缓冲池溢出:若
pgt_buf_end超过pgt_buf_top,内核会回退到memblock_alloc逐页分配,导致性能下降。可通过pgt_buf_alloc_count监控溢出次数。 - NUMA 节点页表局部性:memblock 默认从
MEMBLOCK_NONE区域分配,可能导致远程节点访问延迟。可在alloc_low_page()中增加节点亲和性判断。 - fixmap 冲突:若
early_fixmap_init()分配的 PMD 页表与已有映射重叠,会导致系统崩溃。务必在pmd_none()检查后分配。 - KASLR 影响:开启内核地址空间布局随机化后,页表基址
swapper_pg_dir可能不在低端内存,需确保 memblock 分配不跨越 4GB 边界。