Linux内存管理(三):“看见”物理内存

本文基于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 mappingkernel image mapping.

identity mappingVA和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

  • 11
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值