看了很多关于 linux 内存管理的文章还是云里雾里,听了很多关于 linux 内存管理的课程还是一头雾水。其实很多时候造成不懂的原因不是资料太少,恰恰是资料太多,而且各个内核版本的差异,32 位 64 位的不同,文章的胡编乱造等都给读者带来疑惑。本着对内存深度剖析的态度,希望以版本 kernel-4.14,架构 AARCH64 为专题做个内存管理的架构性整理。
这篇文章我们先来看下 linux 在启动过程中的初始化。
创建启动页表:
在汇编代码阶段的 head.S 文件中,负责创建映射关系的函数是 create_page_tables。create_page_tables 函数负责 identity mapping 和 kernel image mapping。
identity map:是指把 idmap_text 区域的物理地址映射到相等的虚拟地址上,这种映射完成后,其虚拟地址等于物理地址。idmap_text 区域都是一些打开 MMU 相关的代码。
kernel image map:将 kernel 运行需要的地址(kernel txt、rodata、data、bss 等等)进行映射。
arch/arm64/kernel/head.S:
ENTRY(stext)
bl preserve_boot_args
bl el2_setup // Drop to EL1, w0=cpu_boot_mode
adrp x23, __PHYS_OFFSET
and x23, x23, MIN_KIMG_ALIGN - 1 // KASLR offset, defaults to 0
bl set_cpu_boot_mode_flag
bl __create_page_tables
/*
* The following calls CPU setup code, see arch/arm64/mm/proc.S for
* details.
* On return, the CPU will be ready for the MMU to be turned on and
* the TCR will have been set.
*/
bl __cpu_setup // initialise processor
b __primary_switch
ENDPROC(stext)
复制代码
__create_page_tables 主要执行的就是 identity map 和 kernel image map:
_create_page_tables:
......
create_pgd_entry x0, x3, x5, x6
mov x5, x3 // __pa(__idmap_text_start)
adr_l x6, __idmap_text_end // __pa(__idmap_text_end)
create_block_map x0, x7, x3, x5, x6
/*
* Map the kernel image (starting with PHYS_OFFSET).
*/
adrp x0, swapper_pg_dir
mov_q x5, KIMAGE_VADDR + TEXT_OFFSET // compile time __va(_text)
add x5, x5, x23 // add KASLR displacement
create_pgd_entry x0, x5, x3, x6
adrp x6, _end // runtime __pa(_end)
adrp x3, _text // runtime __pa(_text)
sub x6, x6, x3 // _end - _text
add x6, x6, x5 // runtime __va(_end)
create_block_map x0, x7, x3, x5, x6
复制代码
……
其中调用 create_pgd_entry 进行 PGD 及所有中间 level(PUD, PMD) 页表的创建,调用 create_block_map 进行 PTE 页表的映射。关于四级页表的关系如下图所示,这里就不进一步解释了。
汇编结束后的内存映射关系如下图所示:
当执行完上面的 map 之后,MMU 就已经打开了并且开始进入 C 代码运行阶段,那么下一步就要对 dtb 进行映射了。
fixmap 区之 dtb map:
在执行 setup_arch 中,会最先进行 early_fixmap_init(),这个函数就是用来 map dtb 的,但是它只会建立 dtb 对应的这段物理地址中间 level 的页表 entry,而最后一个 level 的页表映射则通过 setup_machine_fdt 函数里的 fixmap_remap_fdt 来创建。
void *__init fixmap_remap_fdt(phys_addr_t dt_phys)
{
void *dt_virt;
int size;
dt_virt = __fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL_RO);
if (!dt_virt)
return NULL;
memblock_reserve(dt_phys, size);
return dt_virt;
}
复制代码
fixmap_remap_fdt 主要是为 fdt 建立地址映射,在该函数的最后,顺便就调用 memblock_reserve 保留了该段内存。
可以看出 dtb 的映射采用的是 fixmap,所谓 fixmap 就是固定映射,它需要我们明确的知道想要映射的物理地址,并把这段地址映射到想要映射的虚拟地址上。当然这里固定映射还有些片面,因为在 fixmap 机制实现上,也有支持动态分配虚拟地址的功能,这个功能主要用于临时 fixmap 映射(这个临时映射就是用来执行 earlyioremap 使用的。),而 dtb 的映射属于永久映射。
fixmap 区之 early ioremap:
对于一些硬件需要在内存管理系统起来之前就要工作的,我们就可以使用这种机制来映射内存给这些硬件 driver 使用。各个模块在使用完 early ioremap 的地址后,需要尽快把这段映射的虚拟地址释放掉,这样才能反复被其他模块继续申请使用。
early_ioremap_init 会调用 early_ioremap_setup:
可见它的实现是依赖 fixmap 的,所以它必须要在 early_fixmap_init 之后才能运行。
注意:如果想要在伙伴系统初始化之前进行设备寄存器的访问,那么可以考虑 early IO remap 机制。
至此我们已经知道 dtb 和 early ioremap 都是在 fixmap 区的,如下图:
系统内存的布局:
完成 dtb 的 map 之后,内核可以访问这一段的内存了,通过解析 dtb 中的内容,内核可以勾勒出整个内存布局的情况,为后续内存管理初始化奠定基础。这一步主要在 setup_machine_fdt 中完成。这里就不看代码了,其调用流程是:setup_machine_fdt->early_init_dt_scan->early_init_dt_scan_nodes
就像注释中所示内核根据 dtb 的不同 node 勾勒出 choosen node,root node,memory node 相应内存区域。
除了这 3 个 node,还有一个 reserved-memorynode,它是在上面讲到 dtbmap 的时候 fixmap_remap_fdt 函数做的。下面我们看下这 4 个 node 的具体实现。
choosen node
该节点有一个 bootargs 属性,该属性定义了内核的启动参数,比如 mem=xx,此外,还处理 initrd 相关的 property,并保存在 initrd_start 和 initrd_end 这两个全局变量中。
root node
与内存无关,暂时不详述,以后有机会讲到 device tree 系列再详述。
memory node
通过 memblock_add 加入到 memblock.memory 对应的 memblock_type 链表中进行管理。
接下来到 arm64_memblock_init 函数:
void __init arm64_memblock_init(void)
{
......
memblock_reserve(__pa_symbol(_text), _end - _text); 1.kernel image 保留区
#ifdef CONFIG_BLK_DEV_INITRD
if (initrd_start) {memblock_reserve(initrd_start, initrd_end - initrd_start); 2.initrd 保留区
/* the generic initrd code expects virtual addresses */
initrd_start = __phys_to_virt(initrd_start);
initrd_end = __phys_to_virt(initrd_end);
}
#endif
early_init_fdt_scan_reserved_mem(); 3.dts 中配置为保留的区域
......
}
复制代码
reserve 内核代码、数据区等(_text 到_end 那一段,具体的内容可以参考内核链接脚本)
保留 initital ramdisk image 区域(从 initrd_start 到 initrd_end 区域)
reserved-memory node 如下所示:
完成:
通过上面的一系列操作,需要动态管理的内存已经被放到了 memorytype 和 reservedtype 这两个 region 中了,现在内存已经被 memblock 模块所管理了,这只是启动后的第一步,后续内存才会加入到伙伴系统去管理。
转载 https://mp.weixin.qq.com/s/Bq…