本文基于linux kernel 5.8,平台是arm64
上文介绍了armv8的地址转换过程,介绍了MMU,页表,内存属性的一些概念。
现在正是开始内核内存管理的探索!
第一步就是要“看见”物理内存。
1. 内核是如何知道物理内存的大小、起始地址等信息的呢?
arm64 如果使用DTB方式启动,物理内存的信息会在该文件中描述, acpi启动的arm64同样会在bios中配置好内存信息。
memory {
device_type = "memory";
reg = <0x0 0x10000000 0x0 0x70000000 0x0 0x80000000 0x0 0x80000000>;
};
DTB中memory节点描述了内存的起始地址及大小.
2. 内核又是如何去解析描述物理内存的配置文件呢?
解析配置文件,那么首先要将内核的代码顺利的执行起来。在【arm64内核内存布局】有描述过,kernel的代码存放在vmalloc区域,如果内核代码需要愉快的执行起来,那么就需要打开MMU。所以在arm64体系架构上,在进入start_kernel之前的汇编代码初始化阶段会进行两次的页表映射:
Identity mapping 和 kernel image mapping.
identity mapping | VA和PA相等的一段映射,主要目的就是为了打开MMU。在打开mmu之前,cpu访问的都是物理地址,打开mmu访问的就是虚拟地址,其实真正打开mmu的操作就是往某个system register的某个bit写1, 如果在开启mmu之前已经下发了某一个数据的操作指令,本来它是想访问物理地址的,结果mmu打开导致访问了虚拟地址,这样会造成混乱。 所以为了解决这一个情况,引入了identity mapping.。VA = PA, 打开mmu前后,无论访问物理地址还是虚拟地址,都是对应同一段物理内存 |
kernel image mapping | 顾名思义,就是内核镜像映射, 主要目的是为了执行内核代码。 打开了MMU后,内核需要运行起来,就需要将kernel运行需要的地址(kernel txt、rodata、data、bss等等)进行映射 |
idmap_pg_dir是identity mapping用到的页表,init_pg_dir是kernel_image_mapping用到的页表。
这两个页表定义在arch/arm64/kernel/vmlinux.lds.S中,同样定义在该文件中的还有另外三个页表reserved_ttbr0, tramp_pg_dir, swapper_pg_dir。
reserved_ttbr0是内核访问用户空间需要用的页表。
tramp_pg_dir适用于映射kaslr的内核区域
swapper_pg_dir 在内核启动期间进行常规映射后,用作内核页表。(在4.20的内核之前其实是没有init_pg_dir这个概念的,arm64/mm: Separate boot-time page tables from swapper_pg_dir添加了启动时pgd的init_pg_dir)
根据section的定义,它们的布局如下图:
. = ALIGN(PAGE_SIZE);
idmap_pg_dir = .;
. += IDMAP_DIR_SIZE;
idmap_pg_end = .;
...
. = ALIGN(PAGE_SIZE);
init_pg_dir = .;
. += INIT_DIR_SIZE;
init_pg_end = .;
// 大小
#define INIT_DIR_SIZE (PAGE_SIZE * EARLY_PAGES(KIMAGE_VADDR + TEXT_OFFSET, _end))
#define IDMAP_DIR_SIZE (IDMAP_PGTABLE_LEVELS * PAGE_SIZE)
EARLY_PAGES的大小和SWAPPER_PGTABLE_LEVELS 配置有关系。SWAPPER_PGTABLE_LEVELS 比PGTABLE_LEVELS小一级。
当SWAPPER_PGTABLE_LEVELS=3时,需要填充pgd->pmd->pte;
当SWAPPER_PGTABLE_LEVELS=2时,需要填充pgd->pte;
现在再结合代码进行说明,在head.S中,创建启动页表的函数__create_page_tables.
代码比较长,截取和页表填充相关的部分
__create_page_tables:
#if (VA_BITS < 48)
#define EXTRA_SHIFT (PGDIR_SHIFT + PAGE_SHIFT - 3)
#define EXTRA_PTRS (1 << (PHYS_MASK_SHIFT - EXTRA_SHIFT))
#if VA_BITS != EXTRA_SHIFT
#error "Mismatch between VA_BITS and page size/number of translation levels"
#endif
mov x4, EXTRA_PTRS
create_table_entry x0, x3, EXTRA_SHIFT, x4, x5, x6 ----------(1)
#else
/*
* If VA_BITS == 48, we don't have to configure an additional
* translation level, but the top-level table has more entries.
*/
mov x4, #1 << (PHYS_MASK_SHIFT - PGDIR_SHIFT)
str_l x4, idmap_ptrs_per_pgd, x5
#endif
1:
ldr_l x4, idmap_ptrs_per_pgd
mov x5, x3 // __pa(__idmap_text_start)
adr_l x6, __idmap_text_end // __pa(__idmap_text_end)
map_memory x0, x1, x3, x6, x7, x3, x4, x10, x11, x12, x13, x14 ----------(2)
/*
* Map the kernel image (starting with PHYS_OFFSET).
*/
adrp x0, init_pg_dir
mov_q x5, KIMAGE_VADDR + TEXT_OFFSET // compile time __va(_text)
add x5, x5, x23 // add KASLR displacement
mov x4, PTRS_PER_PGD
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)
map_memory x0, x1, x5, x6, x7, x3, x4, x10, x11, x12, x13, x14 ------(3)
/*
* Since the page tables have been populated with non-cacheable
* accesses (MMU disabled), invalidate those tables again to
* remove any speculatively loaded cache lines.
*/
dmb sy
adrp x0, idmap_pg_dir
adrp x1, idmap_pg_end
sub x1, x1, x0
bl __inval_dcache_area
adrp x0, init_pg_dir
adrp x1, init_pg_end
sub x1, x1, x0
bl __inval_dcache_area ---------(4)
ret x28
SYM_FUNC_END(__create_page_tables)
(1) 在va_bits < 48时需要调用create_table_entry函数, 主要是解决要标识映射的物理地址超出VA_BITS覆盖范围的问题
(2) 创建一个映射,将从_idmap_text_start到_idmap_text_end区域的虚拟地址和物理地址匹配
(3) 将物理地址的内核镜像映射到_text到_end的范围(init_pg_dir的虚拟地址)
map_memory:
.macro map_memory, tbl, rtbl, vstart, vend, flags, phys, pgds, istart, iend, tmp, count, sv
add \rtbl, \tbl, #PAGE_SIZE
mov \sv, \rtbl
mov \count, #0
compute_indices \vstart, \vend, #PGDIR_SHIFT, \pgds, \istart, \iend, \count
populate_entries \tbl, \rtbl, \istart, \iend, #PMD_TYPE_TABLE, #PAGE_SIZE, \tmp
mov \tbl, \sv
mov \sv, \rtbl
#if SWAPPER_PGTABLE_LEVELS > 3
compute_indices \vstart, \vend, #PUD_SHIFT, #PTRS_PER_PUD, \istart, \iend, \count
populate_entries \tbl, \rtbl, \istart, \iend, #PMD_TYPE_TABLE, #PAGE_SIZE, \tmp
mov \tbl, \sv
mov \sv, \rtbl
#endif
#if SWAPPER_PGTABLE_LEVELS > 2
compute_indices \vstart, \vend, #SWAPPER_TABLE_SHIFT, #PTRS_PER_PMD, \istart, \iend, \count
populate_entries \tbl, \rtbl, \istart, \iend, #PMD_TYPE_TABLE, #PAGE_SIZE, \tmp
mov \tbl, \sv
#endif
compute_indices \vstart, \vend, #SWAPPER_BLOCK_SHIFT, #PTRS_PER_PTE, \istart, \iend, \count
bic \count, \phys, #SWAPPER_BLOCK_SIZE - 1
populate_entries \tbl, \count, \istart, \iend, \flags, #SWAPPER_BLOCK_SIZE, \tmp
.endm
(4) 在等待上面所有内存读/写操作完成后,将idmap_pg_dir到init_pg_dir范围的缓存无效。
函数的整体思想总结如下图:
建立好这两段映射后,kernel就会正式进入虚拟地址空间的世界,但是从它的视角,现在只能看到identity mapping和kernel image mapping映射好的两段物理内存。不过好在有了这两段映射,内核就可以执行起来。现在我们顺着内核执行的路径,开始找寻其他物理内存的踪迹。
现在从start_kernel开始分析,找寻和内存相关的一些调用。
start_kernel()
setup_arch()
| /* 初始化固定映射区*/
|--> early_fixmap_init()
| /* 解析DTB的内存配置*/
|--> setup_machine_fdt()
|
|-->early_init_dt_scan_memory()
/* 初始化memblock*/
|--> arm64_memblock_init()
| /*页表初始化*/
|--> paging_init()
|
|--> bootmem_init()
early_fixmap_init
现在虽然MMU已经打开,kernel image的页表已经建立,但是内核还没有为DTB这段内存创建映射,现在内核还不知道内存的布局,所以内存管理模块还没能初始化。这个时候就需要用到fixmap。
early_fixmap_init初始化固定映射区,“fix”指的是一段固定的虚拟地址空间,利用这段固定的虚拟地址空间,可以对任意的物理地址空间建立映射关系。
在arch/arm64/include/asm/fixmap.h中定义了fixmap的数据结构
enum fixed_addresses {
FIX_HOLE,
#define FIX_FDT_SIZE (MAX_FDT_SIZE + SZ_2M)
FIX_FDT_END,
FIX_FDT = FIX_FDT_END + FIX_FDT_SIZE / PAGE_SIZE - 1,
FIX_EARLYCON_MEM_BASE,
FIX_TEXT_POKE0,
#ifdef CONFIG_ACPI_APEI_GHES
/* Used for GHES mapping from assorted contexts */
FIX_APEI_GHES_IRQ,
FIX_APEI_GHES_SEA,
#ifdef CONFIG_ARM_SDE_INTERFACE
FIX_APEI_GHES_SDEI_NORMAL,
FIX_APEI_GHES_SDEI_CRITICAL,
#endif
#endif /* CONFIG_ACPI_APEI_GHES */
#ifdef CONFIG_UNMAP_KERNEL_AT_EL0
FIX_ENTRY_TRAMP_DATA,
FIX_ENTRY_TRAMP_TEXT,
#define TRAMP_VALIAS (__fix_to_virt(FIX_ENTRY_TRAMP_TEXT))
#endif /* CONFIG_UNMAP_KERNEL_AT_EL0 */
__end_of_permanent_fixed_addresses,
/*
* Temporary boot-time mappings, used by early_ioremap(),
* before ioremap() is functional.
*/
#define NR_FIX_BTMAPS (SZ_256K / PAGE_SIZE)
#define FIX_BTMAPS_SLOTS 7
#define TOTAL_FIX_BTMAPS (NR_FIX_BTMAPS * FIX_BTMAPS_SLOTS)
FIX_BTMAP_END = __end_of_permanent_fixed_addresses,
FIX_BTMAP_BEGIN = FIX_BTMAP_END + TOTAL_FIX_BTMAPS - 1,
/*
* Used for kernel page table creation, so unmapped memory may be used
* for tables.
*/
FIX_PTE,
FIX_PMD,
FIX_PUD,
FIX_PGD,
__end_of_fixed_addresses
};
从fixmap的组成来看,fixmap分为permanent fixmap和temporary fixmap。permanent表示持久化的映射,比如FDT区域,一旦完成地址映射,映射关系一直存在。而temporary表示临时映射,当某个模块使用了这部分的虚拟地址后,需要尽可能快的释放,以便于其他模块使用。
现在我们知道,如果要访问DTB所在的物理地址,那么需要将该物理地址映射到Fixed map中的区域,然后访问该区域中的虚拟地址即可。
结合代码进行分析:
void __init early_fixmap_init(void)
{
pgd_t *pgdp;
p4d_t *p4dp, p4d;
pud_t *pudp;
pmd_t *pmdp;
unsigned long addr = FIXADDR_START; --------(1)
pgdp = pgd_offset_k(addr); --------- (2)
p4dp = p4d_offset(pgdp, addr); -------- (3)
p4d = READ_ONCE(*p4dp);
if (CONFIG_PGTABLE_LEVELS > 3 &&
!(p4d_none(p4d) || p4d_page_paddr(p4d) == __pa_symbol(bm_pud))) {
/*
* We only end up here if the kernel mapping and the fixmap
* share the top level pgd entry, which should only happen on
* 16k/4 levels configurations.
*/
BUG_ON(!IS_ENABLED(CONFIG_ARM64_16K_PAGES));
pudp = pud_offset_kimg(p4dp, addr);
} else {
if (p4d_none(p4d))
__p4d_populate(p4dp, __pa_symbol(bm_pud), PUD_TYPE_TABLE); ------ (4)
pudp = fixmap_pud(addr);
}
if (pud_none(READ_ONCE(*pudp)))
__pud_populate(pudp, __pa_symbol(bm_pmd), PMD_TYPE_TABLE); ----------(5)
pmdp = fixmap_pmd(addr);
__pmd_populate(pmdp, __pa_symbol(bm_pte), PMD_TYPE_TABLE); ----------- (6)
...
}
}
(1) 定义fixmap的起始地址 FIXADDR_START
4KB pages + 4 levels (48-bit)的配置下,FIXADDR_START的虚拟地址是fffffdfffe5f9000,fixmap区域大小为4124KB。
(2) 获取addr地址对应pgd全局页表中的entry, 该pgd全局页表正是init_pg_dir全局页表
以init进程中的pgd页表基地址为pgd的基地址,init进程中的pgd即为 init_pg_dir
#define INIT_MM_CONTEXT(name) \
.pgd = init_pg_dir,
知道了pgd基地址,那么需要知道addr在pgd页表中的哪一项,pgd的索引位为: addr[47:39],占据9位,计算方式为:(addr >> 39) & (2^9 - 1)
(3) 五级页表会在PGD和PUD之间增加一个level叫P4D, 不过在arm64上, p4d = pgd, 所以这里可以忽略p4d, 直接将p4d当作pgd
(4) 将bm_pud的物理地址写到pgd全局页目录表中;
bm_pud/bm_pmd/bm_pte是三个全局数组,相当于是中间的页表,存放各级页表的entry
static pte_t bm_pte[PTRS_PER_PTE] __page_aligned_bss;
static pmd_t bm_pmd[PTRS_PER_PMD] __page_aligned_bss __maybe_unused;
static pud_t bm_pud[PTRS_PER_PUD] __page_aligned_bss __maybe_unused;
(5)将bm_pmd的物理地址写到pud页目录表中;
(6)将bm_pte的物理地址写到pmd页表目录表中;bm_pte具体的内容没有设置,等到使用的时候再做填充。
至此,init_pg_dir和bm_pud/bm_pmd/bm_pte已经建立好联系。
setup_machine_fdt
初始化完成fixmap后,现在可以去访问DTB文件,并解析得到物理地址信息。
/* 读取DTB文件 */
setup_machine_fdt()
| /* 建立PTE entry映射,访问物理地址*/
|---> fixmap_remap_fdt()
|
|---> early_init_dt_scan()
| /*扫描DTB文件中的chose, root, memory节点*/
|---> early_init_dt_scan_nodes()
| /* 获取内存大小*/
|---> early_init_dt_scan_memory()
| /* 添加至memblock*/
|---> early_init_dt_add_memory_arch()
整个setup_machine_fdt的过程就是扫描DTB的关键节点,获取内存的大小信息,然后添加至内核早期的内存管理memblock中。
arm64_memblock_init
struct memblock是一个全局的变量,用于管理内核早期启动阶段过程中的所有物理内存
struct memblock {
bool bottom_up;
phys_addr_t current_limit;
struct memblock_type memory;
struct memblock_type reserved;
};
bool bottom_up: 表示分配器分配内存的方式(true:从低地址(内核映像的尾部)向高地址分配; false:也就是top-down,从高地址向地址分配内存)
current_limit: 内存块的大小限制
struct memblock_type memory:用来存放系统的可用内存;
struct memblock_type reserved: 用来存放系统的保留内存;
struct memblock_region: 描述一段物理内存的信息,是memblock管理的最小单位;每一种类型的memblock有INIT_MEMBLOCK_REGIONS个regions
arm64_memblock_init的主要作用就是将上一步添加至memblock的内存进行规整, 它会将一些特殊的区域添加进reserved内存中。
void __init arm64_memblock_init(void)
{
...
memstart_addr = round_down(memblock_start_of_DRAM(),
ARM64_MEMSTART_ALIGN);
memblock_reserve(__pa_symbol(_text), _end - _text); ---------- (1)
if (IS_ENABLED(CONFIG_BLK_DEV_INITRD) && phys_initrd_size) {
/* the generic initrd code expects virtual addresses */
initrd_start = __phys_to_virt(phys_initrd_start);
initrd_end = initrd_start + phys_initrd_size; ------ (2)
}
early_init_fdt_scan_reserved_mem(); -------- (3)
if (IS_ENABLED(CONFIG_ZONE_DMA)) {
zone_dma_bits = ARM64_ZONE_DMA_BITS;
arm64_dma_phys_limit = max_zone_phys(ARM64_ZONE_DMA_BITS);
}
if (IS_ENABLED(CONFIG_ZONE_DMA32))
arm64_dma32_phys_limit = max_zone_phys(32);
else
arm64_dma32_phys_limit = PHYS_MASK + 1;
reserve_crashkernel(); -------- (4)
reserve_elfcorehdr(); -------- (5)
high_memory = __va(memblock_end_of_DRAM() - 1) + 1;
dma_contiguous_reserve(arm64_dma32_phys_limit); ------- (6)
}
(1) reserve内核代码、text,bss区等
(2) reserve initrd(ramdisk 区域)
(3) reserve dts中配置为保留的区域
(4) reserve crash kernel的保留区域
(5) reserve elf core header的保留区域
(6) reserve 16M的CMA区域
除了memblock保留的内存区域, 剩下的部分就是可以实际去使用的内存了。
现在通过DTB文件我们可以窥探到内存的全局布局,并且通过memblock对物理内存进行管理,后续就需要进行内存的页表映射,完成实际的物理地址到虚拟地址的映射了。
参考资料
内存初始化(上) — wowotech
ARM64 Kernel Image Mapping的变化 — wowotech
ARM64的启动过程之(四):打开MMU — wowotech
Linux物理内存初始化 — loyenwang
kernel/head.S - ARM64