前言
经过上一篇[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 内存信息;