0. 前言

从前面几篇文章中,

[linux内存管理] 第006篇 start_kernel全局简述

[linux内存管理] 第007篇 fixmap映射详解

[linux内存管理] 第008篇 memblock子系统详解

我们可以知道在本篇介绍的paging_init函数之前,存放kernel Image和DTB的两段物理区域已经可以访问了(关键就是这两者的页表创建好了)。尽管物理内存已经通过memblock_add添加进系统了,但是这部分的物理内存到虚拟内存的映射还没有建立,可以通过memblock_alloc分配一段物理内存,但是还不能访问,一切还需要等待paging_init的执行。等待最终页表建立好后,就可以通过虚拟地址去访问最终的物理地址了。

再开始本章的paging_init前,我们再看一下当前的内存状态:

{% tip success %}

目前所有的内存分为了如下2部分:

  • OS已经收集到的内存分布信息(来自DTB解析),这部分保存在memblock中,这部分又分为3个小部分
    • 系统内存占据的空间,保存在memblock.memory中
    • 已经使用或者保留使用的,保存在memblock.reserve中
    • dtb中reserved-memory,但是有No-map属性的,这种内存不属于OS管辖
  • OS还未收集到内存的内存部分,暂未管辖(这部分稍后会被加载到伙伴系统)

切记:到目前为止,OS是无法正常使用内存的,因为memblock定义的都是物理地址,而目前只有kernel和fdt是经过了映射的,其余段都是黑暗状态

{% endtip %}

附上paging_init前的虚拟地址映射函数流程分析图:

fixmap+memblock

1. paging_init总览

void __init paging_init(void)
{
	pgd_t *pgdp = pgd_set_fixmap(__pa_symbol(swapper_pg_dir));  ///通过固定映射映射,访问swapper_pg_dir

	map_kernel(pgdp);   ///建立内核的细粒度映射(分别建立内核每个段的动态映射)
	///	映射memblock子系统添加的内存区域
	map_mem(pgdp);      ///建立物理内存的线性映射(可以访问整个物理内存区域,memblock有效区域)

	///解除fixed区域pgd虚拟地址映射
	pgd_clear_fixmap();

	///将pgd页表的内容切换到swapper_pgd_dir页表
	cpu_replace_ttbr1(lm_alias(swapper_pg_dir));
	init_mm.pgd = swapper_pg_dir;  ///切换内核主进程的pgd地址

	///释放init_pg_dir页表的物理内存
	memblock_free(__pa_symbol(init_pg_dir),
		      __pa_symbol(init_pg_end) - __pa_symbol(init_pg_dir));

	memblock_allow_resize();
}
  • pgd_set_fixmap将数组变量 swapper_pg_dir 用来存放 pgd 页表。通过pgd_set_fixmap()得到对应的虚拟地址 FIX_PGD 处,详细看第 2 节。这里的 pgdp 将会在 paging_init() 整个过程中用来临时映射使用
  • map_kernel() 将内核映像映射到内核空间的虚拟地址(位于vmalloc区域)
  • map_mem()memblock.memory 中所有regions 对应的物理内存进行 线性映射,详细看第 4
  • pgd_clear_fixmappgd_set_fixmap对应,用于map后清理FIX_PGD
  • cpu_replace_ttrbr1切换页表,并将新建的页表内容替换swapper_pg_dir页表内容
  • memblock_free释放init_pg_dir页表的物理内存,从memblock.reserved移除,后期可以加入到buddy系统

注意:

  • map_kernel() 和map_mem()都建立物理内存到内核空间虚拟地址的线性映射,但是映射的地址不一样;
  • map_kernel() 和map_mem() 的参数是 pgdp,也就是 swapper_pg_dir 映射到 fixmap 后的虚拟地址,整个函数映射调用过程中,都是通过 pgdp 这个指针变量;

2. pgd_set_fixmap

首先我们回顾一下FIXMAP区域分布情况:

详细的fixmap 分布,可以查看枚举类型 fixed_addresses,这里将该枚举通过图的方式显示。主要分:

  • 固定映射;
  • 动态映射;
  • 映射查找;

其中,动态映射和映射查找又可以合并称为临时启动时映射。 详细可以查看[linux内存管理] 第007篇 fixmap映射详解2 节中枚举类型。

swapper_pg_dir 全局变量,用以指定 pgd 的页表,这里 swapper_pg_dir 是 pgd 页表的首地址

#define pgd_set_fixmap(addr)	((pgd_t *)set_fixmap_offset(FIX_PGD, addr))

实际上调用的是 set_fixmap_offset(),返回 pgd_t 结构体指针,里面存放转换成虚拟地址的 pgd 值。

另外,注意 set_fixmap_offset() 的第一个参数,选择 fixed_addresses 中枚举值 FIX_PGD

/* Return a pointer with offset calculated */
#define __set_fixmap_offset(idx, phys, flags)				\
({									\
	unsigned long ________addr;					\
	__set_fixmap(idx, phys, flags);					\
	________addr = fix_to_virt(idx) + ((phys) & (PAGE_SIZE - 1));	\
	________addr;							\
})

#define set_fixmap_offset(idx, phys) \
	__set_fixmap_offset(idx, phys, FIXMAP_PAGE_NORMAL)

最终调用 __set_fixmap_offset(),该函数分两部分:

  • __set_fixmap()
  • fix_to_virt()

2.1 fix_to_virt

fix_to_virt函数使用fixed_addresses枚举作为索引来获取虚拟地址。这个函数的实现很简单

static __always_inline unsigned long fix_to_virt(const unsigned int idx)
{
	BUILD_BUG_ON(idx >= __end_of_fixed_addresses);
	return __fix_to_virt(idx);
}

#define __fix_to_virt(x)	(FIXADDR_TOP - ((x) << PAGE_SHIFT))

首先,它使用BUILD_BUG_ON宏检查为fixed_addresses枚举指定的索引是否不大于或等于__end_of_fixed_addresses ,然后返回__fix_to_virt宏的结果

我们将PAGE_SHIFT上的fix-mapped区域的给定索引左移,该索引确定页面的大小,如我上面所写,并从FIXADDR_TOP fix-mapped区域的最高地址)中减去它:

+-----------------+
|    PAGE 1       | FIXADDR_TOP (virt address)
|    PAGE 2       |
|    PAGE 3       |
|    PAGE 4 (idx) | x - 4
|    PAGE 5       |
+-----------------+

有一个反函数用于获取与给定虚拟地址相对应的固定映射区域的索引:

static inline unsigned long virt_to_fix(const unsigned long vaddr)
{
	BUG_ON(vaddr >= FIXADDR_TOP || vaddr < FIXADDR_START);
	return __virt_to_fix(vaddr);
}

#define __virt_to_fix(x)	((FIXADDR_TOP - ((x)&PAGE_MASK)) >> PAGE_SHIFT)

virt_to_fix获取一个虚拟地址,检查该地址是否在FIXADDR_STARTFIXADDR_TOP之间,并调用__virt_to_fix宏。

与前面的示例(在__fix_to_virt宏中)一样,我们从修复映射区域的顶部开始。我们还从上到下搜索与给定虚拟地址对应的固定映射区域的索引。正如您所看到的,首先我们将使用x & PAGE_MASK表达式清除给定虚拟地址中的前12位。这使我们能够获取页面的基地址。当给定的虚拟地址指向页面的开头/中间或结尾的某个位置但不是其基地址时,我们需要执行此操作。在下一步中,从FIXADDR_TOP中减去该值,这将为我们提供固定映射区域中相应页面的虚拟地址。最后我们只需将该地址的值除以PAGE_SHIFT即可。这为我们提供了与给定虚拟地址相对应的固定映射区域的索引。

那 fix_to_virt(idx) + ((phys) & (PAGE_SIZE - 1))该如何理解呢?

fix_to_virt() 用以将物理地址转换成 fixmap 中对应枚举值的虚拟地址,这里是在得到 fixmap 的虚拟地址之后,加上物理地址的中的低12位(PAGE_SHIFT=12时,PAGE_SIZE=4096,对应的十六进制为0x1000,减1即为0xFFF,phys&0xFFF也就是低12位),并将最终得到的虚拟地址返回。

2.2 __set_fixmap

void __set_fixmap(enum fixed_addresses idx,
			       phys_addr_t phys, pgprot_t flags)
{
	// 获取该枚举对应 fixmap 中的虚拟地址
	unsigned long addr = __fix_to_virt(idx);
	pte_t *ptep;

	//该枚举值不允许超出界限
	BUG_ON(idx <= FIX_HOLE || idx >= __end_of_fixed_addresses);

	///获取该虚拟地址的在bm_pte数组中的所在的index的地址
	ptep = fixmap_pte(addr);

	if (pgprot_val(flags)) {
		set_pte(ptep, pfn_pte(phys >> PAGE_SHIFT, flags));   ///填充pte页表
	} else {
		pte_clear(&init_mm, addr, ptep);
		flush_tlb_kernel_range(addr, addr+PAGE_SIZE);
	}
}

__set_fixmap() 用来更新 fixmap 对应的 pte,不仅是在设置的时候,也会在 clear_fixmap() 的时候调用到。

  • 当设置的时候,第二个参数为物理地址,flags 是映射的权限;
  • 当clear_fixmap() 时,第二个参数为0,flags 也是0(FIXMAP_PAGE_CLEAR)。

例如,这里是通过 pgd_set_fixmap() 调用而来,所以走的是设置的case,最终调用 set_pte() 将物理地址中带有 pgprot flag的值存放到 ptep 中。

而如果走的是 clear_fixmap(),会走进 else 的 case 中。

3. map_kernel()

static void __init map_kernel(pgd_t *pgdp)
{
	static struct vm_struct vmlinux_text, vmlinux_rodata, vmlinux_inittext,
				vmlinux_initdata, vmlinux_data;

	/*
	 * External debuggers may need to write directly to the text
	 * mapping to install SW breakpoints. Allow this (only) when
	 * explicitly requested with rodata=off.
	 */
	// PAGE_KERNEL_ROX:只读且可执行(当 rodata 开启)。
	// PAGE_KERNEL_EXEC:可写且可执行(当 rodata 关闭)。
	// 这是为了支持调试器设置断点时对代码段的写入需求。
	pgprot_t text_prot = rodata_enabled ? PAGE_KERNEL_ROX : PAGE_KERNEL_EXEC;

	/*
	 * If we have a CPU that supports BTI and a kernel built for
	 * BTI then mark the kernel executable text as guarded pages
	 * now so we don't have to rewrite the page tables later.
	 */
	// 如果当前 CPU 支持 BTI(控制流完整性保护),将内核代码段标记为受保护的页面(PTE_GP)。
	// BTI 的作用:防止利用漏洞跳转到非法的函数入口点。
	if (arm64_early_this_cpu_has_bti())
		text_prot = __pgprot_modify(text_prot, PTE_GP, PTE_GP);

	/*
	 * Only rodata will be remapped with different permissions later on,
	 * all other segments are allowed to use contiguous mappings.
	 */  
	//将内核的每个段,分别建立动态页表
	// _stext 到 _etext:内核可执行代码段。
	// __start_rodata 到 __inittext_begin:只读数据段。
	// __inittext_begin 到 __inittext_end:初始化代码段。
	// __initdata_begin 到 __initdata_end:初始化数据段。
	// _data 到 _end:内核数据段。
	map_kernel_segment(pgdp, _stext, _etext, text_prot, &vmlinux_text, 0,
			   VM_NO_GUARD);
	map_kernel_segment(pgdp, __start_rodata, __inittext_begin, PAGE_KERNEL,
			   &vmlinux_rodata, NO_CONT_MAPPINGS, VM_NO_GUARD);
	map_kernel_segment(pgdp, __inittext_begin, __inittext_end, text_prot,
			   &vmlinux_inittext, 0, VM_NO_GUARD);
	map_kernel_segment(pgdp, __initdata_begin, __initdata_end, PAGE_KERNEL,
			   &vmlinux_initdata, 0, VM_NO_GUARD);
	map_kernel_segment(pgdp, _data, _end, PAGE_KERNEL, &vmlinux_data, 0, 0);

	// FIXADDR_START 是 FIXMAP 区域的起始地址。
	// pgd_offset_pgd(pgdp, FIXADDR_START):
	// 计算 FIXMAP 对应的 PGD 位置(页表顶层目录)。
	// 返回 FIXMAP 地址对应的 PGD 表项指针。
	if (!READ_ONCE(pgd_val(*pgd_offset_pgd(pgdp, FIXADDR_START)))) {
		/*
		 * The fixmap falls in a separate pgd to the kernel, and doesn't
		 * live in the carveout for the swapper_pg_dir. We can simply
		 * re-use the existing dir for the fixmap.
		 */
		// 在初始化阶段(setup_arch函数),FIXMAP的页表项最初被设置在 init_pg_dir 中。
		//此时需要将 init_pg_dir 中的 FIXMAP 页表项同步到 swapper_pg_dir
		set_pgd(pgd_offset_pgd(pgdp, FIXADDR_START),
			READ_ONCE(*pgd_offset_k(FIXADDR_START)));
	} else if (CONFIG_PGTABLE_LEVELS > 3) {
		// 特殊处理:4 级页表的情况下重新配置 FIXMAP 映射。
		pgd_t *bm_pgdp;
		p4d_t *bm_p4dp;
		pud_t *bm_pudp;
		/*
		 * The fixmap shares its top level pgd entry with the kernel
		 * mapping. This can really only occur when we are running
		 * with 16k/4 levels, so we can simply reuse the pud level
		 * entry instead.
		 */
		BUG_ON(!IS_ENABLED(CONFIG_ARM64_16K_PAGES));
		bm_pgdp = pgd_offset_pgd(pgdp, FIXADDR_START);
		bm_p4dp = p4d_offset(bm_pgdp, FIXADDR_START);
		bm_pudp = pud_set_fixmap_offset(bm_p4dp, FIXADDR_START);
		pud_populate(&init_mm, bm_pudp, lm_alias(bm_pmd));
		pud_clear_fixmap();
	} else {
		BUG();
	}
	// 内核内存访问错误检测工具。
	// 通过 shadow memory 映射,记录内核地址是否有效。
	kasan_copy_shadow(pgdp);
}

首先,根据 rodata 这个参数确定 text 段映射的权限,当不配置该参数时,变量 rodata_enabled 为 true,即以只读的方式。当然需要debug 的时候,可以使用 rodata=off 的配置实现。
接着,函数对内核映像的各个段分别进行映射,映射到内核空间的虚拟地址为 vmalloc 区域。
map_kenel_segment() 主要调用 __create_pgd_mapping() 创建具体的映射,以及通过 vm_area_add_early() 将对应的区域添加到全局 vmlist 中,会在 vmalloc_init() 中使用,详细可以查看第 3.1 节;

  • 映射 文本段,从 _text 到 _etext, 映射权限由 rodata_enabled 决定;
  • 映射 rodata 段,从 __start_rodata 到 __inittext_begin, normal 的映射权限,但附带 NO_CONT_MAPPINGS 的 flag;
  • 映射 inittext 段,从 __inittext_begin 到 __inittext_end, 映射权限同文本段;
  • 映射 initdata 段,从 __initdata_begin 到 __initdata_end, normal的映射权限;
  • 映射 data 段,_data到_end , normal的映射权限;
    最后,确认 FIXADDR_START 的地址是否在 pgd 页表中映射。首先通过 pgd_offset_raw() 函数确定 FIXADDR_START 对应的 pgd 的地址,再通过 pgd_val() 读取页表项的值。如果该页表项的值为空,则说明没有映射过,通过 set_pgd() 将 swapper_pg_dir 中现成的 gpd 页表项值存入到映射到 fixmap 中 pgd 临时页表。
    我们知道,在 [linux内存管理] 第007篇 fixmap详解一文中early_fixmap_init()就将FIXADDR_START映射到了 swapper_pg_dir 中。

3.1 map_kernel_segment()

///建立内核段的动态映射
static void __init map_kernel_segment(pgd_t *pgdp, void *va_start, void *va_end,
				      pgprot_t prot, struct vm_struct *vma,
				      int flags, unsigned long vm_flags)
{
	phys_addr_t pa_start = __pa_symbol(va_start);   ///获取物理地址
	unsigned long size = va_end - va_start;

	BUG_ON(!PAGE_ALIGNED(pa_start));
	BUG_ON(!PAGE_ALIGNED(size));

	__create_pgd_mapping(pgdp, pa_start, (unsigned long)va_start, size, prot,
			     early_pgtable_alloc, flags);   ///建立内存段映射,用early_pgtable_alloc动态分配页表

	if (!(vm_flags & VM_NO_GUARD))   ///添加一个页的guard
		size += PAGE_SIZE;

	vma->addr	= va_start;
	vma->phys_addr	= pa_start;
	vma->size	= size;
	vma->flags	= VM_MAP | vm_flags;
	vma->caller	= __builtin_return_address(0);

	vm_area_add_early(vma);   ///将VMA添加到内核的vma链表
}

参数分别是:

  • pgdp:指定pgd 页表的首地址,这里是第 2 节中经过 fixmap 映射的虚拟地址;
  • va_start:image segment 起始虚拟地址,通过 __pa_symbol() 解析得到物理地址;
  • va_end:image segment 终止地址,一般通过 va_end - va_start 得到 segment 大小;
  • prot:映射时权限;
  • vma:虚拟地址结构体;
  • flags:mapping 时附带的flags,rodata 段的flag 比较特殊,需要使用 NO_CONT_MAPPINGS ,其他都为0;
  • vm_flags:vma 附带的 flag;
    函数先通过 __pa_symbol() 获取起始物理地址,接着通过 __create_pgd_mapping() 进行映射,映射好之后将信息都存入参数 vm 中,并调用 vm_area_add_early() 将 vm 前插到 vmlist 链表中。

注意:

__pa_symbol() 是kernel image 转物理地址的专用函数,物理地址与虚拟地址之间的关系为:
virt address = phys address + kimage_voffset
创建具体的映射使用 __create_pgd_mapping(),详细查看下面第 5 节。

4. map_mem()

static void __init map_mem(pgd_t *pgdp)
{
	static const u64 direct_map_end = _PAGE_END(VA_BITS_MIN);   ///计算需要线性映射的虚拟地址和物理地址
	phys_addr_t kernel_start = __pa_symbol(_stext);
	phys_addr_t kernel_end = __pa_symbol(__init_begin);
	phys_addr_t start, end;
	int flags = NO_EXEC_MAPPINGS;
	u64 i;

	/*
	 * Setting hierarchical PXNTable attributes on table entries covering
	 * the linear region is only possible if it is guaranteed that no table
	 * entries at any level are being shared between the linear region and
	 * the vmalloc region. Check whether this is true for the PGD level, in
	 * which case it is guaranteed to be true for all other levels as well.
	 */
	BUILD_BUG_ON(pgd_index(direct_map_end - 1) == pgd_index(direct_map_end));

	if (can_set_direct_map() || crash_mem_map || IS_ENABLED(CONFIG_KFENCE))
		flags |= NO_BLOCK_MAPPINGS | NO_CONT_MAPPINGS;

	/*
	 * Take care not to create a writable alias for the
	 * read-only text and rodata sections of the kernel image.
	 * So temporarily mark them as NOMAP to skip mappings in
	 * the following for-loop
	 */
	///临时将内核的text段,rodata段打上nomap标志,防止进入下面的循环,后续会解除
	memblock_mark_nomap(kernel_start, kernel_end - kernel_start); ///设备树可以定义nomap区,nomap段将不会被映射

	/* map all the memory banks */
	///映射memblock的所有memory区域,跳过内核text,rodata段
	for_each_mem_range(i, &start, &end) {
		if (start >= end)
			break;
		/*
		 * The linear map must allow allocation tags reading/writing
		 * if MTE is present. Otherwise, it has the same attributes as
		 * PAGE_KERNEL.
		 */
		__map_memblock(pgdp, start, end, pgprot_tagged(PAGE_KERNEL),
			       flags);
	}

	/*
	 * Map the linear alias of the [_stext, __init_begin) interval
	 * as non-executable now, and remove the write permission in
	 * mark_linear_text_alias_ro() below (which will be called after
	 * alternative patching has completed). This makes the contents
	 * of the region accessible to subsystems such as hibernate,
	 * but protects it from inadvertent modification or execution.
	 * Note that contiguous mappings cannot be remapped in this way,
	 * so we should avoid them here.
	 */
	///将text和rodata以只读映射,同时解除nomap标志
	__map_memblock(pgdp, kernel_start, kernel_end,
		       PAGE_KERNEL, NO_CONT_MAPPINGS);
	memblock_clear_nomap(kernel_start, kernel_end - kernel_start);
}
  • 首先,通过 memblock_mark_nomap() 函数将 _text 到 __init_begin 的区域 flag 临时地 置上 MEMBLOCK_NOMAP
  • 接着,轮询 memblock.memory 中所有的 regions,只要该 region 没有设置 MEMBLOCK_NOMAP 的 flag,就会调用 __map_memblock() 进行映射,最终会调用 __create_pgd_mapping()函数实现;
  • 接着,对 kenel image 中_text 到 __init_begin 的区域进行单独的映射;
  • 最后,解除最开始的对 _text 到 __init_begin 的区域临时的 NOMAP 属性;

{% tip success %}
注意:
map_mem() 都是将物理地址映射到 线性区域 中,这里我们发现 Kernel Image 中的 text、rodata 段又被映射到线性区域 (之前,map_kernel 映射到了vmalloc),原因是其他的子系统(例如 hibernate),会映射到线性区域中,可能需要线性区域的地址来引用内核的 text、rodata ,映射的时候也会限制成 只读/不可执行 ,防止意外修改或执行。
{% endtip %}

4.1 __map_memblock()

static void __init __map_memblock(pgd_t *pgdp, phys_addr_t start,
				  phys_addr_t end, pgprot_t prot, int flags)
{
	__create_pgd_mapping(pgdp, start, __phys_to_virt(start), end - start,
			     prot, early_pgtable_alloc, flags);
}

这里需要注意的是第 3 个参数,memblock.memory 中的物理地址都会进行 线性映射线性区域
[linux内存管理] 第008章 memblock子系统详解 一文中计算出了物理地址与虚拟地址的偏移:
virt address = phys address - memstart_addr + PAGE_OFFSET
另外,这里映射是 pgtable 的分配使用的是 early_pgtable_alloc() 函数,详细可以查看第 5 节和第 9 节。

5. __create_pgd_mapping()

对于 paging_init() 期间的 __create_pgd_mapping() 的调用,在上面第 3 节和第 4 节,已经提示过。在剖析该函数之前,需要注意该函数的第 6 个参数(倒数第 2 个),pgtable alloc 的回调函数,在 paging_init() 中使用 early_pgtable_alloc() ,而更早之前的 FDT 映射,则传入 NULL
为什么需要 pgtable alloc 的回调函数?

图中 pgdp 会指向PGD 页表的页表项,而里面的内容就是指向 PMD 页表的基地址。
VA_BITS 为39,页大小为 4k 的三级页表系统来说,每个页表共有 9 bits的页表项,即 512 个页表项,每个页表项存放下一级页表的基地址(8字节),那么,每个页表用占用 4K (512*8) 的空间。
这些页表都需要申请内存空间,所以需要这样的申请接口。对于 paging_init() 的前期映射来说,使用的是 early_pgtable_alloc() 函数,详细可以查看本文第 9 节。
下面正式进入 __create_pgd_mapping() 函数剖析:

///依次动态建立各级页表
static void __create_pgd_mapping(pgd_t *pgdir, phys_addr_t phys,
				 unsigned long virt, phys_addr_t size,
				 pgprot_t prot,
				 phys_addr_t (*pgtable_alloc)(int),
				 int flags)
{
	unsigned long addr, end, next;
	// 确定虚拟地址对应到pgd页表项
	pgd_t *pgdp = pgd_offset_pgd(pgdir, virt);

	/*
	 * If the virtual and physical address don't have the same offset
	 * within a page, we cannot map the region as the caller expects.
	 */
	// 要求物理地址和虚拟地址的offset必须一致
	if (WARN_ON((phys ^ virt) & ~PAGE_MASK))
		return;

	// 清除物理地址的offset
	phys &= PAGE_MASK;
	// 清除虚拟地址的offset,保存到addr中
	addr = virt & PAGE_MASK;
	// 确定映射的end的虚拟地址,这段区域必须按照页对齐
	end = PAGE_ALIGN(virt + size);

	// while循环,开始映射
	do {
		// 找到下一个pgd地址作为映射的结束地址,如果超过了end,则选择end
		next = pgd_addr_end(addr, end);
		//传入pgd页表项地址、虚拟地址起始地址和结束地址物理地址、prot等开始映射
		alloc_init_pud(pgdp, addr, next, phys, prot, pgtable_alloc,
			       flags);
		//映射完成,物理地址偏移,准备下一次映射
		phys += next - addr;
	} while (pgdp++, addr = next, addr != end);// pgdp移动到下一个页表项,虚拟地址跳到上一次结尾
}

参数:

  • pgdir: pgd 页表映射的虚拟地址,在 paging_init() 中使用的 fixmap 中 FIX_PGD 对应的空间;
  • phys: 物理起始地址;
  • virt: 对应的虚拟地址起始地址;
  • size: 映射的空间大小;
  • prot: 映射时权限;
  • pgtable_alloc: 回调函数;
  • flags: 映射时的flag;

5.1 pgd_addr_end()

#define pgd_addr_end(addr, end)						\
({	unsigned long __boundary = ((addr) + PGDIR_SIZE) & PGDIR_MASK;	\
	(__boundary - 1 < (end) - 1)? __boundary: (end);		\
})

addr 偏移的下一个界限是 1个PGD 大小,对于 3 级页表且页大小为4k 的系统来说,1 个 PGD 的大小为 1 << 30 .
当然,如果剩余的映射空间不够 1 个 PGD就到达 end时,将下个界限选则 end。

5.2 while 循环

	// while循环,开始映射
	do {
		// 找到下一个pgd地址作为映射的结束地址,如果超过了end,则选择end
		next = pgd_addr_end(addr, end);
		//传入pgd页表项地址、虚拟地址起始地址和结束地址物理地址、prot等开始映射
		alloc_init_pud(pgdp, addr, next, phys, prot, pgtable_alloc,
			       flags);
		//映射完成,物理地址偏移,准备下一次映射
		phys += next - addr;
	} while (pgdp++, addr = next, addr != end);// pgdp移动到下一个页表项,虚拟地址跳到上一次结尾

除去中间的映射函数,有几个变量:

  • next: 下一个界限,也是下一次映射的起始地址;
  • phys: 当映射完成后物理地址会向后偏移 next-addr,即映射了的空间大小;
  • pgdp: 当映射完,while() 中先将 pgd 页表项往后移,准备下一次的映射;
  • addr: 当映射完成后 addr 会被赋值为 next,即上一次映射的结尾地址,也是下一次映射的起始地址;
  • 当然,如果下一个界限为end,表示此次的映射完毕,退出循环;

注意,
while循环中,确定了 pgdp,虚拟地址的起始地址、结尾地址,物理地址,prot,pgtable_alloc 的回调函数,flags 等参数后,调用 alloc_init_pud() 进行该区域的映射。因为代码比较多,单独一节来剖析说明,详细看第 6 节。

6. alloc_init_pud()

static void alloc_init_pud(pgd_t *pgdp, unsigned long addr, unsigned long end,
			   phys_addr_t phys, pgprot_t prot,
			   phys_addr_t (*pgtable_alloc)(int),
			   int flags)
{
	unsigned long next;
	pud_t *pudp;
	p4d_t *p4dp = p4d_offset(pgdp, addr);
	p4d_t p4d = READ_ONCE(*p4dp);

	if (p4d_none(p4d)) {
		p4dval_t p4dval = P4D_TYPE_TABLE | P4D_TABLE_UXN;
		phys_addr_t pud_phys;

		if (flags & NO_EXEC_MAPPINGS)
			p4dval |= P4D_TABLE_PXN;
		BUG_ON(!pgtable_alloc);
		pud_phys = pgtable_alloc(PUD_SHIFT);   ///动态分配一个pud,填充pgd表项
		__p4d_populate(p4dp, pud_phys, p4dval);
		p4d = READ_ONCE(*p4dp);
	}
	BUG_ON(p4d_bad(p4d));
 
	//获取pudp,对于三级页表来说pudp也就是pgdp
	pudp = pud_set_fixmap_offset(p4dp, addr);  ///pgd表项保存的是pud的物理地址,要线转换成虚拟地址,CPU才能访问
	// 按照pud 逐级映射
    // 对于三级页表来说没有pud,所以,pudp 也就是pgdp,获取的next 直接等于end
    // 即,对于三级页表来说,在pmd 映射中会映射完整个区域
	do {
		pud_t old_pud = READ_ONCE(*pudp);
        // 确定下一次映射的pud 界限
        // 对于三级页表来说,没有pud 页表,所以,next 直接等于end,整个区域在pmd 中映射完成
		next = pud_addr_end(addr, end);

		/*
		 * For 4K granule only, attempt to put down a 1GB block
		 */
		if (use_1G_block(addr, next, phys) && //针对1G block,对于三级页表来说不会进这个case
		    (flags & NO_BLOCK_MAPPINGS) == 0) {
			pud_set_huge(pudp, phys, prot);

			/*
			 * After the PUD entry has been populated once, we
			 * only allow updates to the permission attributes.
			 */
			BUG_ON(!pgattr_change_is_safe(pud_val(old_pud),
						      READ_ONCE(pud_val(*pudp))));
		} else { //-----对于三级页表,着重来看这里
			alloc_init_cont_pmd(pudp, addr, next, phys, prot,
					    pgtable_alloc, flags);

			BUG_ON(pud_val(old_pud) != 0 &&
			       pud_val(old_pud) != READ_ONCE(pud_val(*pudp)));
		}
		phys += next - addr;
	} while (pudp++, addr = next, addr != end);
	// 对于三级页表,没有pud一级页表,这里是空函数
    // 但对于有pud页表的,需要清理fixmap中的FIX_PUD对应的pte值
	pud_clear_fixmap();
}

本专题系列主要针对三级页表,在博文最开始已经约定。
对于三级页表来说,代码比较清晰的,通过 alloc_init_cont_pmd() 来对整个区域进行映射。
同样的,确定了 pgdp,虚拟地址的起始地址、结尾地址,物理地址,prot,pgtable_alloc 的回调函数,flags 等参数后,调用 alloc_init_cont_pmd() 进行该区域的映射。因为代码比较多,单独一节来剖析说明,详细看第 7 节。

7. alloc_init_cont_pmd()

static void alloc_init_cont_pmd(pud_t *pudp, unsigned long addr,
				unsigned long end, phys_addr_t phys,
				pgprot_t prot,
				phys_addr_t (*pgtable_alloc)(int), int flags)
{
	unsigned long next;
	// 读取pudp 的值,即pud 页表项的值,代表pmd 页表的基地址
	pud_t pud = READ_ONCE(*pudp);

	/*
	 * Check for initial section mappings in the pgd/pud.
	 */
	BUG_ON(pud_sect(pud));
	//如果pud 的值为空,则需要通过__pud_populate() 进行填充,这里涉及到了 pgtable_alloc(),创建新的页表
	if (pud_none(pud)) {
		pudval_t pudval = PUD_TYPE_TABLE | PUD_TABLE_UXN;
		phys_addr_t pmd_phys;

		if (flags & NO_EXEC_MAPPINGS)
			pudval |= PUD_TABLE_PXN;
		BUG_ON(!pgtable_alloc);
		pmd_phys = pgtable_alloc(PMD_SHIFT);     ///动态分配pmd页表,填充pud表项 
		__pud_populate(pudp, pmd_phys, pudval);
		pud = READ_ONCE(*pudp);
	}
	BUG_ON(pud_bad(pud));
	// 按照pmd逐级映射,通过pmd_cont_addr_end()找到下一个pmd的界限,并将其作为下一次的起始地址
	do {
		pgprot_t __prot = prot;

		next = pmd_cont_addr_end(addr, end);

		/* use a contiguous mapping if the range is suitably aligned */
		//如果映射的地址恰好特殊对齐,则可以连续映射
		if ((((addr | next | phys) & ~CONT_PMD_MASK) == 0) &&
		    (flags & NO_CONT_MAPPINGS) == 0)
			__prot = __pgprot(pgprot_val(prot) | PTE_CONT);

		init_pmd(pudp, addr, next, phys, __prot, pgtable_alloc, flags);

		phys += next - addr;
	} while (addr = next, addr != end);
}

函数重点有三处:

  • pgtable_alloc() 调用, 当页表项的值为空时,表示下一级页表还没有创建,需要通过该函数创建一个新的页表,并将页表的地址填充到该页表项中;
  • 确定连续映射, 当 flags 设置了NO_CONT_MAPPING时,可以通过映射的地址恰好特殊对齐,来决定后面的映射权限;
  • init_pmd调用, 确定 pte 页表的映射;

7.1 init_pmd()

static void init_pmd(pud_t *pudp, unsigned long addr, unsigned long end,
		     phys_addr_t phys, pgprot_t prot,
		     phys_addr_t (*pgtable_alloc)(int), int flags)
{
	unsigned long next;
	pmd_t *pmdp;
	// pudp指定的是pmd 页表的基地址,需要先对其进行映射,映射到 fixmap的FIX_PMD
	pmdp = pmd_set_fixmap_offset(pudp, addr);   ///转换成虚拟地址,CPU才能访问
	// 上一级函数是alloc_init_cont_pmd() 可能是连续的pmd映射
    // 这里将连续的pmd映射分解,按照PMD_SIZE进行逐步映射
	do {
		pmd_t old_pmd = READ_ONCE(*pmdp);

		next = pmd_addr_end(addr, end);

		/* try section mapping first */
		if (((addr | next | phys) & ~PMD_MASK) == 0 &&
		    (flags & NO_BLOCK_MAPPINGS) == 0) {
			pmd_set_huge(pmdp, phys, prot);

			/*
			 * After the PMD entry has been populated once, we
			 * only allow updates to the permission attributes.
			 */
			BUG_ON(!pgattr_change_is_safe(pmd_val(old_pmd),
						      READ_ONCE(pmd_val(*pmdp))));
		} else {
			alloc_init_cont_pte(pmdp, addr, next, phys, prot,
					    pgtable_alloc, flags);

			BUG_ON(pmd_val(old_pmd) != 0 &&
			       pmd_val(old_pmd) != READ_ONCE(pmd_val(*pmdp)));
		}
		phys += next - addr;
	} while (pmdp++, addr = next, addr != end);
	//同理,映射完需要清理fixmap映射关系
	pmd_clear_fixmap();
}

该函数是通过上级 alloc_init_cont_pmd() 调用,可能连续映射,而这里继续分解成按照 PMD_SIZE 进行下面 pte 页表的映射处理。 alloc_init_cont_pte() 可以查看下一节。

8. alloc_init_cont_pte()

static void alloc_init_cont_pte(pmd_t *pmdp, unsigned long addr,
				unsigned long end, phys_addr_t phys,
				pgprot_t prot,
				phys_addr_t (*pgtable_alloc)(int),
				int flags)
{
	unsigned long next;
	pmd_t pmd = READ_ONCE(*pmdp);

	BUG_ON(pmd_sect(pmd));
	if (pmd_none(pmd)) {
		pmdval_t pmdval = PMD_TYPE_TABLE | PMD_TABLE_UXN;
		phys_addr_t pte_phys;

		if (flags & NO_EXEC_MAPPINGS)
			pmdval |= PMD_TABLE_PXN;
		BUG_ON(!pgtable_alloc);
		pte_phys = pgtable_alloc(PAGE_SHIFT);   ///动态分配pte页表,填充pmd表项
		__pmd_populate(pmdp, pte_phys, pmdval);
		pmd = READ_ONCE(*pmdp);
	}
	BUG_ON(pmd_bad(pmd));

	do {
		pgprot_t __prot = prot;

		next = pte_cont_addr_end(addr, end);

		/* use a contiguous mapping if the range is suitably aligned */
		if ((((addr | next | phys) & ~CONT_PTE_MASK) == 0) &&
		    (flags & NO_CONT_MAPPINGS) == 0)
			__prot = __pgprot(pgprot_val(prot) | PTE_CONT);

		init_pte(pmdp, addr, next, phys, __prot);

		phys += next - addr;
	} while (addr = next, addr != end);
}

到这里函数流程其实很清晰的,最开始同样确认 PMD 页表项的值,如果为空,则通过 pgtable_alloc() 创建新的页表并进行填充 pmd 页表项。新的页表也就是 PTE 页表,接着通过调用 init_pte() 进行更进一步映射。

总体来说就是逐级页表建立映射关系,同时中间会进行权限的控制等。

9. early_pgtable_alloc()

static phys_addr_t __init early_pgtable_alloc(int shift)
{
	phys_addr_t phys;
	void *ptr;

	//从memblock 中申请PAGE_SIZE的空间
	phys = memblock_phys_alloc(PAGE_SIZE, PAGE_SIZE);
	if (!phys)
		panic("Failed to allocate page table page\n");

	/*
	 * The FIX_{PGD,PUD,PMD} slots may be in active use, but the FIX_PTE
	 * slot will be free, so we can (ab)use the FIX_PTE slot to initialise
	 * any level of table.
	 */
	///通过fixmap映射得到虚拟地址,可以访问,清零
	ptr = pte_set_fixmap(phys);

	memset(ptr, 0, PAGE_SIZE);

	/*
	 * Implicit barriers also ensure the zeroed page is visible to the page
	 * table walker
	 */
	///清除临时pte页表项
	pte_clear_fixmap();

	return phys;
}

这里接续分析 memblock_phys_alloc():

static inline phys_addr_t memblock_phys_alloc(phys_addr_t size,
					      phys_addr_t align)
{
	return memblock_phys_alloc_range(size, align, 0,
					 MEMBLOCK_ALLOC_ACCESSIBLE);
}

注意, 这里最后一个参数指定的是 memblock 区域的结尾地址,这里选择 MEMBLOCK_ALLOC_ACCESSIBLE ,当从memblock中查找时,会从最大的区域分配。
所以,如果将 memblock 的debug功能打开时,paging_init() 函数中会有这样的打印:

<6>[    0.000000] memblock_reserve: [0x00000000fffff000-0x00000000ffffffff] memblock_alloc_range_nid+0xa8/0x164
<6>[    0.000000] memblock_reserve: [0x00000000ffffe000-0x00000000ffffefff] memblock_alloc_range_nid+0xa8/0x164
<6>[    0.000000] memblock_reserve: [0x00000000ffffd000-0x00000000ffffdfff] memblock_alloc_range_nid+0xa8/0x164
<6>[    0.000000] memblock_reserve: [0x00000000ffffc000-0x00000000ffffcfff] memblock_alloc_range_nid+0xa8/0x164
<6>[    0.000000] memblock_reserve: [0x00000000ffffb000-0x00000000ffffbfff] memblock_alloc_range_nid+0xa8/0x164
<6>[    0.000000] memblock_reserve: [0x00000000ffffa000-0x00000000ffffafff] memblock_alloc_range_nid+0xa8/0x164
 
...
 
...
 
<6>[    0.000000] memblock_reserve: [0x00000000ffc5e000-0x00000000ffc5efff] memblock_alloc_range_nid+0xa8/0x164

log 很清晰的看到在创建页表,并且将这些页表内存添加到 reserved 中,不参与 buddy 系统的管理。

10. 页表替换及内存释放


void __init paging_init(void)
{
	...
 
    pgd_clear_fixmap();
 
	cpu_replace_ttbr1(lm_alias(swapper_pg_dir));
	init_mm.pgd = swapper_pg_dir;
 
	memblock_free(__pa_symbol(init_pg_dir),
		      __pa_symbol(init_pg_end) - __pa_symbol(init_pg_dir));
 
	memblock_allow_resize();
}

我们在第 1 节中提到 map_kernel()map_mem() 的参数是 pgdp,在映射完成之后会清理掉 swapper_pg_dir 到 fixmap 的映射关系,并将内核的页表 PGD 的首地址也就是 swapper_pg_dir 存入寄存器 TTBr1 中。

10.1 init_pg_dir 内存释放

最后,我们来看下 init_pg_dir 内存。
这一部分内存作为内核一部分,前期用于临时页表的映射。当 init_mm.pgd 使用 swapper_pg_dir 之后, init_pg_dir 的物理内存就可以释放。

vmlinux.lds.S中

. = ALIGN(PAGE_SIZE);
	init_pg_dir = .;
	. += INIT_DIR_SIZE;
	init_pg_end = .;
 
	__pecoff_data_size = ABSOLUTE(. - __initdata_begin);
	_end = .;

对于三级页表来说,init_pg_dir 会占用 3 个 pages 的内存空间,从System.map 看起来更加清晰。

这一块内存,在memblock 初始化时加入到 memblock.reserved (在区间 [_text, _end) 中),这里释放后续可以加入到 buddy 系统中。详细可以查看[linux内存管理] 第008章 memblock子系统详解一文第 2 节。当 memblock debug 功能打开时,会有这样的log 信息:

<6>[    0.000000] memblock_free: [0x00000000c234f000-0x00000000c2351fff] paging_init+0x12c/0x1a4

11. 总结

paging_init流程图