B07 —— 从 e820 到内存管理初始化:memblock allocator

金句:内核启动的第一步,是搞清楚自己有多少内存可以管理。


内核如何获取内存信息

内核启动时,通过两种方式获取物理内存信息:

Bash
1. 直接从 BIOS 获取(legacy)
   - 通过 INT 15h E820h 调用
   - 返回 e820 内存映射表

2. 从 bootloader 传递(UEFI)
   - UEFI bootloader 已经初始化了内存
   - 通过 boot_params 结构体传递(Linux Boot Protocol)

Linux 的做法:
  - 启动参数 boot_params 包含 e820map
  - 内核解析 e820map,构建 memblock
  - 最终 memblock 初始化 buddy system

memblock 的数据结构

memblock 是 Linux 早期使用的内存分配器(现在被简化的 version 取代):

Bash
memblock 的核心结构:

struct memblock {
    struct memblock_type memory;   // 可用物理内存区域
    struct memblock_type reserved; // 已分配/保留区域
};

struct memblock_type {
    unsigned long cnt;             // 区域数量
    unsigned long max;            // 数组容量
    phys_addr_t total_size;        // 总大小
    struct memblock_region *regions; // 区域数组
};

struct memblock_region {
    phys_addr_t base;              // 起始地址
    phys_addr_t size;             // 大小
    unsigned long flags;          // MEMBLOCK_* 标志
    const char *name;              // 区域名称
};

flags 取值:
  MEMBLOCK_NONE = 0
  MEMBLOCK_HOTPLUG = 1(热插拔内存)
  MEMBLOCK_MIRROR = 2(镜像内存)
  MEMBLOCK_NOMAP = 4(不映射,不能直接使用)

memblock 的初始化过程

Bash
memblock 初始化流程(Linux 6.1+):

1. early_init_fdt_scan_reserved_mem()
   - 扫描 Device Tree(ARM/ARM64)
   - 读取 reserved-memory 节点
   - 加入 memblock.reserved

2. early_init_fdt_scan_reserved_mem()
   - 也适用于 x86(如果通过 FDT 传递)

3. e820__memory_setup()  // x86
   - 解析 e820 表
   - 把 Type=1(可用内存)加入 memblock.memory
   - 把 Type=2(保留)加入 memblock.reserved

4. memblock_add()
   - 把内存区域加入 memblock.memory
   - 合并相邻区域(如果有)
   - 更新 total_size

5. memblock_reserve()
   - 保留内核代码/数据(0~1MB)
   - 保留 initrd(如果有)
   - 加入 memblock.reserved

最终 memblock 结构:
  memory:可用物理内存区域(DRAM)
  reserved:已分配的/保留的区域

e820 到 memblock 的转换

Bash
e820 表到 memblock 的代码(简化):

void __init e820__memory_setup(void)
{
    char *addr = boot_params.e820_table;
    struct e820_entry *entry;
    int i;

    for (i = 0; i < boot_params.e820_entries; i++) {
        entry = &addr[i * sizeof(struct e820_entry)];

        if (entry->type == E820_TYPE_MEMORY) {
            // 可用内存,加入 memblock.memory
            memblock_add(entry->addr, entry->size);
        } else if (entry->type == E820_TYPE_RESERVED) {
            // 保留区域,加入 memblock.reserved
            memblock_reserve(entry->addr, entry->size);
        }
        // E820_TYPE_ACPI 和 E820_TYPE_NVS 需要特殊处理
    }
}

e820_entry 的类型:
  #define E820_TYPE_MEMORY   1  // 可用物理内存
  #define E820_TYPE_RESERVED 2  // 保留/不可用
  #define E820_TYPE_ACPI     3  // ACPI 表,可回收
  #define E820_TYPE_NVS      4  // ACPI NVS,不可覆盖

早期内存分配:bootmem

在 buddy system 初始化之前,内核用 bootmem 分配器来管理早期内存分配:

Bash
bootmem 的工作方式:

bootmem 是一个简单的"位图+链表"分配器:
  - 维护一个位图,每 bit 对应一个 4KB 页框
  - 分配:找到连续的 1(空闲位),标记为 0
  - 释放:把对应的位从 0 改回 1

初始化(arch/x86/mm/init.c):

void __init init_bootmem(void)
{
    min_low_pfn = 0;
    max_pfn = e820__end_of_low_ram_pfn();

    // 分配位图,大小 = max_pfn / 8 bytes
    bootmem_map = alloc_bootmem_pages(PFN_BYTES(max_pfn));

    // 初始化位图,全部置 1(空闲)
    memset(bootmem_map, 0xff, PFN_BYTES(max_pfn));
}

bootmem 分配函数:

void * __init alloc_bootmem_pages(unsigned long size)
{
    // 找到第一个满足大小的连续空闲区域
    // 标记为已用
    // 返回物理地址
}

Bash
bootmem 的问题(为什么要替换成 memblock):

1. 分配算法 O(n)
   - 最坏情况扫描整个位图
   - 256GB 内存 = 64M 个页框 = 8MB 位图
   - 每次分配扫描 8MB,效率低

2. 碎片化
   - 多次分配释放后,位图碎片化
   - 无法找到大块连续空间

3. 无法保留内存区域
   - memblock 可以把区域标记为"保留"
   - bootmem 只能分配,不能区分"已分配""保留"

替代方案:
  - 早期用 bootmem,buddy system 就绪后迁移
  - 现代 Linux(5.x+)用更简单的 memblock allocator(无 bootmem)

memblock 分配函数

Bash
memblock 核心分配函数:

1. memblock_alloc(phys_addr_t size, phys_addr_t align)
   - 分配指定大小的内存,返回物理地址
   - align:对齐要求

2. memblock_alloc_try_nid()
   - 尝试在不同 NUMA 节点分配
   - 用于 NUMA 系统的本地内存分配

3. memblock_reserve(phys_addr_t base, phys_addr_t size)
   - 保留一段内存区域(标记为已用)
   - 内核启动时保留代码和数据

4. memblock_remove(phys_addr_t base, phys_addr_t size)
   - 从 memblock.memory 中移除一段
   - 用于标记热插拔内存或损坏的内存

实际使用:

  // 保留低端 1MB(BIOS 区域)
  memblock_reserve(0x0, 0x100000);

  // 保留内核镜像(TEXT + DATA)
  memblock_reserve(__pa_symbol(_text),
                   _end - _text);

  // 分配 2MB 的 boot page table
  page_table = memblock_alloc(PAGE_SIZE * 512, PAGE_SIZE);

从 memblock 到 buddy system 的迁移

Bash
buddy system 就绪后,bootmem 被废弃:

page_alloc_init() // 初始化伙伴系统

  - 读取 memblock.memory 中所有可用区域
  - 对每个区域:
    1. 调用 free_bootmem_page() 加入 buddy system
    2. 逐页释放给伙伴系统

  // 伪代码
  for each region in memblock.memory:
      start_pfn = __phys_to_pfn(region.base);
      end_pfn = __phys_to_pfn(region.base + region.size);

      for pfn in [start_pfn, end_pfn):
          free_bootmem_page(pfn_to_page(pfn));

完成后,buddy system 完全接管物理内存分配:
  - 分配:alloc_pages(gfp_mask, order)
  - 释放:__free_pages(page, order)
  - memblock 可以继续存在(用于早期分配),也可以移除

/proc/meminfo 和 /proc/buddyinfo

Bash
Linux 查看内存状态:

$ cat /proc/meminfo
MemTotal:       32768176 kB   // 总物理内存
MemFree:        23456789 kB   // 空闲
MemAvailable:   28901234 kB   // 可用(包含缓存)
Buffers:           123456 kB   // 块设备缓存
Cached:           5678901 kB   // 页缓存
SwapCached:           2345 kB   // 交换缓存

$ cat /proc/buddyinfo
Node 0, zone      DMA      free pages 1234/5678
Node 0, zone    DMA32    free pages 23456/34567
Node 0, zone   Normal    free pages 78901/123456

各列 = Order 0~Order 10 的空闲页数:
  - DMA:低端 16MB(Order 0~10)
  - DMA32:16MB~4GB(只有 Order 0~9 适用于 32-bit)
  - Normal:4GB 以上

总结

  • memblock:早期内存分配器,存储可用内存和保留区域(memblock.memory + memblock.reserved)
  • e820 → memblock:BIOS e820 表(Type=1 加入 memory,Type=2 加入 reserved)→ memblock_add
  • bootmem:早期位图分配器(O(n) 扫描),buddy system 就绪后废弃
  • memblock_alloc:早期分配函数(buddy 之前),对齐分配
  • 迁移:buddy 就绪后,遍历 memblock.memory,逐页 free_bootmem_page 给 buddy
  • /proc/meminfo:显示总内存/空闲/可用/缓存;/proc/buddyinfo:显示各 Order 的空闲页数

下篇预告(B08):内存初始化后,内核是怎么管理进程地址空间的?vmalloc、kmalloc、以及用户空间和内核空间的内存布局。


关注公众号「AI不着急」,回复”资料”获取内存学习路线图。

最后修改: 2024年12月5日

作者

评论

发表评论

您的邮箱地址不会被公开。