1.2 初期memblock内存分配器

dmesg 的输出结果中,输出的最后一列为 usable 的是实际可用的物理内存地址范围。被标记为 reserved 的内存不能分配使用,可能是内存启动时用来保存内核的一些关键数据和代码,也可能没有实际的物理内存映射到这个范围。也建议大家使用 dmesg 查看下你的 Linux 对物理内存的探测结果。

1.3 初期memblock内存分配器

内核在启动时通过 E820 机制获得到可用的内存地址范围后,还需要将这些内存都管理起来,以应对后面系统运行时的各种功能的内存申请。内存分配器包括两种。刚启动时采用的是初期分配器。这种内存分配器仅仅只为了满足系统启动时间内对内存页的简单管理,管理粒度较粗。另外一种是在系统起来后正常运行时采用的复杂一些但能高效管理 4KB 粒度页面的伙伴系统,是运行时的主要物理页内存分配器。

Dec 23 04:53:10 kernel: [    0.000000] BIOS-e820: [mem 0x0000000100000000-0x000000104fefffff] usable
Dec 23 04:53:10 kernel: [    0.000000] BIOS-e820: [mem 0x000000104ff00000-0x000000104fffffff] reserved
Dec 23 04:53:10 kernel: [    0.000000] BIOS-e820: [mem 0x0000001050000000-0x000000204fefffff] usable
Dec 23 04:53:10 kernel: [    0.000000] BIOS-e820: [mem 0x000000204ff00000-0x000000204fffffff] reserved
Dec 23 04:53:10 kernel: [    0.000000] BIOS-e820: [mem 0x0000002050000000-0x000000304fefffff] usable
Dec 23 04:53:10 kernel: [    0.000000] BIOS-e820: [mem 0x000000304ff00000-0x000000304fffffff] reserved
Dec 23 04:53:10 kernel: [    0.000000] BIOS-e820: [mem 0x0000003050000000-0x000000404f2fffff] usable
Dec 23 04:53:10 kernel: [    0.000000] BIOS-e820: [mem 0x000000404f300000-0x000000404fffffff] reserved
Dec 23 04:53:10 kernel: [    0.000000] BIOS-e820: [mem 0x0000004050000000-0x000000504fefffff] usable
Dec 23 04:53:10 kernel: [    0.000000] BIOS-e820: [mem 0x000000504ff00000-0x000000504fffffff] reserved
Dec 23 04:53:10 kernel: [    0.000000] BIOS-e820: [mem 0x0000005050000000-0x000000604fefffff] usable
Dec 23 04:53:10 kernel: [    0.000000] BIOS-e820: [mem 0x000000604ff00000-0x000000604fffffff] reserved
Dec 23 04:53:10 kernel: [    0.000000] BIOS-e820: [mem 0x0000006050000000-0x000000704fefffff] usable
Dec 23 04:53:10 kernel: [    0.000000] BIOS-e820: [mem 0x000000704ff00000-0x000000704fffffff] reserved
Dec 23 04:53:10 kernel: [    0.000000] BIOS-e820: [mem 0x0000007050000000-0x000000804fefffff] usable
Dec 23 04:53:10 kernel: [    0.000000] BIOS-e820: [mem 0x000000804ff00000-0x000000804fffffff] reserved
......

在初期分配器中,在 Linux 的早期版本采用的是 bootmem。但在 2010 年之后,就慢慢替换成了 memblock 内存分配器。关于这个替换参见这个历史 commit:https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/mm/memblock.c?id=95f72d1ed41a66f1c1c29c24d479de81a0bea36f。本书中我们只介绍较新的 memblock 分配器。

1.3.1 memblock内存分配器的创建

内核在通过 E820 机制检测到可用的内存地址范围后,调用 e820__memory_setup 把检测结果保存到了 e820_table 全局数据结构中。紧接着下一步就是调用 e820__memblock_setup 创建 memblock 内存分配器。

在看创建 memblock 之前我们先来看看这种内存分配器是长什么样子的。memblock 的实现非常简单,就是按照检测到的内存地址范围是 usable 还是 reserved 分成两个对象,然后分别用 memblock_region 数组给存起来。memblock 分配器定义相关的源码位于 mm/memblock.c 文件下。

//file:arch/x86/kernel/setup.c
void __init setup_arch(char **cmdline_p)
{
  ...
  // 保存物理内存检测结果
  e820__memory_setup();
  ...
  // membloc内存分配器初始化
  e820__memblock_setup();
}
//file:mm/memblock.c
struct memblock memblock __initdata_memblock = {
  .memory.regions   = memblock_memory_init_regions,
  .memory.cnt   = 1,  
  .memory.name    = "memory",
  .reserved.regions = memblock_reserved_init_regions,
  .reserved.cnt   = 1,  
  .reserved.name    = "reserved",
  .....
}
 
#define INIT_MEMBLOCK_REGIONS     128
#define INIT_MEMBLOCK_RESERVED_REGIONS    INIT_MEMBLOCK_REGIONS
#define INIT_MEMBLOCK_MEMORY_REGIONS    INIT_MEMBLOCK_REGIONS
 
static struct memblock_region 
memblock_memory_init_regions[INIT_MEMBLOCK_MEMORY_REGIONS] __initdata_memblock;
static struct memblock_region 
memblock_reserved_init_regions[INIT_MEMBLOCK_RESERVED_REGIONS] __initdata_memblock;

e820__memblock_setup 会根据 e820_table 来对 memblock 内存分配器进行创建。

创建过程是遍历 e820 table 中的每一段内存区域。判断如果是预留内存就调用 memblock_reserve 添加到 reserved 成员中,也就是预留内存列表。添加过程是会修改 reserved 中的区域数量 cnt,然后在设置 regions 中的一个元素。如果是可用内存就调用 memblock_add 添加到 memory 成员中,也就是可用内存列表,添加过程同上。

memblock 创建完成后,紧接着还调用 memblock_dump_all() 进行了一次打印输出。这个输出信息对于我们观察 memblock 的创建过程非常的有帮助。Linux 内核会把启动时的各种日志信息记录下来,后面可以使用 dmesg 命令来查看。不过 memblock_dump_all 输出的信息要需要修改 Linux 启动参数,添加 memblock=debug 并重启才可以。

//file:arch/x86/kernel/e820.c
void __init e820__memblock_setup(void)
{
  ...
  for (i = 0; i < e820_table->nr_entries; i++) {
    struct e820_entry *entry = &e820_table->entries[i];
    ...
    if (entry->type == E820_TYPE_SOFT_RESERVED)
      memblock_reserve(entry->addr, entry->size);
    memblock_add(entry->addr, entry->size);
  }
  // 打印 memblock 创建结果
  memblock_dump_all();
}

我的修改方式是编辑 /boot/grub/grub.cfg 文件找到启动参数行,在最后面添加 “memblock=debug”(不同的发行版可能修改方式会有一些出入)。

# vi /boot/grub/grub.cfg
......
linux   /boot/vmlinuz-5.4.143.bsk.8-amd64 ...... memblock=debug

重启后通过查看 /proc/cmdline 输出中是否包含了 “memblock=debug” 来确认开启生效。

# cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-5.4.143.bsk.8-amd64 ...... memblock=debug

然后就可以通过 dmesg 可以看到 Linux 启动时的 memblock 内存分配器输出的日志信息了。

[    0.010238] MEMBLOCK configuration:
[    0.010239]  memory size = 0x00000003fff78c00 reserved size = 0x0000000003c6d144
[    0.010240]  memory.cnt  = 0x3
[    0.010241]  memory[0x0] [0x0000000000001000-0x000000000009efff], 0x000000000009e000 bytes flags: 0x0
[    0.010243]  memory[0x1] [0x0000000000100000-0x00000000bffd9fff], 0x00000000bfeda000 bytes flags: 0x0
[    0.010244]  memory[0x2] [0x0000000100000000-0x000000043fffffff], 0x0000000340000000 bytes flags: 0x0
[    0.010245]  reserved.cnt  = 0x4
[    0.010246]  reserved[0x0] [0x0000000000000000-0x0000000000000fff], 0x0000000000001000 bytes flags: 0x0
[    0.010247]  reserved[0x1] [0x00000000000f5a40-0x00000000000f5b83], 0x0000000000000144 bytes flags: 0x0
[    0.010248]  reserved[0x2] [0x0000000001000000-0x000000000340cfff], 0x000000000240d000 bytes flags: 0x0
[    0.010249]  reserved[0x3] [0x0000000034f31000-0x000000003678ffff], 0x000000000185f000 bytes flags: 0x0
......

1.3.2 向memblock分配器申请内存

内核在启动时伙伴系统创建之前,所有的内存都是通过 memblock 内存分配器来分配的。比较重要的两个使用的场景是 crash kernel 和页管理初始化。

crash kernel 内存申请

内核为了在崩溃时能记录崩溃的现场,方便以后排查分析,设计实现了一套 kdump 机制。kdump 机制实际上在服务器上启动了两个内核,第一个是正常使用的内核,第二个是崩溃发生时的应急内核。有了 kdump 机制,发生系统崩溃的时候 kdump 使用 kexec 启动到第二个内核中运行。这样第一个内核中的内存就得以保留下来了。然后可以把崩溃时的所有运行状态都收集到 dump core 中。

本书中我们不对 kdump 机制过多展开,我们想重点说的是这套机制是需要额外的内存才能工作的。通过 reserve_crashkernel_lowreserve_crashkernel 两个函数向 memblock 内存分配器申请内存。

这两个函数在申请完内存后把信息通过日志的方式打印出来了,在 dmesg 的输出中可以看到。

//file:arch/x86/kernel/setup.c
static int __init reserve_crashkernel_low(void)
{
  ...
  // 申请内存
  low_base = memblock_phys_alloc_range(low_size, CRASH_ALIGN, 0, CRASH_ADDR_LOW_MAX);
  pr_info("Reserving %ldMB of low memory at %ldMB for crashkernel (low RAM limit: 
%ldMB)\n",
    (unsigned long)(low_size >> 20),
    (unsigned long)(low_base >> 20),
    (unsigned long)(low_mem_limit >> 20));
  ...
}
 
static void __init reserve_crashkernel(void)
{
  ...
  // 申请内存
  low_base = memblock_phys_alloc_range(low_size, CRASH_ALIGN, 0, CRASH_ADDR_LOW_MAX);
  pr_info("Reserving %ldMB of memory at %ldMB for crashkernel (System RAM: %ldMB)\n",
    (unsigned long)(crash_size >> 20),
    (unsigned long)(crash_base >> 20),
    (unsigned long)(total_mem >> 20));
}
......

在我的这台虚拟机中,总共为 crash kernel 预留了两个 128MB,总共 512MB 的内存。这些内存会一直被占用,我们自己的用户程序无法使用。

[    0.010832] Reserving 128MB of low memory at 2928MB for crashkernel (System low RAM: 3071MB)
[    0.010835] Reserving 128MB of memory at 17264MB for crashkernel (System RAM: 16383MB)

页管理初始化

将来 Linux 的伙伴系统是按页的方式来管理所有的物理内存的,页的大小是 4KB。每一个页都需要使用一个 struct page 对象来表示。这个对象也是需要消耗内存的。在不同的版本中,struct page 的大小不一样,一般大小是 64 字节。

页管理机制的初始化具体函数是 paging_init,具体的执行路径是在 start_kernel -> setup_arch -> x86_init.paging.pagetable_init -> paging_init。在 paging_init 这个函数中为所有的页面都申请一个 struct page 对象。将来通过这个对象来对页面进行管理。

内存页管理模型也经过了几代的变化,在最早的时候,采用的是 FLAT 模型、中间还经历了 DISCONTIG 模型,现在都默认采用了 SPARSEMEM 模型。SPARSEMEM 模型在内存中就是一个二维数组。

在这个二维数组中,通过层层包装,最后包含的最小单元就是表示内存的 struct page 对象。

//file:include/linux/mm_types.h
struct page {
  unsigned long flags;
  ...
}

整个内存初始化的调用链:

start_kernel
-> setup_arch
---> e820__memory_setup   // 内核把物理内存检测保存从boot_params.e820_table保存到e820_table中,并打印出来
---> e820__memblock_setup // 根据e820信息构建memblock内存分配器,开启调试能打印
---> x86_init.paging.pagetable_init(native_pagetable_init)
-----> paging_init        // 页管理机制的初始化
->mm_init
--->mem_init
-----> memblock_free_all  // 向伙伴系统移交控制权
//file:mm/sparse.c
#ifdef CONFIG_SPARSEMEM_EXTREME
struct mem_section **mem_section;
#else
struct mem_section mem_section[NR_SECTION_ROOTS][SECTIONS_PER_ROOT]
  ____cacheline_internodealigned_in_smp;
#endif
EXPORT_SYMBOL(mem_section);

图像参考

此部分在原始页面中对应图 83(第 15 页),图示为 memblock 内存分配器的结构或创建流程。由于无实际图像,此处保留文字说明。

memblock | E820 | bootmem | crash kernel | kdump | kdump | struct page | SPARSEMEM