参考文档:
感谢作者的无私分享。
我们住关注 start_kernel 中关于内存管理的部分总览:
asmlinkage __visible void __init start_kernel(void)
{
.....
setup_arch(&command_line);
mm_init_cpumask(&init_mm);
.....
setup_per_cpu_areas();
.....
build_all_zonelists(NULL, NULL);
page_alloc_init();
.....
/*
* These use large bootmem allocations and must precede
* mem_init();
* kmem_cache_init();
*/
mm_init();
.....
kmem_cache_init_late();
.....
kmemleak_init();
setup_per_cpu_pageset();
.....
rest_init();
}
函数 | 功能 |
setup_arch | 是一个特定于体系结构的设置函数, 其中一项任务是负责初始化自举分配器 |
mm_init_cpumask | 初始化CPU屏蔽字 |
setup_per_cpu_areas | 给每个CPU分配内存,拷贝.data.percpu段的数据. 为系统中的每个CPU的per_cpu变量申请空间 |
build_all_zonelists | 建立并初始化结点和内存域的数据结构 |
mm_init | 建立了内核的内存分配器, 停用bootmem分配器并迁移到实际的内存管理器(比如伙伴系统)然后调用kmem_cache_init函数初始化内核内部用于小块内存区的分配器 |
kmem_cache_init_late | 在kmem_cache_init之后, 完善分配器的缓存机制, 当前3个可用的内核内存分配器,slab,slob,slub都会定义此函数 |
kmemleak_init | Kmemleak工作于内核态,Kmemleak 提供了一种可选的内核泄漏检测,其方法类似于跟踪内存收集器。当独立的对象没有被释放时,其报告记录在/sys/kernel/debug/kmemleak中, Kmemcheck能够帮助定位大多数内存错误的上下文 |
setup_per_cpu_pageset | 初始化CPU高速缓存行, 为pagesets的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分配由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在CPU中设置一个最近存取页面的高速缓存硬件机制,当进行存储器访问时,先检查要访问的页面是否在高速缓存中. |
1. setup_arch函数初始化内存流程
在 setup_arch 中,设置了与处理器相关的部分,这里我们关注内存相关的部分:
void __init setup_arch(char **cmdline_p)
{
/* 扫描内存并通过 memblock_add 函数将内存添加到 memblock 的 memory */
setup_machine_fdt(__atags_pointer);
/* 初始化memblock */
arm_memblock_init();
/* 分页机制初始化 */
paging_init();
bootmem_init();
}
该函数主要执行了如下操作:
-
使用 arm_memblock_init 来完成memblock机制的初始化工作, 至此 memblock 分配器接受系统中系统中内存的分配工作
-
调用 paging_init 来完成系统分页机制的初始化工作, 建立页表, 从而内核可以完成虚拟内存的映射和转换工作
-
最后调用 bootmem_init 来完成实现 buddy 内存管理所需要的工作
在内存管理还没有起来之前,就要用到内存,这部分是通过早期的简易内存管理系统 memblock 来进行管理,总体的流程如下:
start_kernel()
|---->page_address_init()
| 考虑支持高端内存
| 业务:初始化page_address_pool链表;
| 将page_address_maps数组元素按索引降序插入
| page_address_pool链表;
| 初始化page_address_htable数组.
|
|---->setup_arch(&command_line);
| 初始化特定体系结构的内容
|
|---->arm_memblock_init();
| 初始化引导阶段的内存分配器memblock
----------|----------------------------------------------------------------------
|---->paging_init(); <<<<---- [当前位置] <<<<---- vv
| 分页机制初始化 <<<<---- [当前位置] <<<<---- vv
----------|----------------------------------------------------------------------
|
|---->bootmem_init();
| 始化内存数据结构包括内存节点, 内存域和页帧page
|
|---->arm64_numa_init();
| 支持numa架构
|
|---->zone_sizes_init(min, max);
来初始化节点和管理区的一些数据项
|
|---->free_area_init_node
| 初始化内存节点
|
|---->free_area_init_core
| 初始化zone
|
|---->memmap_init
| 初始化page页面
|
|---->memblock_dump_all();
| 初始化完成, 显示memblock的保留的所有内存信息
|
|---->build_all_zonelist()
| 为系统中的zone建立后备zone的列表.
| 所有zone的后备列表都在
| pglist_data->node_zonelists[0]中;
|
| 期间也对per-CPU变量boot_pageset做了初始化.
|
关于早期的 memblock 的内存管理,参考文章:Linux 内存管理窥探(7):内存初始化(memblock)
2. paging_init 分页机制初始化
接下来就是分页机制的初始化:
void __init paging_init(const struct machine_desc *mdesc)
{
void *zero_page;
prepare_page_table();
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);
}
2.1 prepare_page_table 清除一级页表
在内存使用之前,需要首先清理页表信息,在 prepare_page_table 函数做了这个事情:
static inline void prepare_page_table(void)
{
unsigned long addr;
phys_addr_t end;
/*
* Clear out all the mappings below the kernel image.
*/
for (addr = 0; addr < MODULES_VADDR; addr += PMD_SIZE)
pmd_clear(pmd_off_k(addr));
#ifdef CONFIG_XIP_KERNEL
/* The XIP kernel is mapped in the module area -- skip over it */
addr = ((unsigned long)_exiprom + PMD_SIZE - 1) & PMD_MASK;
#endif
for ( ; addr < PAGE_OFFSET; addr += PMD_SIZE)
pmd_clear(pmd_off_k(addr));
/*
* Find the end of the first block of lowmem.
*/
end = memblock.memory.regions[0].base + memblock.memory.regions[0].size;
if (end >= arm_lowmem_limit)
end = arm_lowmem_limit;
/*
* Clear out all the kernel space mappings, except for the first
* memory bank, up to the vmalloc region.
*/
for (addr = __phys_to_virt(end);
addr < VMALLOC_START; addr += PMD_SIZE)
pmd_clear(pmd_off_k(addr));
}
这里对三段地址使用 pmd_clear 来清理了一级页表的内容:
- 0 ~ MODULES_VADDR
- MODULES_VADDR ~ PAGE_OFFSET
- arm_lowmem_limit ~ VMALLOC_START
2.2 map_lowmem 页表映射
static void __init map_lowmem(void)
{
struct memblock_region *reg;
phys_addr_t kernel_x_start = round_down(__pa(KERNEL_START), SECTION_SIZE);
phys_addr_t kernel_x_end = round_up(__pa(__init_end), SECTION_SIZE);
/* Map all the lowmem memory banks. */
for_each_memblock(memory, reg) {
phys_addr_t start = reg->base;
phys_addr_t end = start + reg->size;
struct map_desc map;
if (memblock_is_nomap(reg))
continue;
if (end > arm_lowmem_limit)
end = arm_lowmem_limit;
if (start >= end)
break;
if (end < kernel_x_start) {
map.pfn = __phys_to_pfn(start);
map.virtual = __phys_to_virt(start);
map.length = end - start;
map.type = MT_MEMORY_RWX;
create_mapping(&map);
} else if (start >= kernel_x_end) {
map.pfn = __phys_to_pfn(start);
map.virtual = __phys_to_virt(start);
map.length = end - start;
map.type = MT_MEMORY_RW;
create_mapping(&map);
} else {
/* This better cover the entire kernel */
if (start < kernel_x_start) {
map.pfn = __phys_to_pfn(start);
map.virtual = __phys_to_virt(start);
map.length = kernel_x_start - start;
map.type = MT_MEMORY_RW;
create_mapping(&map);
}
map.pfn = __phys_to_pfn(kernel_x_start);
map.virtual = __phys_to_virt(kernel_x_start);
map.length = kernel_x_end - kernel_x_start;
map.type = MT_MEMORY_RWX;
create_mapping(&map);
if (kernel_x_end < end) {
map.pfn = __phys_to_pfn(kernel_x_end);
map.virtual = __phys_to_virt(kernel_x_end);
map.length = end - kernel_x_end;
map.type = MT_MEMORY_RW;
create_mapping(&map);
}
}
}
}
为 Kernel Image 创建内存映射,同时为1G空间的低端内存创建内存映射。
Kernel 的属性为可读可写可执行,低端内存为可读可写。然后调用 create_mapping() 来创建直接的物理映射。