转自:https://zhuanlan.zhihu.com/p/355205941
介绍完内存初始化过程中最为重要的一个数据结构后,我们就正式开始跟着代码从start_kernel一步一步了解内存初始化的整个流程。我们再次借用初始化第一章节的代码流程图。
setup_arch
setup_arch是一个特定于体系结构的设置函数。
setup_machine_fdt
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
setup_dma_zone传递的参数是mdesc,根据mdesc->dma_zone_size设置DMA区域的大小arm_dma_zone_size和DMA区域的结束地址arm_dma_limit。
adjust_lowmem_bounds
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函数会被调用两次。
arm_memblock_init
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种方法:
- machine的reserve接口中设置:mdesc->reserve();
- 设备树reserved-memory节点中设置;
- 配置文件:Device Drivers> Generic Driver Options> DMA Contiguous Memory Allocator;
- 启动参数添加字段 mem=size;
paging_init
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的最小内存单元。在添加此模型的补丁中,作者描述了该模型的几大优势:
- 对于有内存空洞的设备,减少系统struct page使用的内存
- 内存热插拔系统需要使用
- 在NUMA系统上支持内存的重叠
section就是几个page组合而成,比page更大一些的内存区域,但又比node的范围要小。这样整个系统的物理内存就被分成一个个section,并由mem_section结构体表示。而这个结构体中保存了该section范围的struct page结构体的地址。
bootmem_init函数中另一个重要的函数是zone_sizes_init, 先看以下zone_sizes_init的函数调用图:
- calculate_node_totalpages:从名字就可以看出这个函数是用来统计node结点中的页面数的,而统计方法如图所示:
- free_area_init_core:主要完成struct pglist_data结构中的字段函数初始化,比如初始化pglist_data内部使用的锁和队列,并初始化它所管理的各个zone。
build_all_zonelists
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中。
参考资料