0%

Linux内核内存管理 - 初始化C代码中的内存处理概览

这是<Linux内核内存管理>系列的第四篇

第一篇为内核内存管理过程知识点的的简单梳理

第二篇介绍了内核的数据结构

第三篇介绍了从内核第一行代码加载到跳转到C代码前的内存处理。

x86_64体系结构C代码处理

前文我们从汇编代码跳转到了x86_64_start_kernel,该函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
asmlinkage __visible void __init x86_64_start_kernel(char * real_mode_data)
{

BUILD_BUG_ON(MODULES_VADDR < __START_KERNEL_map);
BUILD_BUG_ON(MODULES_VADDR - __START_KERNEL_map < KERNEL_IMAGE_SIZE);
BUILD_BUG_ON(MODULES_LEN + KERNEL_IMAGE_SIZE > 2*PUD_SIZE);
BUILD_BUG_ON((__START_KERNEL_map & ~PMD_MASK) != 0);
BUILD_BUG_ON((MODULES_VADDR & ~PMD_MASK) != 0);
BUILD_BUG_ON(!(MODULES_VADDR > __START_KERNEL));
MAYBE_BUILD_BUG_ON(!(((MODULES_END - 1) & PGDIR_MASK) ==
(__START_KERNEL & PGDIR_MASK)));
BUILD_BUG_ON(__fix_to_virt(__end_of_fixed_addresses) <= MODULES_END);

cr4_init_shadow();
reset_early_page_tables();
clear_bss();
clear_page(init_top_pgt);

sme_early_init();
kasan_early_init();
idt_setup_early_handler();
copy_bootdata(__va(real_mode_data));

load_ucode_bsp();
init_top_pgt[511] = early_top_pgt[511];

x86_64_start_reservations(real_mode_data);
}

首先几个BUILD_BUG_ON 用于检查潜在的配置错误,分别检查的是:

  • 内核模块的虚拟地址位于内核映像之后。
  • 内核模块+内核映像所占空间小于 2^31 (即2GB)
  • 内核映像和内核模块的地址为2MB对齐
  • 固定映射结束地址大于内核模块结束地址

接着初始化CR4 Shadow,内核Check-in List给出其作用如下:

Context switches and TLB flushes can change individual bits of CR4. CR4 reads take several cycles, so store a shadow copy of CR4 in a per-cpu variable. To avoid wasting a cache line, I added the CR4 shadow to cpu_tlbstate, which is already touched during context switches.

也就是说,CR4读取是需要多个CPU时钟周期的,所以将CR4存在一个per-cpu变量内。CR4 Shadow放置于cpu_tlbstate,因为cpu_tlbstate在上下文切换时会被CPU加载到Cache,由此可以节省Cache line的使用。

reset_early_page_tables将early_top_pgt清除并重新加载其为内核页表。clear_bss清理BSS和init_top_pgtsme_early_init是初始化内存加密相关。
kasan_early_init作用是初始化KASAN功能,后续会再具体介绍KASAN,这里略过不表。
idt_setup_early_handler作用是加载IDT Handler,其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
SYM_CODE_START_LOCAL(early_idt_handler_common)

cld

incl early_recursion_flag(%rip)
pushq %rsi /* pt_regs->si */
movq 8(%rsp), %rsi /* RSI = vector number */
movq %rdi, 8(%rsp) /* pt_regs->di = RDI */
pushq %rdx /* pt_regs->dx */
pushq %rcx /* pt_regs->cx */
pushq %rax /* pt_regs->ax */
pushq %r8 /* pt_regs->r8 */
pushq %r9 /* pt_regs->r9 */
pushq %r10 /* pt_regs->r10 */
pushq %r11 /* pt_regs->r11 */
pushq %rbx /* pt_regs->bx */
pushq %rbp /* pt_regs->bp */
pushq %r12 /* pt_regs->r12 */
pushq %r13 /* pt_regs->r13 */
pushq %r14 /* pt_regs->r14 */
pushq %r15 /* pt_regs->r15 */
UNWIND_HINT_REGS

movq %rsp,%rdi /* RDI = pt_regs; RSI is already trapnr */
call do_early_exception

decl early_recursion_flag(%rip)
jmp restore_regs_and_return_to_kernel
SYM_CODE_END(early_idt_handler_common)

上述代码主要作用是寄存器状态保存,同时执行do_early_exception
copy_bootdata的主要作用是检查初始化参数,并将它们复制boot_paramsboot_command_line内。同时将early_top_pgt页表的第512项赋值给init_top_pgt对应项。
最后x86_64_start_reservations执行一些特定平台相关的”quirks”后,开始执行start_kernel

start_kernel

start_kernel执行所有内核初始化代码。本文仅分析与内存管理相关的步骤如下图:
内核初始化的内存处理

  • set_task_stack_end_magic 为内核栈底设置Magic Number,用于栈溢出的检查。
  • page_address_init 初始化page_address_htable链表
  • setup_arch为体系结构相关的初始化代码。X64系统对应的setup_arch定义在arch/x86/kernel/setup.c
    • early_ioremap_init 初始化数组 slot_virt 用于保存虚拟地址和外设物理地址的早期固定映射,其定义在fixmap.h
    • setup_olpc_ofw_pgd 为“One Laptop Per Child”公益项目相关设备初始化PGD。
    • e820__memory_setup 执行 e820__memory_setup_default ,主要作用是从 E820获取硬件内存布局,保存在全局变量e820_table。代码如下:
      1
      2
      3
      4
      5
      6
      7
      static struct e820_table e820_table_init		__initdata;
      static struct e820_table e820_table_kexec_init __initdata;
      static struct e820_table e820_table_firmware_init __initdata;

      struct e820_table *e820_table __refdata = &e820_table_init;
      struct e820_table *e820_table_kexec __refdata = &e820_table_kexec_init;
      struct e820_table *e820_table_firmware __refdata = &e820_table_firmware_init;

注意 initdata和refdata修饰作用在内核代码有说明,其中init的作用是为了标记初始化使用的数据以便内核初始化结束后释放对应的内存。而refdata的用于引用__initdata标记的数据。

1
2
3
4
5
6
7
8
9
#define __init		__section(".init.text") __cold  __latent_entropy __noinitretpoline __nocfi
#define __initdata __section(".init.data")
#define __initconst __section(".init.rodata")
#define __exitdata __section(".exit.data")
#define __exit_call __used __section(".exitcall.exit")

#define __ref __section(".ref.text") noinline
#define __refdata __section(".ref.data")
#define __refconst __section(".ref.rodata")
  • early_reserve_memory 作用是将已占用的内存区域标记为不可用。这样后续不允许被memblock或者伙伴系统分配器再分配。

    • [_text, __end_of_kernel_reserve]
    • [0,64K]
    • setup_data: [hdr.setup_data, sizeof(setup_data)+hdr.setup_data]
    • initrd
    • ibft(iSCSI Boot Format Table) 区域(如果有的话)
    • BIOS区域: [BIOS Start, 0x1000000]
    • etc.
  • memblock_set_bottom_up 标记memblock内存分配是从低地址到高地址

    memblock是系统初始化初期,伙伴系统接管前的分配器,它取代了内核早期的bootmem分配器。

  • e820__reserve_setup_data 将Boot Loader扩展的数据区标记为内核保留区域,并为其分配内存映射。

  • e820__finish_early_params 更新e820表。用户可以通过Loader传入内核CMD line来自定义内存区域映射。下图是在QEMU中E820扫描到的内存映射。
    E820物理内存映射

  • probe_roms 为ROM的分配IO资源

  • insert_resource 将code、rodata、data和bss插入IOMEM资源

  • e820_add_kernel_range 将内核_text 到 _end区域加入到e820表。

  • trim_bios_range 处理一些BIOS识别内存的特殊情况

    • 0到4KB区域没有被BIOS加入到e820中,我们将这段区域加入到e820保留区域。
    • 将BIOS区域中BIOS_BEGIN到BIOS_END(640Kb -> 1Mb) 从e820表中删除。这是因为有些BIOS会将这段区域识别为物理内存(但起始不是)。
  • early_gart_iommu_check 针对早期的AMD处理器中基于GART IOMMU的支持。

  • e820__end_of_ram_pfn 从e820获取最大物理页帧号

  • init_cache_modes 待确定

  • kernel_randomize_memory 与KASLR相关,后续介绍,此处不表

  • early_alloc_pgt_buf 为初始化过程中分配PGT预留堆空间

  • reserve_brk 在Boot分配器Reserve堆空间

  • e820__memblock_setup 将e820内存分布表的数据读出,并填写到Boot分配器管理

    关于memblock分配器memblock,系列后续文章专门介绍

  • e820__memblock_alloc_reserved_mpc_new 从Memblock为MPC Table分配内存。

  • reserve_real_mode 从Memblock为实模式的[0, 1MB]分配内存。

  • init_mem_mapping 待确定

  • memblock_set_current_limit 设置memblock.current_limit为membelcok管理的最大页帧号

  • initmem_init 初始化NUMA(如果开启对应Kconfig的话),为memblock的现有区域分配NUMA节点ID号

  • dma_contiguous_reserve 为DMA预留连续内存

  • reserve_crashkernel 为kernel crash分配内存

  • memblock_find_dma_reserve 计算DMA区域的大小

  • x86_init.paging.pagetable_init 调用 native_pagetable_init 来初始化paging 待确定

  • kasan_init 初始化KASAN

  • sync_initial_page_table 待确定

  • e820__reserve_resources 为e820表项分配IO resource (reserve标记的表项除外)

  • x86_init.resources.reserve_resources 使用reserve_standard_io_resources 为下面硬件端口分配ioport resource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static struct resource standard_io_resources[] = {
{ .name = "dma1", .start = 0x00, .end = 0x1f,
.flags = IORESOURCE_BUSY | IORESOURCE_IO },
{ .name = "pic1", .start = 0x20, .end = 0x21,
.flags = IORESOURCE_BUSY | IORESOURCE_IO },
{ .name = "timer0", .start = 0x40, .end = 0x43,
.flags = IORESOURCE_BUSY | IORESOURCE_IO },
{ .name = "timer1", .start = 0x50, .end = 0x53,
.flags = IORESOURCE_BUSY | IORESOURCE_IO },
{ .name = "keyboard", .start = 0x60, .end = 0x60,
.flags = IORESOURCE_BUSY | IORESOURCE_IO },
{ .name = "keyboard", .start = 0x64, .end = 0x64,
.flags = IORESOURCE_BUSY | IORESOURCE_IO },
{ .name = "dma page reg", .start = 0x80, .end = 0x8f,
.flags = IORESOURCE_BUSY | IORESOURCE_IO },
{ .name = "pic2", .start = 0xa0, .end = 0xa1,
.flags = IORESOURCE_BUSY | IORESOURCE_IO },
{ .name = "dma2", .start = 0xc0, .end = 0xdf,
.flags = IORESOURCE_BUSY | IORESOURCE_IO },
{ .name = "fpu", .start = 0xf0, .end = 0xff,
.flags = IORESOURCE_BUSY | IORESOURCE_IO }
};
  • e820__setup_pci_gap 在0到4GB找到空闲的内存区域,用于PCI设备的IO映射。
  • build_all_zonelists 将所有内存节点的区域加入到对应的zonelist,可以参考数据结构的介绍
  • page_alloc_init 待确定
  • mm_init 为内存管理初始化最核心的部分,由以下部分组成
    • page_ext_init_flatmem
    • init_mem_debugging_and_hardening
    • kfence_alloc_pool
    • report_meminit
    • stack_depot_init
    • mem_init 回收memblock分配的内存(Reserve的除外),接着用mem_init_print_info打印内存信息
    • kmem_cache_init 初始化SLAB分配器
    • kmemleak_init 初始化Kmemleak
    • pgtable_init 为启动后各进程的页表分配建立SLAB
    • debug_objects_mem_init 为debugobject功能分配SLAB内存
    • vmalloc_init
  • kmem_cache_init_late 为SLAB分配器的后续初始化。如果编译时采用的是SLUB,此处无操作。
  • numa_policy_init
  • anon_vma_init

总结

本文是对初始化C代码中内存管理的概览,并没有介绍到每个子部分的细节,后续将会在专门的章节进行具体介绍。