转自:https://zhuanlan.zhihu.com/p/355205941
介绍完内存初始化过程中最为重要的一个数据结构后,我们就正式开始跟着代码从start_kernel一步一步了解内存初始化的整个流程。我们再次借用初始化第一章节的代码流程图。
setup_arch是一个特定于体系结构的设置函数。
void __init setup_arch(char **cmdline_p) { /* * 重要数据结构,内核通过machine_desc结构来控制系统体系架构相关部分的初始化 * machine_desc机构提的成员包含了体系架构相关部分的几个最重要的初始化函数 * 包括map_io、init_irq、init_machine、phys_io、timer等 */ const struct machine_desc *mdesc; ... mdesc = setup_machine_fdt(__atags_pointer); ... }
setup_machine_fdt函数用于获取内核前期初始化所需的bootargs,cmdline等系统引导参数 。
const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys) { ... early_init_dt_scan_nodes(); ... } void __init early_init_dt_scan_nodes(void) { /* Retrieve various information from the /chosen node */ of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line); /* Initialize {size,address}-cells info */ of_scan_flat_dt(early_init_dt_scan_root, NULL); /* Setup memory, calling early_init_dt_add_memory_arch */ of_scan_flat_dt(early_init_dt_scan_memory, NULL); }
early_init_dt_scan_nodes函数中通过of_scan_flat_dt函数扫描整个设备树,实际动作是在回调函数中完成的。early_init_dt_scan_chosen是对chosen节点的操作,主要是将节点下的bootargs属性的字符串拷贝到boot_command_line指向的内存中。early_init_dt_scan_root是根据节点的#address-cells属性和#size-cells属性初始化全局变量dt_root_size_size_cells和dt_root_addr_cells,如果没有设置属性的话这里就使用默认值。early_init_dt_scan_memory是对内存的初始化。
int __init early_init_dt_scan_memory(unsigned long node, const char *uname,int depth, void *data) { ... base = dt_mem_next_cell(dt_root_addr_cells, ®); size = dt_mem_next_cell(dt_root_size_cells, ®); early_init_dt_add_memory_arch(base, size); }
对于dt_root_addr_cells和dt_root_size_cells的使用,我们可以看出根节点的#address-cells属性和#size-cells属性都是用来描述内存地址和大小的,得到每块内存的起始地址和大小后,再调用early_init_dt_add_memory_arch函数。
void __init early_init_dt_add_memory_arch(u64 base, u64 size) { ... /* Add the chunk to the MEMBLOCK list */ if (add_mem_to_memblock) { if (validate_mem_limit(base, &size)) memblock_add(base, size); } }
在比较完内核对地址和大小的一系列要求后,最后调用memblock_add将内存块加入内存。
setup_dma_zone传递的参数是mdesc,根据mdesc->dma_zone_size设置DMA区域的大小arm_dma_zone_size和DMA区域的结束地址arm_dma_limit。
void __init adjust_lowmem_bounds(void) { ... vmalloc_limit = (u64)(uintptr_t)vmalloc_min - PAGE_OFFSET + PHYS_OFFSET; /* * The first usable region must be PMD aligned. Mark its start * as MEMBLOCK_NOMAP if it isn't * 四级页表结构,PMD对应中间页目录 */ for_each_memblock(memory, reg) { if (!memblock_is_nomap(reg)) { if (!IS_ALIGNED(reg->base, PMD_SIZE)) { phys_addr_t len; len = round_up(reg->base, PMD_SIZE) - reg->base; memblock_mark_nomap(reg->base, len); } break; } } for_each_memblock(memory, reg) { phys_addr_t block_start = reg->base; phys_addr_t block_end = reg->base + reg->size; if (memblock_is_nomap(reg)) continue; if (reg->base < vmalloc_limit) { if (block_end > lowmem_limit) lowmem_limit = min_t(u64, vmalloc_limit, block_end); /* * 找到第一个非pmd对齐的页面,然后将memblock_limit指向该页面。 * 这取决于将限制向下舍入为pmd对齐,此限制发生在此函数的末尾。 * 使用此算法,几乎任何存储体的开始或结束都可以不按PMD对齐。 * 唯一的例外是存储体0的开始必须是部分对齐的, * 因为否则在映射存储体0的开始时就需要分配内存, * 这是在映射任何可用内存之前发生的。 */ if (!memblock_limit) { if (!IS_ALIGNED(block_start, PMD_SIZE)) memblock_limit = block_start; else if (!IS_ALIGNED(block_end, PMD_SIZE)) memblock_limit = lowmem_limit; } } } arm_lowmem_limit = lowmem_limit; high_memory = __va(arm_lowmem_limit - 1) + 1; if (!memblock_limit) memblock_limit = arm_lowmem_limit; ... }
adjust_lowmem_bounds负责为lowmem/highmem设置边界。它需要先进行系统设置,然后才能进行内存块保留。在进行内存块保留时,也可以从系统中删除内存。低内存/高内存边界和内存尾部可能会受到删除操作的影响,但删除后内存并未重新计算。虽然在某些系统上,这种情况是无害的,而在其他系统上,这可能导致将错误的范围传递给主内存分配器。所以在完成所有保留后,需要通过重新计算lowmem/highmem边界来更正此问题。这也就是为什么adjust_lowmem_bounds函数会被调用两次。
void __init arm_memblock_init(const struct machine_desc *mdesc) { /* Register the kernel text, kernel data and initrd with memblock. */ /* 预留内核镜像内存,其中包括.text,.data,.init */ memblock_reserve(__pa(KERNEL_START), KERNEL_END - KERNEL_START); arm_initrd_init(); /* * 预留vector page内存 * 如果CPU支持向量重定向(控制寄存器的V位),则CPU中断向量被映射到这里。 */ arm_mm_memblock_reserve(); /* reserve any platform specific memblock areas */ if (mdesc->reserve) mdesc->reserve(); //预留架构相关的内存,这里包括内存屏障和安全ram early_init_fdt_reserve_self(); //预留设备树自身加载所占内存 early_init_fdt_scan_reserved_mem(); //初始化设备树扫描reserved-memory节点预留内存 /* reserve memory for DMA contiguous allocations */ dma_contiguous_reserve(arm_dma_limit); //内核配置参数或命令行参数中预留的DMA连续内存 arm_memblock_steal_permitted = false; memblock_dump_all(); }
由arm_memblock_init函数可以看出设置保留内存的4种方法:
void __init paging_init(const struct machine_desc *mdesc) { void *zero_page; /* * prepare_page_table() * 在内存使用之前,需要首先清理页表信息。 * 在prepare_page_table函数中对三段地址使用pmd_clear来清理一级页表的内容 * 0~MODULES_VADDR,MODULES_VADDR~PAGE_OFFSET,arm_lowmem_limit~VMALLOC_START */ prepare_page_table(); /* * map_lowmem() * 将lowmem部分的一级页表即PGD页表填充初始化。 * 1MB对齐部分的物理内存会被初始化PGD中,不足1MB的会通过PTE来映射。 * 对此,boot阶段初始化的页表就被覆盖了。 */ map_lowmem(); memblock_set_current_limit(arm_lowmem_limit); dma_contiguous_remap(); early_fixmap_shutdown(); devicemaps_init(mdesc); kmap_init(); tcm_init(); top_pmd = pmd_off_k(0xffff0000); /* allocate the zero page. */ zero_page = early_alloc(PAGE_SIZE); bootmem_init(); empty_zero_page = virt_to_page(zero_page); __flush_dcache_page(NULL, empty_zero_page); /* Compute the virt/idmap offset, mostly for the sake of KVM */ kimage_voffset = (unsigned long)&kimage_voffset - virt_to_idmap(&kimage_voffset); }
paging_init主要完成初始化内核的分页机制,通过对boot阶段页表的覆盖,并填充新的一级页表,这样我们的虚拟内存空间就初步建立,并可以完成物理地址到虚拟地址的映射工作了。
在paging_init中最为重要的函数要数bootmem_init(),接下来我们来详细介绍一下bootmem_init。
void __init bootmem_init(void) { unsigned long min, max_low, max_high; memblock_allow_resize(); max_low = max_high = 0; /* 通过find_linits找出物理内存开始帧号、结束帧号和NORMAL区域的结束帧号 */ find_limits(&min, &max_low, &max_high); early_memtest((phys_addr_t)min << PAGE_SHIFT, (phys_addr_t)max_low << PAGE_SHIFT); /* * Sparsemem tries to allocate bootmem in memory_present(), * so must be done after the fixed reservations */ /* * 遍历所有memory region,每个memory region分成1G大小的section,并设置section在位 * 函数中调用memory_present函数: * sparse_index_init(section,nid): * 遍历所有的section,为其分配“struct mem_section”实例,需要注意 * 1.如果memory region不是按照section对齐的,那么最后一个section会有空洞,即没有对应的物理页 * 2.SECTIONS_PER_ROOT即一个物理页面可以存放多少“struct mem_section”实例,由于目前内存是按照物理页面来管理的, * 所以一次会分配一个物理页面来存放“struct mem_section”实例,称为一个ROOT, * 如果“struct mem_section”实例很多的话,可能需要分配多个物理页面。 * ms->section_mem_map = sparse_encode_early_nid(nid) | SECTION_MARKED_PRESENT: * 用来设置section在位 */ arm_memory_present(); /* * sparse_init() needs the bootmem allocator up and running. */ /* 初始化section机制 * 初始化mem_section数组使之与每一个section映射 * 初始化section_mem_map与page映射 */ sparse_init(); /* * Now free the memory - free_area_init_node needs * the sparse mem_map arrays initialized by sparse_init() * for memmap_init_zone(), otherwise all PFNs are invalid. */ zone_sizes_init(min, max_low, max_high); /* * This doesn't seem to be used by the Linux memory manager any * more, but is used by ll_rw_block. If we can get rid of it, we * also get rid of some of the stuff above as well. */ min_low_pfn = min; max_low_pfn = max_low; max_pfn = max_high; }
bootmem_init函数中提到了一个新的机制——Sparsemem Memory Model。之前在基础知识中我们学过,内存的最基本单位是page,但在Sparse Memory模型中,section是管理内存online/offline的最小内存单元。在添加此模型的补丁中,作者描述了该模型的几大优势:
section就是几个page组合而成,比page更大一些的内存区域,但又比node的范围要小。这样整个系统的物理内存就被分成一个个section,并由mem_section结构体表示。而这个结构体中保存了该section范围的struct page结构体的地址。
bootmem_init函数中另一个重要的函数是zone_sizes_init, 先看以下zone_sizes_init的函数调用图:
pagin_init完成了分页机制的初始化,然后bootmem_init完成了内存结点和内存域的初始化工作,此时,数据结构已经基本准备完毕,之后要做的就是将所有节点的内存域都链入到zonelists中,方便后面内存分配的工作。
build_all_zonelists中将大部分内存相关的工作都交给了__build_all_zonelists,后者又对系统中的各个NUMA结点分别调用了build_zonelists。
static void __build_all_zonelists(void *data) { int nid; int __maybe_unused cpu; pg_data_t *self = data; static DEFINE_SPINLOCK(lock); spin_lock(&lock); #ifdef CONFIG_NUMA memset(node_load, 0, sizeof(node_load)); #endif /* * This node is hotadded and no memory is yet present. So just * building zonelists is fine - no need to touch other nodes. */ if (self && !node_online(self->node_id)) { build_zonelists(self); } else { for_each_online_node(nid) { pg_data_t *pgdat = NODE_DATA(nid); build_zonelists(pgdat); } ... } spin_unlock(&lock); }
for_each_online_node遍历了系统中所有的活动结点。如果是UMA系统只有一个结点,build_zonelists只调用了一次,就对所有的内存创建了内存域列表。NUMA系统调用该函数的次数等于结点的个数,每次调用都会对一个不同的结点生成内存域数据。build_zonelists传入的参数是一个指向pgdat_t实例的指针参数,该数据结构包含了结点的所有信息。由于UMA和NUMA架构下结点的层次结构有很大的区别,因此,内核分别提供了两套不同的build_zonelists接口。但大体实现方法都是通过for循环遍历所有结点并加入到zonelists中。
参考资料