4. 页表分配器初始化:memblock与bootmem阶段页表内存的预留与初始化流程

在Linux内核启动早期,内存管理子系统尚未完全建立,伙伴系统和slab分配器均不可用。此时,内核必须依赖一个自举式内存分配器来完成页表等关键数据结构的预留与初始化。x86-64架构经历了从bootmem到memblock的演进,本章聚焦于这两个阶段中页表内存的分配策略与初始化流程。

核心要点:页表分配器的初始化本质上是“用有限的自举内存管理机制,为后续完整内存管理搭建页表框架”。memblock 是当前主流方案,bootmem 仅用于旧内核或特定架构兼容。

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 管理空闲物理页。其页表内存分配流程如下:

  1. 位图初始化:在 setup_arch() 中调用 init_bootmem(),根据 e820 内存映射建立空闲页位图。
  2. 页表预留:调用 alloc_bootmem_pages() 或 alloc_bootmem_low_pages() 分配连续的物理页用于页表。
  3. 固定映射预留:通过 reserve_bootmem() 锁定 fixmap 区域,防止被其他分配覆盖。
  4. 映射建立:使用分配的页表页填充 swapper_pg_dir,建立内核线性映射。
bootmem 的局限性:位图扫描效率低,不支持大页分配,且无法在分配后释放碎片页。Linux 4.x 之后主流架构已全面迁移至 memblock。

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));
    }
}
性能调优视角:memblock 的页表缓冲池大小由 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 到页表就绪的完整调用链与内存分配关系:

页表分配器初始化流程 memblock 阶段页表内存初始化流程 start_kernel() setup_arch() early_alloc_pgt_buf() — 分配页表缓冲池 init_mem_mapping() — 建立线性映射 alloc_low_page() 循环:从缓冲池取页 → 清0 → 填入页表项 early_fixmap_init() — 固定映射页表 页表就绪,开启分页 关键数据结构 swapper_pg_dir pgt_buf_start/end/top memblock.memory 性能关键点 缓冲池大小可调 避免频繁 memblock_alloc 大页减少页表层级

4.6 从 bootmem 到 memblock 的迁移路径

对于仍使用 bootmem 的旧内核或嵌入式架构,迁移至 memblock 可带来显著的页表分配性能提升。迁移步骤包括:

  1. 替换分配接口:将 alloc_bootmem_pages() 替换为 memblock_phys_alloc()。
  2. 移除位图初始化:删除 init_bootmem() 调用,改用 memblock_add() 添加内存区域。
  3. 调整预留顺序:确保页表缓冲池在 memblock_reserve() 之后分配,避免覆盖内核代码。
  4. 验证对齐: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 边界。