前言

经过上一篇[linux内存管理] 第005篇 启动代码分析之汇编部分,最终执行bl start_kernel开始了C语言的世界。

start_kernel函数负责的功能很多,这个阶段会详细分析start_kernel的各个重要的函数,而本篇会对start_kernel做一个全局的简单介绍。

start_kernel中内存管理代码流程


----start_kernel()                         // init/main.c
 
    ----setup_arch()                       // arch/arm64/kernel/setup.c
        ----early_fixmap_init()            // arch/arm64/mm/mmu.c
        ----early_ioremap_init()           // arch/arm64/mm/ioremap.c
        ----setup_machine_fdt()            // arch/arm64/kernel/setup.c
        ----arm64_memblock_init()          // arch/arm64/mm/init.c
        ----paging_init()                  // arch/arm64/mm/mmu.c
        ----unflatten_device_tree()        // drivers/of/fdt.c
        ----bootmem_init()                 // arch/arm64/mm/init.c
            ----memblocks_present()        // mm/sparse.c
            ----sparse_init()              // mm/sparse.c
            ----zone_sizes_init()          // arch/arm64/mm/init.c
            ----memblock_dump_all()        // include/linux/memblock.h
        ----early_ioremap_reset()          // mm/early_ioremap.c
 
    ----setup_nr_cpu_ids()                 //percpu需要知道cpu的core数量,详细见percpu
    ----setup_per_cpu_areas()              // mm/percpu.c
 
    ----build_all_zonelists()              // mm/page_alloc.c
 
    ----page_alloc_init()                  // mm/page_alloc.c
 
    ----mm_init()                          // init/main.c
 
    ----ftrace_init()
 
    ----kmem_cache_init_late()             // mm/slub.c
 
    ----setup_per_cpu_pageset()            // mm/page_alloc.c
 
    ----arch_call_rest_init()              // init/main.c
        ----rest_init()
            ----kernel_init()

setup_arch函数

void __init __no_sanitize_address setup_arch(char **cmdline_p)
{
	///初始化,init_mm的地址,定义在arch/arm64/kernel/vmlinux.lds
	setup_initial_init_mm(_stext, _etext, _edata, _end);

	*cmdline_p = boot_command_line;

	/*
	 * If know now we are going to need KPTI then use non-global
	 * mappings from the start, avoiding the cost of rewriting
	 * everything later.
	 */
	arm64_use_ng_mappings = kaslr_requires_kpti();

	///注意这里都是创建的静态页表,因为内存子系统尚未建立(伙伴系统还没开始工作)
	early_fixmap_init();   ///fixmap区映射,只创建中间级转换页表,最后一级页表未创建
	early_ioremap_init();  ///早期ioremap映射,依赖fixmap转换表

	setup_machine_fdt(__fdt_pointer);///设备树映射,读取内存信息后,填入memblock系统,用于后面伙伴系统

	/*
	 * Initialise the static keys early as they may be enabled by the
	 * cpufeature code and early parameters.
	 */
	jump_label_init();
	parse_early_param();

	/*
	 * Unmask asynchronous aborts and fiq after bringing up possible
	 * earlycon. (Report possible System Errors once we can report this
	 * occurred).
	 */
	local_daif_restore(DAIF_PROCCTX_NOIRQ);

	/*
	 * TTBR0 is only used for the identity mapping at this stage. Make it
	 * point to zero page to avoid speculatively fetching new entries.
	 */
	cpu_uninstall_idmap();

	xen_early_init();
	efi_init();

	if (!efi_enabled(EFI_BOOT) && ((u64)_text % MIN_KIMG_ALIGN) != 0)
	     pr_warn(FW_BUG "Kernel image misaligned at boot, please fix your bootloader!");

	///整理memblock的内存区域
	arm64_memblock_init();

	///至此,物理内存通过memblock模块添加入了系统,但此时只有dtb,Image所在的两段物理内存可以访问;
	//其他区域的物理内存,还没建立映射,可以通过memblock_alloc分配,但不能访问;
	//接下来通过paging_init建立不能访问的物理区域的页表;
	//
	//paging_init是内存初始化最核心的一步,将完成细粒度内核镜像映射(分别映射每个段),线性映射(内核可以访问整个物理内存)
	paging_init();   ///建立动态页表

	acpi_table_upgrade();

	/* Parse the ACPI tables for possible boot-time configuration */
	acpi_boot_table_init();

	if (acpi_disabled)
		unflatten_device_tree();

	///至此,内核已经可以访问所有物理内存
	///接下来开始初始化内存的关键数据结构,Linux当前默认采用sparse内存模型
	bootmem_init();

	kasan_init();

	request_standard_resources();

	early_ioremap_reset();

	if (acpi_disabled)
		psci_dt_init();
	else
		psci_acpi_init();

	init_bootcpu_ops();
	smp_init_cpus();
	smp_build_mpidr_hash();

	/* Init percpu seeds for random tags after cpus are set up. */
	kasan_init_sw_tags();

#ifdef CONFIG_ARM64_SW_TTBR0_PAN
	/*
	 * Make sure init_thread_info.ttbr0 always generates translation
	 * faults in case uaccess_enable() is inadvertently called by the init
	 * thread.
	 */
	init_task.thread_info.ttbr0 = phys_to_ttbr(__pa_symbol(reserved_pg_dir));
#endif

	if (boot_args[1] || boot_args[2] || boot_args[3]) {
		pr_err("WARNING: x1-x3 nonzero in violation of boot protocol:\n"
			"\tx1: %016llx\n\tx2: %016llx\n\tx3: %016llx\n"
			"This indicates a broken bootloader or old kernel\n",
			boot_args[1], boot_args[2], boot_args[3]);
	}
}

参数 cmdline_p

cmdline_p 是从 start_kernel() 中传入的一个临时指针变量,用以获取 boot_command_line 的地址,在 setup_machine_fdt() 函数中解析 chosen 节点时解析 cmdline,最终会在函数 start_kernel() 调用完 setup_arch()之后,在 setup_command_line() 中进行保存;

early_fixmap_init()

early_fixmap_init() 完成 fixmap 的初始化工作,本函数为 fixmap 的初始化函数,其根本目的就是将 fixmap 的起始地址 FIXADDR_START 与全局的页表 bm_pud、bm_pmd、bm_pte 进行映射;

early_ioremap_init()

early_ioremap_init() 完成ioremap 的初始化工作;

一般传统驱动模块都是使用 ioremap() 函数来完成地址映射的,但是该函数必须依赖 buddy 系统来创建某个 level 的转换表。一些硬件需要在内存管理系统运行起来之前就需要工作,内核在启动初期采用 early_ioremap() 机制来映射内存给这些硬件驱动使用,并且这些硬件驱动在使用完 early_ioremap() 的地址后需要尽快的释放掉这些内存,这样才能保证其他硬件模块继续使用。因此 early_ioremap() 采用的是 fixed map 的 temporary fixmap 段虚拟地址。

setup_machine_fdt()

setup_machine_fdt() 完成 dtb 物理内存到 FIX_FDT 的映射,并完成chosen、 dtb root、memory 节点的扫描工作,并将 dtb 区域映射为 RO 权限;

arm64_memblock_init()

arm64_memblock_init() 完成 memblock 的初始化,并扫描、解析 dtb 中 reserved-memory 节点,以及 CMA 区域的初始化;

该函数中确定了 physvir_offset 为后续现象映射做准备,也确定了 vmemmap 的起始地址。

通过 max_zone_dama_phys() 调用,确定了 arm64_dma_phys_limit 值为后期zone 初始化做准备。

paging_init()

paging_init() 调用 map_kernel() 将内核镜像映射到内核空间的虚拟地址(位于 vmalloc 区域),虚拟地址与物理地址之间相差 kimage_voffset。另外注意各个段映射的权限不相同。

调用 map_mem() 对 memblock . memory 中所有的 regions 进行线性映射 (被标记 MEMBLOCK_NOMAP 属性的regions 除外)。映射时会创建页表,用以存放映射信息,调用函数 early_pgtable_alloc() 完成。线性映射虚拟地址与物理地址相差 PAGE_OFFSET

需要注意,内核镜像 [ _text, __init_begin) 会被映射两次。

unflatten_device_tree()

unflatten_device_tree() 完成dtb 中其他设备节点、aliases 节点的扫描、展开工作;

bootmem_init()


void __init bootmem_init(void)
{
	unsigned long min, max;
 
    //获取memblock.memory 的起始物理地址和结束物理地址,获取其页帧号
	min = PFN_UP(memblock_start_of_DRAM());
	max = PFN_DOWN(memblock_end_of_DRAM());
 
	early_memtest(min << PAGE_SHIFT, max << PAGE_SHIFT);
 
    //初始化全局变量 max_pfn 和 max_low_pfn,确定物理内存最大的页帧号
	max_pfn = max_low_pfn = max;
 
    //初始化全局变量 min_low_pfn,确定物理内存最小页帧号
	min_low_pfn = min;
 
    //CONFIG_NUMA 使能时调用,详细看 numa.c
	arm64_numa_init();

    //将所有的 memblocks 标记为 present,
    //根据每个memblock regions创建sparse memory模型需要的mem_section
	memblocks_present();
 
    //sparse memory 初始化
	sparse_init();
 
	zone_sizes_init(min, max);
 
    //dump memblock.memory和memblock.reserved 信息
    //当 sparse memory 初始化完成,memblock中的内存分布已经基本完成
	memblock_dump_all();
}

当调用到 bootmem_init() 时, memblock 已经完成了初始化工作,并且完成了 kernel img 的映射 (paging_init 函数中的 map_kernel) 和其他内存的线性映射 (paging_init 函数中的 map_mem)。

函数主要做下面几件事情:

  • 确定 max_pfn 和 min_low_pfn, memblock 已经初始化完成了,到底是普通memory还是 reserved memory,或者是hole,已经 基本上(后面还有些reserved memory 会释放)确定了。
  • memblocks_present() 通过memblock regions 模型创建 mem_section,并标记 mem_section 为 present 状态;
  • sparse_init() 完成 sparse memory 内存模型的初始化,struct page 完成对于 vmemmap 的映射,正式进入 page 管理内存页的时代;
  • zone_sizes_init() 完成zone 初始化;对于 UMA 架构来说,这里第一次接触到 contig_page_data,这是buddy 系统的核心数据结构 pg_data_t。通过该函数对 contig_page_data 进行了初始化,也包括该数据结构管理的 zone 结构。同时,也对每个zone 所管理的page 所对应的 struct page也进行了初始化;
  • memblock_dump_all(), dump 目前系统memblock . memory 和 memblock . reserved 内存信息;