Linux Kernel:直接映射区的构建

958 阅读1小时+
封面-直接映射区构建.png

本文采用 Linux 内核 v3.10 版本 x86_64 架构

我们在《Linux Kernel:内存管理之分页(Paging)》一文中介绍了内核启动时各级页表的创建。在那篇文章中,直接映射区只映射了 1GB 的物理内存。在本文中,我们将介绍直接映射区的完整构建过程。

一、基本概念

1.1 直接映射区

在 v3.10 版本下, Linux 的虚拟内存布局如下图所示:

Memory-Layout.png

其中,直接映射区从地址 PAGE_OFFSET(扩展为 0xFFFF880000000000) 开始,共计 64 TB 大小。Linux 内核会把从地址 0 开始的物理内存直接映射到该虚拟地址空间。换句话说,在直接映射区,物理地址加上 PAGE_OFFSET 就得到虚拟地址。为此,内核专门提供了 2 个宏用于直接映射区的物理和虚拟地址转换:__va__pa

// file: arch/x86/include/asm/page.h
#define __va(x)			((void *)((unsigned long)(x)+PAGE_OFFSET))

__va 用于将物理地址转换为直接映射区的虚拟地址,转换方法就是将物理地址直接加上 PAGE_OFFSET

// file: arch/x86/include/asm/page.h
#define __pa(x)		__phys_addr((unsigned long)(x))

__pa 用于将直接映射区内核代码映射区的虚拟地址转换为物理地址。

该宏最终扩展为 __phys_addr_nodebug 函数:

// file: arch/x86/include/asm/page_64.h
#define __phys_addr(x)		__phys_addr_nodebug(x)

static inline unsigned long __phys_addr_nodebug(unsigned long x)
{
	unsigned long y = x - __START_KERNEL_map;

	/* use the carry flag to determine if x was < __START_KERNEL_map */
	x = y + ((x > y) ? phys_base : (__START_KERNEL_map - PAGE_OFFSET));

	return x;
}

__START_KERNEL_map 扩展为 0xFFFFFFFF80000000,表示内核代码映射区的起始地址。

当虚拟地址 x 大于 __START_KERNEL_map时,说明该地址位于内核代码映射区__pa 扩展为 x - __START_KERNEL_map + phys_base,其中 phys_base 是内核代码的起始物理地址;当虚拟地址 x 小于 __START_KERNEL_map时,说明虚拟地址位于直接映射区__pa 扩展为 x - PAGE_OFFSET

1.2 分页大小

在 x86-64 架构下,处理器可支持 4KB、2MB 和 1GB 三种大小的页。其中,4KB 大小的页总是支持的;如果处理器支持 PSE (page-size extensions)特征,说明支持 2MB 大小的页;如果处理器支持 pdpe1gb 特征,说明支持 1GB 大小的页。

注:在 x86-64 架构下,处理器总是支持 PSE 特征的,所以 2MB 大小的页总是支持的;但是,并不是所有处理器都支持 1GB 大小的页。

可通过 cat /proc/cpuinfo 或者 lscpu 命令,查看处理器支持的特征。如果 Flags 项里有 pdpe1gb 特征,说明处理器支持 1GB 大小的页。

cpu_pdpe1gb.png

顺便说一句,当我们使用 cat /proc/cpuinfo 指令查询处理器信息时,实际是调用 arch/x86/kernel/cpu/proc.c 文件下的 show_cpuinfo 函数来打印处理器信息的。这些信息是在进行处理器探测时,保存到 cpuinfo_x86 结构体中的。

1.3 内存的分页机制

本文涉及到大量的内存分页相关内容,包括原理、数据结构、接口等等,这些内容在 《Linux Kernel:内存管理之分页(Paging)》 和《x86-64架构:内存分页机制》中进行了详细的介绍,不熟悉的同学可以翻翻这两篇文章。

本文用到的《Linux Kernel:内存管理之分页(Paging)》中的常量、数据结构以及接口如下表所示:

数据结构/接口说明
常量
PTE_PFN_MASK页帧掩码,见 4.1.4
PTE_FLAGS_MASK页属性掩码,见 4.1.5
PTRS_PER_PUD每个上层页目录中包含的表项数量,见 4.1.1
PTRS_PER_PMD每个中层页目录中包含的表项数量,见 4.1.1
PTRS_PER_PTE每个页表中包含的表项数量,见 4.1.1
PGDIR_MASK全局页目录掩码,低 39 位为0,见 4.1.2
PGDIR_SIZE全局页目项控制的内存大小,扩展为 512 GB,见 4.1.2
PUD_MASK上层页目录项掩码,低 30 位为 0,见 4.1.2
PUD_SIZE上层页目项控制的内存大小,扩展为 1GB,见 4.1.2
PMD_MASK中层页目录项掩码,低 21 位为 0,见 4.1.2
PMD_SIZE中层页目项控制的内存大小,扩展为 2MB,见 4.1.2
PAGE_MASK页掩码,低 12 位为 0,见 4.1.2
PAGE_SIZE页大小,扩展为 4KB,见 4.1.2
数据结构
pgd_t全局页目录项结构体,见 4.2.1
pud_t上层页目录项结构体,见 4.2.1
pmd_t中层页目录项结构体,见 4.2.1
pte_t页表项结构体,见 4.2.1
pgprot_t页属性结构体,见 4.2.1
接口
pud_index获取虚拟地址对应的上层页目录项索引,见 4.3.1
pmd_index获取虚拟地址对应的中层页目录项索引,见 4.3.1
pte_index获取虚拟地址对应的页表项索引,见 4.3.1
pgd_val获取全局页目录项的值,见 4.3.2
pud_val获取上层页目录项的值,见 4.3.2
pmd_val获取中层页目录项的值,见 4.3.2
pte_val获取页表项的值,见 4.3.2
set_pud设置上层页目录项的值,见 4.3.3
set_pmd设置中层页目录项的值,见 4.3.3
set_pte设置页表项的值,见 4.3.3
pgd_page_vaddr获取全局页目录项所指向的上层页目录的虚拟地址,见 4.3.7
pud_page_vaddr获取上层页目录项所指向的中层页目录的虚拟地址,见 4.3.7
pmd_page_vaddr获取中层页目录项所指向的页表的虚拟地址,见 4.3.7
pgd_offset_k获取内核空间全局页目录项的虚拟地址,见 4.3.8
pud_offset获取上层页目录项的虚拟地址,见 4.3.8
pmd_offset获取中层页目录项的虚拟地址,见 4.3.8
pgd_populate填充全局页目录项,内部会调用 set_pgd 函数,见 4.3.9
pud_populate填充上层页目录项,内部会调用 set_pud 函数,见 4.3.9
pmd_populate_kernel填充中层页目录项,内部会调用 set_pmd 函数,见 4.3.9

本文用到的新的分页相关接口如下表所示:

接口说明
pud_large检测上层页目项是否直接映射为 1GB 页,见本文 “4.1” 节
pmd_large检测中层页目录项是否直接映射为 2MB 页,见本文 “4.2”
pte_clrhuge清除页表项中的 PS 位(位 7),PS 位指示该表项是否直接映射到 1GB 或 2MB 的巨页,见本文 “4.3” 节
pte_pgprot屏蔽掉页表项中的物理地址位,只保留属性位,见本文 “4.4” 节
pfn_pte将页帧号和页属性组合成页表项格式,见本文 “4.5” 节

1.4 E820 内存

我们在 《Linux Kernel:物理内存布局探测》一文中介绍过,x86 系统架构是通过 E820 接口来探测物理内存的。E820 内存分为多种类型:

 // file: arch/x86/include/uapi/asm/e820.h
 #define E820_RAM    1
 #define E820_RESERVED   2
 #define E820_ACPI   3
 #define E820_NVS    4
 #define E820_UNUSABLE   5

/*
 * reserved RAM used by kernel itself
 * if CONFIG_INTEL_TXT is enabled, memory of this type will be
 * included in the S3 integrity calculation and so should not include
 * any memory that BIOS might alter over the S3 transition
 */
#define E820_RESERVED_KERN        128

其中,E820_RAME820_RESERVED_KERN 都是可用内存类型。

由于内存中会有空洞,所以探测到的内存不是一个大的连续的内存空间,而是一段一段的小的内存空间,我们称为内存块

内核使用结构体 e820entry 来表示内存块:

// file: arch/x86/include/uapi/asm/e820.h
struct e820entry {
	__u64 addr;	/* start of memory segment */
	__u64 size;	/* size of memory segment */
	__u32 type;	/* type of memory segment */
} __attribute__((packed));

其中,addr 表示内存块的起始地址;size 表示内存块的大小;type 表示内存类型。

然后,内核使用结构体 e820map 来保存所有的内存块信息。

// file: arch/x86/include/uapi/asm/e820.h
struct e820map {
	__u32 nr_map;
	struct e820entry map[E820_X_MAX];
};

其中,map 是一个数组,保存了所有的内存块信息;nr_map 表示数组中的内存块数量。

而全局变量 e820e820map 结构体的一个实例,探测到的内存块信息实际保存在 e820 中:

// file: arch/x86/kernel/e820.c
struct e820map e820;

后续如果需要查找内存信息,就可以直接从 e820 中获取。

E820_X_MAX 指示内存块的最大数量,该值依赖于内核配置选项 CONFIG_EFI

// file: arch/x86/include/asm/e820.h
#ifdef CONFIG_EFI
#include <linux/numa.h>
#define E820_X_MAX (E820MAX + 3 * MAX_NUMNODES)
#else	/* ! CONFIG_EFI */
#define E820_X_MAX E820MAX
#endif

在早期的系统中,该值为 E820MAX,即 128;在支持 EFI 的系统中,可能会有更多的内存块存在,所以对内存块最大数量做了扩展,扩展后为 E820MAX + 3 * MAX_NUMNODES,其中宏 MAX_NUMNODES 表示系统支持的最大 NUMA 节点数量,默认值为 1024。

// file: arch/x86/include/uapi/asm/e820.h
#define E820MAX	128		/* number of entries in E820MAP */

/*
 * Legacy E820 BIOS limits us to 128 (E820MAX) nodes due to the
 * constrained space in the zeropage.  If we have more nodes than
 * that, and if we've booted off EFI firmware, then the EFI tables
 * passed us from the EFI firmware can list more nodes.  Size our
 * internal memory map tables to have room for these additional
 * nodes, based on up to three entries per node for which the
 * kernel was built: MAX_NUMNODES == (1 << CONFIG_NODES_SHIFT),
 * plus E820MAX, allowing space for the possible duplicate E820
 * entries that might need room in the same arrays, prior to the
 * call to sanitize_e820_map() to remove duplicates.  The allowance
 * of three memory map entries per node is "enough" entries for
 * the initial hardware platform motivating this mechanism to make
 * use of additional EFI map entries.  Future platforms may want
 * to allow more than three entries per node or otherwise refine
 * this size.
 */

1.5 MemBlock 内存分配器

通过 E820 接口完成物理内存探测后,就需要对内存进行管理,比如内存分配、内存回收等等。在伙伴系统就绪之前,是通过 MemBlock 内存分配器来管理内存的。MemBlock 内存分配器将内存分为 2 种类型:可用的(memory 类型)和不可用的(reserved 类型)。在 MemBlock 分配器刚完成初始化时,其可用内存为空,无法分配内存。内核通过 memblock_x86_fill() 函数将 E820 内存中的 E820_RAME820_RESERVED_KERN 类型的内存块添加到 memory 类型的内存中。填充完成后,就能进行内存分配了。

MemBlock 分配器使用 3 种数据结构来管理内存:struct memblock_regionstruct memblock_typestruct memblock。其中,struct memblock是最顶级结构,也是 MemBlock 分配器名称的由来,其次是 struct memblock_type;最底层的是 struct memblock_region

与 E820 内存块类似,MemBlock 也是通过内存块(即 struct memblock_region)来管理的,只不过 memblock_region 中不再需要类型字段,而是增加了节点ID 字段 nid

 // file: include/linux/memblock.h
 struct memblock_region {
     phys_addr_t base;
     phys_addr_t size;
 #ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
     int nid;
 #endif
 };

MemBlock 层级分配器如下图所示:

memblock_overview.png

当需要分配内存时,既可以通过 memblock_find_in_range 函数在所有节点中查找可用内存,也可以通过 memblock_find_in_range_node 函数在指定节点中查找可用内存。分配完成后,要通过 memblock_reserve 函数将分配出去的内存区间加入到 reserved 类型的内存中,表示这段内存不能使用了。

MemBlock 内存分配器详细内容请参考:《Linux Kernel:启动时内存管理(MemBlock 分配器)

1.6 同步内核页表

每个用户进程都有自己独立的全局页目录,进而有着独立的页表。内核将所有用户进程的全局页目录地址放入一个双向链表 pgd_list (见 “5.3.2.9 pgd_list” 节)里进行管理。

内核空间也有自己的全局页目录和页表。内核空间的全局页目录的虚拟地址为 swapper_pg_dir

#define swapper_pg_dir init_level4_pgt

当我们使用 fork 系统调用创建一个新进程时,内核会为新进程的全局页目录分配内存并进行构建。全局页目录的创建过程如下所示:

do_fork() -> copy_process() -> copy_mm() -> dup_mm() -> mm_init() -> mm_alloc_pgd() -> pgd_alloc()

pgd_alloc 函数中,会为全局页目录分配内存并调用 pgd_ctor 函数来构建全局页目录:

// file: 
pgd_t *pgd_alloc(struct mm_struct *mm)
{
    pgd_t *pgd;
......
	pgd = (pgd_t *)__get_free_page(PGALLOC_GFP);
......
	mm->pgd = pgd;
......
	pgd_ctor(mm, pgd);
......
	return pgd;
......
}

pgd_ctor 函数定义如下:

static void pgd_ctor(struct mm_struct *mm, pgd_t *pgd)
{
	/* If the pgd points to a shared pagetable level (either the
	   ptes in non-PAE, or shared PMD in PAE), then just copy the
	   references from swapper_pg_dir. */
	if (PAGETABLE_LEVELS == 2 ||
	    (PAGETABLE_LEVELS == 3 && SHARED_KERNEL_PMD) ||
	    PAGETABLE_LEVELS == 4) {
		clone_pgd_range(pgd + KERNEL_PGD_BOUNDARY,
				swapper_pg_dir + KERNEL_PGD_BOUNDARY,
				KERNEL_PGD_PTRS);
	}

	/* list required to sync kernel mapping updates */
	if (!SHARED_KERNEL_PMD) {
		pgd_set_mm(pgd, mm);
		pgd_list_add(pgd);
	}
}

pgd_ctor 函数中会做 2 件事:

1、调用 clone_pgd_range 函数将内核空间的全局页目录项拷贝到新进程的全局页目录中

clone_pgd_range.png

2、调用 pgd_list_add 函数(见 “5.3.2.10 pgd_list_add” 节)将新进程的全局页目录加入 pgd_list 列表中。

当内核修改了自己的全局页目录时,即新增或修改了内核空间的全局页目录项,就需要将修改同步到所有用户进行的页表中

上文介绍过,所有用户进程的全局页目录都被链入了双向链表 pgd_list 中。此时,就需要遍历 pgd_list 中的每一个成员,将修改后的全局页目录项拷贝到每个用户进程的全局页目录中。同步过程类似于 clone_pgd_range 函数,不同的是, clone_pgd_range 函数会同步内核空间的所有全局页目录项,而此时只需要同步部分全局页目录项。实现细节请参考 “5.3.2.8 sync_global_pgds” 小节。

二、映射流程

直接映射区的构建是在 init_mem_mapping 函数中完成的,整个映射流程大致分为以下几个步骤:

2.1 探测系统支持的页大小

上文介绍过,x86-64 系统最多支持 3 种页大小: 4KB、2MB 和 1GB 。其中,4KB 页是默认支持的,而 2MB 和 1GB 的页是否支持,由处理器特征确定。内核通过 probe_page_size_mask 函数,检测系统是否支持 2MB 和 1GB 的页。检测结果,会保存在位图 page_size_mask 中。

2.2 分段映射

进行映射时,并不是一次性直接映射整个内存区间,而是分成一段段小内存进行映射。

2.2.1 按页大小分割映射区间

分段后,进型映射时,根据系统支持的页大小以及映射区间的大小,最多可将映射区间分成如下所示的 5 个部分:

split_mem_range-1.png

如果系统不支持 1GB 的页,那么最多只能分割成 3 个部分:

split_mem_range-2.png

分割后,不同页大小的映射区间保存到 struct map_range 类型的数组中。map_range 结构定义如下:

// file: arch/x86/mm/init.c
struct map_range {
	unsigned long start;
	unsigned long end;
	unsigned page_size_mask;
};

其中,start 指示区间的起始地址;end 指示区间的结束地址;page_size_mask 指示该区间能够映射的页大小的掩码。

注:page_size_mask 中,无论 4KB 页对应的比特位是否置位,4KB 页都是可用的。对于 1GB 页映射区,page_size_mask 中 2MB 和 1GB 页对应的比特位都会置位;对于 2MB 页映射区,page_size_mask 中 2MB 页对应的比特位会置位。实际映射时,会优先选用最大的页来映射。

另外,在进行不同页大小的内存区间分割时,不同区间的边界要求是页大小的整数倍,这就会出现内存空间比页大但却不能映射该大小页的情况。比如,有一段内存空间为 6KB ~ 2012 MB,该段内存比 1GB 要大,但却无法映射 1GB 页。因为 1GB 页映射区的边界必须是 1GB 的整数倍,当将内存区下边界对齐到 1024MB 时,剩余的空间就不足 1GB 了,所以就无法映射 1GB 页。这种情况下,分割后的任何内存区间的 page_size_mask 中,1GB 页对应的比特位不会被置位。

split_mem_range-3.png

2.2.2 按照分割后的区间进行映射

在上一步中,将映射区间按照不同的页大小的进行了分割。这一步,就是为这些区间创建页表,进行映射。不同大小的页,其页表的层级也是不同的。对于 1GB 页来说,只需要全局页目录(Page Global Directory,PGD)和上层页目录(Page Upper Directory,PUD)就足够了;对于 2MB 页来说,除了 PGD 和 PUD 之外,还需要中层页目录(Page Middle Directory,PUD);对于 4KB 页来说,还需要页表(Page Table,PT)。

注:在 phys_pud_init 函数中,构建上层页目录;在 phys_pmd_init 函数中,构建中层页目录;在 phys_pte_init 函数中,构建页表。

2.2.3 将内核空间页表同步给用户进程

如果映射后,内核空间的全局页目录项有变化,需要将修改同步给所有的用户进程。具体来说,就是用修改后的全局页目录项覆盖掉用户进程的同位置的全局页目录项。

同步工作是在 sync_global_pgds 函数中进行的,具体可参考 “5.3.2.8 sync_global_pgds” 小节。。

2.2.4 刷新 TLB

在进行映射时,页表项进行了大量修改,原先的 TLB 已经过时了,所以调用 __flush_tlb_all 函数使所有的 TLB 失效。

2.3 重新加载 CR3 并刷新 TLB

通过 2 行代码实现该功能:

void __init init_mem_mapping(void)
{
......
    load_cr3(swapper_pg_dir);
	__flush_tlb_all();
......
}

2.4 内存坏块检测

这是通过 early_memtest 函数实现的。

......
early_memtest(0, max_pfn_mapped << PAGE_SHIFT);
......

是否需要执行内存坏区检测,依赖于命令行参数 memtest。该参数默认值为 0,意味着不进行检测。

检测原理就是以 8 字节为单位向内存写入数据然后再读出数据,如果读出的数据与写入的一致,说明这 8 字节内存是好的;否则,说明这块内存是坏的。

三、查看直接映射区信息

通过 cat /proc/meminfo 命令,可以查看系统的内存信息,其中就包括直接映射区的信息。

下图是我的虚拟机(1核2G)的内存信息,红框内的是直接映射区信息:

meminfo.png

其中,DirectMap4k 表示映射为 4KB 页的总内存大小,DirectMap2M 表示映射为 2MB 页的总内存大小,DirectMap1G 表示映射为 1GB 页的总内存大小。

cat /proc/meminfo 命令会调用 fs/proc/meminfo.c 文件中的 meminfo_proc_show 函数来输出内存信息:

// file: fs/proc/meminfo.c
static int meminfo_proc_show(struct seq_file *m, void *v)
{
......
	arch_report_meminfo(m);
......
}

其中,直接映射区的信息是通过 arch_report_meminfo 函数输出的:

void arch_report_meminfo(struct seq_file *m)
{
	seq_printf(m, "DirectMap4k:    %8lu kB\n",
			direct_pages_count[PG_LEVEL_4K] << 2);
#if defined(CONFIG_X86_64) || defined(CONFIG_X86_PAE)
	seq_printf(m, "DirectMap2M:    %8lu kB\n",
			direct_pages_count[PG_LEVEL_2M] << 11);
#else
	seq_printf(m, "DirectMap4M:    %8lu kB\n",
			direct_pages_count[PG_LEVEL_2M] << 12);
#endif
#ifdef CONFIG_X86_64
	if (direct_gbpages)
		seq_printf(m, "DirectMap1G:    %8lu kB\n",
			direct_pages_count[PG_LEVEL_1G] << 20);
#endif
}

在创建直接映射区的页表时,会把不同大小的页的数量保存到 direct_pages_count 数组中,查询时直接从中获取即可。

注:在 phys_pud_init 函数中,会更新 1GB 页的数量;在 phys_pmd_init 函数中,会更新 2MB 页的数量;在 phys_pte_init函数中,会更新 4KB 页的数量。数量更新是通过 update_page_count 函数进行的,该函数会把不同大小的页数量保存在 direct_pages_count 中。

四、分页相关接口

4.1 pud_large

pud_large 函数检测上层页目录项的 PS 位(位 7)和 P 位(位 0)是否全部为1。如果是,说明该上层页目录项直接映射到大页,返回 true;否则,返回 false。

// file: arch/x86/include/asm/pgtable.h
static inline int pud_large(pud_t pud)
{
	return (pud_val(pud) & (_PAGE_PSE | _PAGE_PRESENT)) ==
		(_PAGE_PSE | _PAGE_PRESENT);
}

上层页目录项格式如下(根据是否直接映射到页有 2 种格式):

PDPTE_for_1G_format.png PDPTE_for_PD_format.png

我们主要关注 P 位(位 0)、PS 位(位 7):

  • P 位(Present,存在位)标识当前页是否在内存中;如果为 1,说明在内存中,可以直接访问;如果为 0,访问时会触发 Page-Fault 异常。
  • PS (Page Size)位,如果为 1 ,表示当前项直接映射到页;如果为 0,表示该页结构项引用了另一个页结构。在上层页目录项里,如果该位为 1,指示该页表项映射到 1GB 页。

4.2 pmd_large

pmd_large 函数用于检测中层页目录项是否直接映射到 2MB 的页。如果是,返回 true;否则,返回 false。

static inline int pmd_large(pmd_t pte)
{
	return pmd_flags(pte) & _PAGE_PSE;
}

中层页目录项的格式如下图所示(根据是否直接映射到页有 2 种格式):

PDE_for_2M_format.png PDE_for_PT_format.png

我们主要关注 P 位(位 0)、PS 位(位 7):

  • P 位(Present,存在位)标识当前页是否在内存中;如果为 1,说明在内存中,可以直接访问;如果为 0,访问时会触发 Page-Fault 异常。
  • PS (Page Size)位,如果为 1 ,表示当前项直接映射到页;如果为 0,表示该页结构项引用了另一个页结构。在中层页目录项里,如果该位为 1,指示该页表项映射到 2MB 页。

pmd_flags 函数用于获取页表项的页标志,该函数定义如下:

static inline pmdval_t pmd_flags(pmd_t pmd)
{
	return native_pmd_val(pmd) & PTE_FLAGS_MASK;
}

native_pmd_val 函数获取页表项的值,宏PTE_FLAGS_MASK 是页表项的标志位掩码,该掩码的位 12 ~ 位 45 为 0,其余位为1,如下图所示:

PTE_FLAGS_MASK.png

页表项与 PTE_FLAGS_MASK 按位与操作后,就会将页表项中物理地址位清零,仅保留页标志位。

PTE_FLAGS_MASK 的具体实现可参考 《Linux Kernel:内存管理之分页(Paging)4.1.5 页属性掩码 -- PTE_FLAGS_MASK

4.3 pte_clrhuge

pte_clrhuge 函数会清除掉页表项中的 PS 位(位 7),其内部直接调用 pte_clear_flags 函数完成清除功能。

注:当 PS 位为 1 时,表示该表项直接映射到页;否则,表示该页表项引用了下一级页表。

// file: arch/x86/include/asm/pgtable.h
static inline pte_t pte_clrhuge(pte_t pte)
{
	return pte_clear_flags(pte, _PAGE_PSE);
}
// file: pte_clrhuge
#define _PAGE_PSE	(_AT(pteval_t, 1) << _PAGE_BIT_PSE)
#define _PAGE_BIT_PSE		7	/* 4 MB (or 2MB) page */

4.4 pte_pgprot

pte_pgprot 会屏蔽掉页表项中的物理地址位,只保留属性位,然后将其转换成包装类型 pgprotval_t

// file: arch/x86/include/asm/pgtable.h
#define pte_pgprot(x) __pgprot(pte_flags(x) & PTE_FLAGS_MASK)

页表项格式如下图所示:

PTE_format.png

PTE_FLAGS_MASK 格式见 “4.2 pmd_large” 节。

pte_flags 函数会屏蔽掉页表项中的物理地址位:

// file: arch/x86/include/asm/pgtable_types.h
static inline pteval_t pte_flags(pte_t pte)
{
	return native_pte_val(pte) & PTE_FLAGS_MASK;
}

native_pte_val 会对包装类型 pte_t 解包装,获取页表项的值:

static inline pteval_t native_pte_val(pte_t pte)
{
	return pte.pte;
}

__pgprot 会将页表项的属性位转换成 pgprot_t 类型:

// file: arch/x86/include/asm/pgtable_types.h
#define __pgprot(x)	((pgprot_t) { (x) } )

4.5 pfn_pte

pfn_pte 函数将页帧号和页属性组合成页表项格式,并通过 __pte 宏转换成 pte_t 类型。

// file: arch/x86/include/asm/pgtable.h
static inline pte_t pfn_pte(unsigned long page_nr, pgprot_t pgprot)
{
	return __pte(((phys_addr_t)page_nr << PAGE_SHIFT) |
		     massage_pgprot(pgprot));
}
// file: arch/x86/include/asm/pgtable.h
#define __pte(x)	((pte_t) { (x) } )

五、源码解析

直接映射区的构建,是在 init_mem_mapping 函数中进行的,其完整调用链为 start_kernel() -> setup_arch() -> init_mem_mapping()

5.1 init_mem_mapping

// file: arch/x86/mm/init.c
void __init init_mem_mapping(void)
{
	unsigned long end, real_end, start, last_start;
	unsigned long step_size;
	unsigned long addr;
	unsigned long mapped_ram_size = 0;
	unsigned long new_mapped_ram_size;

    /*
     * 检测处理器和内核支持的页大小,并将其保存到掩码 page_size_mask 中
     * Intel 处理器可能支持 4KB、2MB、1GB 大小的页,具体支持哪些,需要通过检测确定
     * 不同大小的页,对应着 page_size_mask 中不同的比特位
     * 4KB、2MB、1GB 页分别对应着 page_size_mask 中的位 1、位 2、位 3;
     * 探测完成后,通过测试 page_size_mask 中不同位置的比特位的值,就能判断出系统支持哪几种页大小
     */
	probe_page_size_mask();

/*
 * max_pfn 指示物理内存的最大页帧号
 * 如果物理内存大于 4GB,max_low_pfn 指示 4GB 内的最大页帧号;否则,max_low_pfn 等于 max_pfn
 * end 指示要映射的最大物理内存地址
 * 在 x86_64 系统下,end 表示 max_pfn 对应的物理地址
 * 在 32 位系统下,由于最大只能寻址 4GB 的内存,所以 end 表示 max_low_pfn 对应的物理地址
 * max_pfn 和 max_low_pfn 的计算过程见 5.1.1 小节
 */
#ifdef CONFIG_X86_64
	end = max_pfn << PAGE_SHIFT;
#else
	end = max_low_pfn << PAGE_SHIFT;
#endif
   
    /* 
     * 为 ISA (Industry Standard Architecture)区间(0 ~ 1MB)创建直接映射
     * ISA_END_ADDRESS 表示 ISA 区间的结束地址,扩展为 1MB
     * #define ISA_END_ADDRESS		0x100000
     */
    /* the ISA range is always mapped regardless of memory holes */
	init_memory_mapping(0, ISA_END_ADDRESS);

    /* 
     * memblock_find_in_range 函数原型:
     * memblock_find_in_range(phys_addr_t start,
							phys_addr_t end, phys_addr_t size,
							phys_addr_t align)
	 * 该函数在 start ~ end 范围内搜索 size 大小的可分配内存,内存边界对齐到 align 字节。
	 * 由于未指定分配节点,会搜索所有节点的可用内存。
	 * 需要强调的是,memblock_find_in_range 进行搜索时是反向搜索,即从高地址向低地址搜索,
	 * 所以,搜索到的是最靠近内存地址顶端的可用区间。
     * addr 是搜索到的内存区间的起始地址,real_end 是内存区间的结束地址
     * 在本例中,是搜索 PMD_SIZE(扩展为 2MB)大小的内存区间,且内存边界也要对齐到 PMD_SIZE
     */
    /* xen has big range in reserved near end of ram, skip it at first.*/
	addr = memblock_find_in_range(ISA_END_ADDRESS, end, PMD_SIZE, PMD_SIZE);
	real_end = addr + PMD_SIZE;

    /* 
     * step_size 表示每次映射的内存大小,初始化为 PMD_SIZE(扩展为 2MB),如果需要映射的内存区间很大,该值也会随之增大
     * max_pfn_mapped 表示已经映射的最大页帧号,初始化为 0,每次映射后都会更新
     * min_pfn_mapped 表示已经映射的最小页帧号,同样每次映射后都会更新
     * start 表示当前映射的起始地址,last_start 表示当前映射的内存区间的结束地址,也是下一次映射的起始地址,同样每次映射后都会更新
     */
    /* step_size need to be small so pgt_buf from BRK could cover it */
	step_size = PMD_SIZE;
	max_pfn_mapped = 0; /* will get exact value next */
	min_pfn_mapped = real_end >> PAGE_SHIFT;
	last_start = start = real_end;

    
    /* 
     * 从高地址向低地址进行分段映射,每次映射 step_size 大小
     * start 表示本次映射的起始地址,last_start 表示本次映射的内存区间的结束地址
     * 由于 0 ~ ISA_END_ADDRESS 之间的内存已经映射过了,这里映射的是从 ISA_END_ADDRESS 到最大内存地址的区间
     * 所以要求 last_start 和 start 都必须大于等于 ISA_END_ADDRESS
     */
    /*
	 * We start from the top (end of memory) and go to the bottom.
	 * The memblock_find_in_range() gets us a block of RAM from the
	 * end of RAM in [min_pfn_mapped, max_pfn_mapped) used as new pages
	 * for page table.
	 */
	while (last_start > ISA_END_ADDRESS) {
        /*
         * 计算 start 的值,要求对齐到 step_size
         * 如果剩余空间不足 step_size,说明到边界了,则将 start 设置为 ISA_END_ADDRESS
         */
		if (last_start > step_size) {
			start = round_down(last_start - 1, step_size);
			if (start < ISA_END_ADDRESS)
				start = ISA_END_ADDRESS;
		} else
			start = ISA_END_ADDRESS;
        
        /*
         * 通过 init_range_memory_mapping 函数对 start ~ last_start 的内存段进行直接映射,返回实际映射的内存大小
         * 因为 start ~ last_start 之间可能会有内存空洞,这些空洞没有做映射,所以实际映射的内存可能会比 last_start - start 小
         */
		new_mapped_ram_size = init_range_memory_mapping(start,
							last_start);
        
        /*
         * 更新 last_start 的值,这是下一次要映射的内存区间的结束地址
         * 更新 min_pfn_mapped (已映射内存的最小页帧)的值
         */
		last_start = start;
		min_pfn_mapped = last_start >> PAGE_SHIFT;
        
        /*
         * 如果本次映射的内存比以前映射的所有内存还大,则增大步进值
         * STEP_SIZE_SHIFT 扩展为 5,也就是说步进值扩大为原来的 32 (2的5次方)倍
         * 然后更新 mapped_ram_size 的值,即映射总量增加
         */
		/* only increase step_size after big range get mapped */
		if (new_mapped_ram_size > mapped_ram_size)
			step_size <<= STEP_SIZE_SHIFT;
		mapped_ram_size += new_mapped_ram_size;
	}

    /*
     * end 是最大可用物理内存地址,real_end 是已经映射的最大地址
     * 因为 real_end 是通过 memblock_find_in_range 函数找到的一段 2MB 连续内存的结束地址,并且还要对齐到 2MB,
     * 所以 real_end 可能会比 end 小,导致 real_end ~ end 之间的内存没有映射
     * 这种情况下,需要将这段内存空间也映射上
     */
	if (real_end < end)
		init_range_memory_mapping(real_end, end);

/*
 * 在 64 位架构下,因处理器可寻址空间足够大,所有的物理内存都能够寻址到,
 * 所以,不需要区分 max_pfn 和 max_low_pfn,将两者设置成相同的值
 */
#ifdef CONFIG_X86_64
	if (max_pfn > max_low_pfn) {
		/* can we preseve max_low_pfn ?*/
		max_low_pfn = max_pfn;
	}
#else
	......
#endif
    
    /*
     * swapper_pg_dir 是内核全局页目录的基地址,load_cr3 会将指定的全局页目录地址加载到 CR3 寄存器
     * 因为我们更新了很多页表项,所以调用 __flush_tlb_all 函数使所有的 TLB 缓存失效
     */
	load_cr3(swapper_pg_dir);
	__flush_tlb_all();
    
    /*
     * 内存坏区检测
     */
	early_memtest(0, max_pfn_mapped << PAGE_SHIFT);
}

5.1.1 max_pfn 和 max_low_pfn

全局变量 max_pfnmax_low_pfnsetup_arch 函数中被赋值:

// file: arch/x86/kernel/setup.c
void __init setup_arch(char **cmdline_p)
{
......
    max_pfn = e820_end_of_ram_pfn();
......
	if (max_pfn > (1UL<<(32 - PAGE_SHIFT)))
		max_low_pfn = e820_end_of_low_ram_pfn();
	else
		max_low_pfn = max_pfn;
...... 
}

max_pfn 表示系统物理内存最大页帧号,max_low_pfn 表示 32 位系统所能寻址的最大页帧号。

可以看到,在计算系统最大页帧 max_pfnmax_low_pfn 时,分别调用了 e820_end_of_ram_pfne820_end_of_low_ram_pfn 函数。而这两个函数内部,都调用了 e820_end_pfn 函数,只不过参数不同。

e820_end_of_ram_pfn 函数根据 e820 接口探测到的内存信息计算出系统最大的页帧号并保存到 max_pfn 中。如果 max_pfn 超过 32 位所能表示的最大页帧号,即物理内存超过 4GB,那么 max_low_pfn 就是 4GB 内存对应的最大页帧号;否则,max_low_pfnmax_pfn 是同一个值。

但是,对于 64 位系统来说,已经不存在 max_low_pfn 的限制了。所以 x86-64 系统下,这两个值是一致的(见 init_mem_mapping 函数)。

5.1.1.1 e820_end_of_ram_pfn
// file: arch/x86/kernel/e820.c
unsigned long __init e820_end_of_ram_pfn(void)
{
	return e820_end_pfn(MAX_ARCH_PFN, E820_RAM);
}
5.1.1.2 e820_end_of_low_ram_pfn
unsigned long __init e820_end_of_low_ram_pfn(void)
{
	return e820_end_pfn(1UL<<(32 - PAGE_SHIFT), E820_RAM);
}

e820_end_pfn 函数会在指定的内存类型中,搜索不超过最大限定值的页帧号。可以看到,这两个函数都是在 E820_RAM 内存类型中进行搜索。

我们在 《Linux Kernel:物理内存布局探测》一文中介绍过,E820 内存分为多种类型:

 // file: arch/x86/include/uapi/asm/e820.h
 #define E820_RAM    1
 #define E820_RESERVED   2
 #define E820_ACPI   3
 #define E820_NVS    4
 #define E820_UNUSABLE   5

其中,E820_RAM 表示可用内存类型。

另外,对于 e820_end_of_ram_pfn 函数,其限定值是 MAX_ARCH_PFN,指示内核支持的最大页帧号,所以该函数计算的是已探测到的所有可用内存的最大页帧号;对于 e820_end_of_low_ram_pfn 函数来说,其限定值为 32 位物理内存(即 4GB)的最大页帧号,所以该函数计算的是不超过 4GB 的可用内存的最大页帧号。

MAX_ARCH_PFN 表示系统的最大页帧号,通过将系统支持的最大物理内存右移 PAGE_SHIFT 位(扩展为 12)得到。其定义如下:

// file: arch/x86/kernel/e820.c
# define MAX_ARCH_PFN MAXMEM>>PAGE_SHIFT

V3.10 版本的 Linux 内核,支持最大 MAX_PHYSMEM_BITS(扩展为 46) 位的物理内存,而宏 MAXMEM 扩展为 46 位物理内存的最大地址,PAGE_SHIFT (扩展为 12)指示一个页所占用的位数。

// file: arch/x86/include/asm/pgtable_64_types.h
#define MAXMEM		 _AC(__AC(1, UL) << MAX_PHYSMEM_BITS, UL)

MAX_PHYSMEM_BITS 扩展为 46:

// file: arch/x86/include/asm/sparsemem.h
# define MAX_PHYSMEM_BITS	46

所以,宏 MAX_ARCH_PFN 扩展为 (1<<46)>>12,表示 46 位物理地址的最大页帧号。

5.1.1.3 e820_end_pfn

e820_end_pfn 函数在指定类型的内存中搜索不超过限制值的最大页帧号。该函数接收 2 个参数:

  • @limit_pfn:该值指定了搜索的页帧号上限,即内存的页帧号不能大于该值。
  • @type:E820 内存类型
// file: arch/x86/kernel/e820.c
/*
 * Find the highest page frame number we have available
 */
static unsigned long __init e820_end_pfn(unsigned long limit_pfn, unsigned type)
{
	int i;
    /*
     * last_pfn 用来保存找到的页帧号,初始化为 0
     */
	unsigned long last_pfn = 0;
    /*
     * max_arch_pfn 为架构最大页帧号 MAX_ARCH_PFN,MAX_ARCH_PFN 的计算见上文;
     * 该值表示页帧号上限,limit_pfn 不应该超过该值。
     */
	unsigned long max_arch_pfn = MAX_ARCH_PFN;

    /*
     * 遍历 E820 内存块(E820 内存的说明见 “1.4” 小节),在 type 类型的内存块中,找到不超过 limit_pfn 的最大页帧号
     */
	for (i = 0; i < e820.nr_map; i++) {
		struct e820entry *ei = &e820.map[i];
		unsigned long start_pfn;
		unsigned long end_pfn;
		/*
     	 * 跳过内存类型不符的内存块
     	 */
		if (ei->type != type)
			continue;
		
        /*
     	 * 计算内存块的起始页帧和结束页帧
     	 */
		start_pfn = ei->addr >> PAGE_SHIFT;
		end_pfn = (ei->addr + ei->size) >> PAGE_SHIFT;

        /*
     	 * 如果内存块的起始页帧比 limit_pfn 大,说明不符合要求,跳过
     	 */
		if (start_pfn >= limit_pfn)
			continue;
        
        /*
     	 * 如果内存块的起始页帧比 limit_pfn 小,而结束页帧比 limit_pfn 大
     	 * 说明要找的最大页帧就是 limit_pfn, 将 last_pfn 设置为 limit_pfn,然后跳出循环
     	 */
		if (end_pfn > limit_pfn) {
			last_pfn = limit_pfn;
			break;
		}
        
        /*
     	 * 如果内存块的起始页帧和结束页帧都比 limit_pfn 小,则更新 last_pfn 的值,
     	 * 其值为不超过 limit_pfn 的最大页帧号
     	 */
		if (end_pfn > last_pfn)
			last_pfn = end_pfn;
	}

    /*
     * 找到的页帧号不能超过系统的最大页帧 max_arch_pfn,
     * 如果超过了,将其修正为 max_arch_pfn
     */
	if (last_pfn > max_arch_pfn)
		last_pfn = max_arch_pfn;

	printk(KERN_INFO "e820: last_pfn = %#lx max_arch_pfn = %#lx\n",
			 last_pfn, max_arch_pfn);
	return last_pfn;
}

e820_end_pfn 函数会遍历 e820 中所有的内存块,即 e820.map 中的每个元素,从类型为 type 的内存块中找到不超过 limit_pfn 的最大页帧号。同时还要保证找到的页帧号不能超过系统支持最大页帧号 max_arch_pfn,如果超过了,则将其修正为系统最大页帧号。

5.2 probe_page_size_mask

probe_page_size_mask 函数探测系统可以使用的页大小。理论上,x86-64 处理器支持 4KB、2MB 和 1GB 大小的页。但正如我们在“1.2 分页大小” 小节中介绍的,并不是所有处理器都支持 1GB 的页。所以,使用该函数检测处理器实际支持的页大小。

// file: arch/x86/mm/init.c
static void __init probe_page_size_mask(void)
{

    init_gbpages();

#if !defined(CONFIG_DEBUG_PAGEALLOC) && !defined(CONFIG_KMEMCHECK)
	/*
	 * For CONFIG_DEBUG_PAGEALLOC, identity mapping will use small pages.
	 * This will simplify cpa(), which otherwise needs to support splitting
	 * large pages into small in interrupt context, etc.
	 */
	if (direct_gbpages)
		page_size_mask |= 1 << PG_LEVEL_1G;
	if (cpu_has_pse)
		page_size_mask |= 1 << PG_LEVEL_2M;
#endif

	/* Enable PSE if available */
	if (cpu_has_pse)
		set_in_cr4(X86_CR4_PSE);

	/* Enable PGE if available */
	if (cpu_has_pge) {
		set_in_cr4(X86_CR4_PGE);
		__supported_pte_mask |= _PAGE_GLOBAL;
	}
}

init_gbpages 函数检测处理器是否支持 1GB 大小的页,检测结果保存在变量 direct_gbpages 中。如果 direct_gbpages 为 1,则处理器支持 1GB 的页;否则,不支持。对于连续的物理内存来说,使用较大的页既能够减少页表项的数量,节省内存空间;也能够减少 TLB 缓存抖动, 提高性能。

接下来,如果内核选项 CONFIG_DEBUG_PAGEALLOC 以及 CONFIG_KMEMCHECK 均未设置,那么就允许使用较大的页;否则,只能使用 4KB 的页。如果允许使用大页,还要检测允许使用哪种大页,是 2MB 的页还是 1GB 的页。如果 direct_gbpages 为真,说明系统支持 1GB 的页,则将掩码 page_size_mask 中 1GB 页对应的比特位置位;如果处理器支持 PSE(Page Size Extensions )特征,说明处理器支持 2MB 的页,则将 page_size_mask 中对应的比特位置位。

page_size_mask是一个静态变量,其声明如下:

// file: arch/x86/mm/init.c
static int page_size_mask;

PG_LEVEL_1GPG_LEVEL_2M 是枚举类型 pg_level 的元素,分别对应着常量 3 和 2。

enum pg_level {
	PG_LEVEL_NONE,
	PG_LEVEL_4K,
	PG_LEVEL_2M,
	PG_LEVEL_1G,
	PG_LEVEL_NUM
};

cpu_has_psecpu_has_gbpages 分别用来指示处理器是否支持页大小扩展(Page Size Extensions )、 1GB 大小的页,其定义如下:

// file: arch/x86/include/asm/cpufeature.h
#define cpu_has_pse		boot_cpu_has(X86_FEATURE_PSE)
#define cpu_has_gbpages		boot_cpu_has(X86_FEATURE_GBPAGES)

这些能力是在进行处理器初始化时,通过 CPUID 指令探测并保存到全局变量 boot_cpu_datax86_capability 字段中。boot_cpu_datastruct cpuinfo_x86 的一个实例,用于存储启动处理器 BSP (Bootstrap Processor)的信息。

 // file: arch/x86/kernel/setup.c
 struct cpuinfo_x86 boot_cpu_data __read_mostly = {
     .x86_phys_bits = MAX_PHYSMEM_BITS,
 };

cpuinfo_x86x86_capability 字段是一个位图数组,保存着探测到的处理器的各种能力,每种能力对应着位图中的一个比特位。

struct cpuinfo_x86 {
......
    __u32			x86_capability[NCAPINTS + NBUGINTS];
......
}

通过 cat /proc/cpuinfo 指令,可以查看处理器支持的特征:

cpuinfo.jpg

可以看到,上述处理器支持 PSE (Page Size Extensions )特征,但并不支持 1GB 大小的页。

接下来,如果处理器支持 PSE 和 PGE(Page Global Enable)特征,则将控制寄存器 CR4 中对应的标志置位。

控制寄存器 CR4 格式如下:

CR4-2.png

CR4.PSE(Page Size Extensions ,CR4 中的位 4):

对于 32 位分页来说,如果 CR4.PSE 置位,则处理器支持 4MB 大小的页;对于 PAE 分页和 4 级分页来说,不论 CR4.PSE 是否置位,都支持 2MB 大小的页。

CR4.PGE(Page Global Enable ,CR4 中的位 7 ):

指示是否支持全局页特征。全局页允许将经常使用的页或共享的页标记为全局的(上层页目录项、中层页目录项或页表项中的位 8,指示页是否为全局的)。当任务切换或者向控制寄存器 CR3 中写入数据(通常会导致 TLB 刷出)时,全局页的 TLB 不会刷出。

5.2.1 init_gbpages

init_gbpages 函数检查系统是否支持 1GB 大小的页,检查结果保存到变量 direct_gbpages 中。

// file: arch/x86/mm/init.c
static void __init init_gbpages(void)
{
#ifdef CONFIG_X86_64
	if (direct_gbpages && cpu_has_gbpages)
		printk(KERN_INFO "Using GB pages for direct mapping\n");
	else
		direct_gbpages = 0;
#endif
}

direct_gbpages 是一个全局变量,如果设置了内核配置选项 CONFIG_DIRECT_GBPAGES,则 direct_gbpages 默认为 1;否则,由于其全局变量的特性,默认值为 0。

// file: arch/x86/mm/init.c
int direct_gbpages
#ifdef CONFIG_DIRECT_GBPAGES
				= 1
#endif
;

如果配置了内核选项 CONFIG_DIRECT_GBPAGES 且处理器支持 1GB大小的页(cpu_has_gbpages 为真),则 direct_gbpages 的值为 1;否则,为 0。

5.3 init_memory_mapping

init_memory_mapping 函数是构建直接映射区的核心函数,主要的映射过程都是在该函数中完成的。

该函数将指定的物理内存区域映射到直接映射区,它接收 2 个参数:

  • @start:物理内存的起始地址
  • @end:物理内存的结束地址

返回映射的最大页帧号。

// file: arch/x86/mm/init.c
/*
 * Setup the direct mapping of the physical memory at PAGE_OFFSET.
 * This runs before bootmem is initialized and gets pages directly from
 * the physical memory. To access them they are temporarily mapped.
 */
unsigned long __init_refok init_memory_mapping(unsigned long start,
					       unsigned long end)
{
	struct map_range mr[NR_RANGE_MR];
	unsigned long ret = 0;
	int nr_range, i;

	pr_info("init_memory_mapping: [mem %#010lx-%#010lx]\n",
	       start, end - 1);

	memset(mr, 0, sizeof(mr));
	nr_range = split_mem_range(mr, 0, start, end);

	for (i = 0; i < nr_range; i++)
		ret = kernel_physical_mapping_init(mr[i].start, mr[i].end,
						   mr[i].page_size_mask);

	add_pfn_range_mapped(start >> PAGE_SHIFT, ret >> PAGE_SHIFT);

	return ret >> PAGE_SHIFT;
}

变量 mrstruct map_range 类型的数组,该数组包含 NR_RANGE_MR(扩展为 5) 个成员:

// file: arch/x86/mm/init.c
#define NR_RANGE_MR 5

结构体 struct map_range 用来表示映射范围:

// file: arch/x86/mm/init.c
struct map_range {
	unsigned long start;
	unsigned long end;
	unsigned page_size_mask;
};

该结构包含 3 个字段:起始物理地址 start;结束物理地址 end;系统支持的页尺寸掩码 page_size_mask

在函数一开始,通过 memset 函数,使用 0 来填充 mr 变量。

然后,通过 split_mem_range 函数将内存区域根据可映射的页大小切分成不同的区间。比如有的区间可使用 1GB 大小的页,有的区间只能使用 2MB 或 4KB 大小的页。不同页大小的区间必须切分出来,以便在构建页表时能够明确区分。切分后的内存区间,保存在变量 mr 中。

kernel_physical_mapping_init 函数将指定的物理内存区间,映射到直接映射区,返回映射的最大物理地址。映射时,每个区域映射的页大小,参考 map_range 中的页大小掩码 page_size_mask

映射完成后,调用 add_pfn_range_mapped 函数将已映射内存区间的页帧号保存起来。

5.3.1 split_mem_range

在讲解 split_mem_range 的代码实现之前,我们先来介绍下区域划分的原则。在上文介绍过,变量 mr 是一个包含 5 个成员的数组,每个成员表示一段不同页大小的内存区域。 split_mem_range 函数会将待映射的物理内存区间根据可映射的页大小进行分割,并将分割后的起始、结束地址以及页大小掩码保存到 mr 中。划分的原则就是尽量使用的更大的页来进行映射。

至于为什么 mr 的成员数量为 5?因为一段内存,根据映射的页大小不同,最多可被分成下图所示的 5 个区间: split_mem_range-1.png

如果系统不支持 1GB 的页,一段内存最多可以分成 3 个区间:

split_mem_range-2.png

如果内存的起始和结束地址都对齐到 2MB, 那么两端的 4KB 映射区就不存在了,整个区间都映射成 2MB 的页

所以,mr 数组有 5 个成员完全够用了。

split_mem_range 函数分为三个阶段:

  1. 区域分割

    根据将内存区域根据可映射的页大小切分为不同的内存区间,将切分后内存区间的起始、结束地址以及页大小掩码保存到数组 mr 中,最多分割成 5 个区间。

  2. 调整 mr 数组中内存区间的页大小掩码

    这是通过 adjust_range_page_size_mask 函数完成的。注意,调整的是区域的页大小掩码,即 map_range->page_size_mask,起始和结束地址没有变化。 调整的目的是尽量使用较大页来进行映射,比如检查 4KB 映射区是否能够调整为 2MB 或 1GB 的页;2MB 映射区能否调整为 1GB 的页。

    调整过程详见 adjust_range_page_size_mask 函数。

  3. 区间合并

​ 区域的页掩码调整后,有些区域就能够合并了。比如原先的 4KB 映射区,调整后变成了 2MB 映射区,如果跟后一段内存区间地址相邻且两段内存的 page_size_mask 相同,那么这两段内存区间就可以合并成一个区间。

区间合并的原则如下:

  • 两个内存区间在地址上必须是相邻的,即前一个区间的结束地址必须等于下一个区间的起始地址
  • 两个内存区间的页大小掩码 page_size_mask 必须一致

split_mem_range 函数定义如下:

// file: arch/x86/mm/init.c
static int __meminit split_mem_range(struct map_range *mr, int nr_range,
				     unsigned long start,
				     unsigned long end)
{
	unsigned long start_pfn, end_pfn, limit_pfn;
	unsigned long pfn;
	int i;

    /* 
     * 计算结束物理地址 end 对应的页帧 
     * #define PFN_DOWN(x)	((x) >> PAGE_SHIFT)
     */
	limit_pfn = PFN_DOWN(end);

	/* head if not big page alignment ? */
	/* 计算起始物理地址 start 对应的页帧 */
	pfn = start_pfn = PFN_DOWN(start);
#ifdef CONFIG_X86_32
	......
#else /* CONFIG_X86_64 */
    /* 
     * 此处,end_pfn 表示从起始地址开始的 4KB 页映射区的结束页帧,同时也是 2MB 页映射区的起始页帧 
     * 宏 PMD_SIZE 表示中层页目录的大小,扩展为 2MB
     * 通过 round_up 函数将起始页帧 pfn 对齐到 2MB 后,作为第一阶段 4KB 映射区上边界
     */
	end_pfn = round_up(pfn, PFN_DOWN(PMD_SIZE));
#endif
    /* 
     * 对 end_pfn 进行修正,确保该值不能大于结束地址 end 对应的页帧 limit_pfn 
     */
	if (end_pfn > limit_pfn)
		end_pfn = limit_pfn;
    /* 
     * 如果 start_pfn == end_pfn,说明 start_pfn 本身是对齐到 2MB 的,不需要映射 4KB 的页;否则,需要映射 4KB 的页
     * 调用 save_mr 函数,将 start_pfn 到 end_pfn 这段页帧对应的地址范围写入以 nr_range 为下标的 mr 数组成员中
     * save_mr 函数负责将映射区间写入 mr 数组中,该函数的最后一个参数表示页大小的掩码,由于当前的地址区间映射的是 4KB 的页,所以掩码值为 0
     * save_mr 函数返回下一个可用的 mr 数组成员下标
     * mr 保存成功后,将当前的 end_pfn 赋值给 pfn,作为 2MB 页映射的起始页帧
     * start_pfn < end_pfn,说明需要映射 4KB 的页
     */
	if (start_pfn < end_pfn) {
		nr_range = save_mr(mr, nr_range, start_pfn, end_pfn, 0);
		pfn = end_pfn;
	}

	/* big page (2M) range */
    /* 
     * 接下来,准备设置 2MB 的页的地址区间
     * 通过 round_up 函数将起始页帧 pfn 对齐到 2MB
     */
	start_pfn = round_up(pfn, PFN_DOWN(PMD_SIZE));
#ifdef CONFIG_X86_32
	......
#else /* CONFIG_X86_64 */
    /* 
     * 计算 2MB 页的结束地址页帧 end_pfn,即通过 round_up 函数将起始页帧 pfn 向上对齐到 1GB
     * PUD_SIZE 表示每个上层页目录项的控制范围,每个上层页目录项控制着 30 位地址空间,即 1GB 大小的内存
     * 因为映射的是 2MB 的页,end_pfn 不应该大于 limit_pfn 向下对齐到 2MB 后的页帧,不足 2MB 的区间需要映射更细粒度(4KB)的页
     */
	end_pfn = round_up(pfn, PFN_DOWN(PUD_SIZE));
	if (end_pfn > round_down(limit_pfn, PFN_DOWN(PMD_SIZE)))
		end_pfn = round_down(limit_pfn, PFN_DOWN(PMD_SIZE));
#endif
    /* 
     * 如果 start_pfn == end_pfn,说明 start_pfn 本身是对齐到 1GB 的,不需要映射 2MB 的页;否则,需要映射 2MB 的页
     * 调用 save_mr 函数,将 start_pfn 到 end_pfn 这段页帧对应的地址范围写入以 nr_range 为下标的 mr 数组成员中
     * save_mr 函数负责将数据写入到 mr 数组中,该函数的最后一个参数表示页大小的掩码,
     * 由于当前的地址范围映射的是 2MB 的页,所以掩码中包含 PG_LEVEL_2M 对应的掩码位,表示这段内存支持 4KB、2MB 两种页大小
     * save_mr 函数返回下一个可用的 mr 数组成员下标
     * mr 保存成功后,将当前的 end_pfn 赋值给 pfn,作为 1GB 页映射的起始页帧
     * start_pfn < end_pfn 时,说明需要映射 2MB 页
     */
	if (start_pfn < end_pfn) {
		nr_range = save_mr(mr, nr_range, start_pfn, end_pfn,
				page_size_mask & (1<<PG_LEVEL_2M));
		pfn = end_pfn;
	}

#ifdef CONFIG_X86_64
	/* big page (1G) range */
    /* 
     * 将 start_pfn 和 end_pfn 都对齐到 1GB 
     * 如果 start_pfn == end_pfn,说明地址空间不足 1GB,不能映射 1GB 的页;否则,需要映射 1GB 的页
     * 调用 save_mr 函数,将 start_pfn 到 end_pfn 这段页帧对应的地址范围写入以 nr_range 为下标的 mr 数组成员中
     * save_mr 函数负责将数据写入到 mr 数组中,该函数的最后一个参数表示页大小的掩码,
     * 由于当前的地址范围映射的是 1GB 的页,所以掩码中包括 PG_LEVEL_1G 和 PG_LEVEL_2M 对应的掩码位,表示这段内存支持 4KB、2MB、1GB 三种页大小
     * save_mr 函数返回下一个可用的 mr 数组成员下标
     * mr 保存成功后,将当前的 end_pfn 赋值给 pfn,作为下一阶段 2MB 页映射的起始页帧
     * 当 start_pfn < end_pfn 时,说明能够映射 1GB 页
     */
	start_pfn = round_up(pfn, PFN_DOWN(PUD_SIZE));
	end_pfn = round_down(limit_pfn, PFN_DOWN(PUD_SIZE));
	if (start_pfn < end_pfn) {
		nr_range = save_mr(mr, nr_range, start_pfn, end_pfn,
				page_size_mask &
				 ((1<<PG_LEVEL_2M)|(1<<PG_LEVEL_1G)));
		pfn = end_pfn;
	}

	/* tail is not big page (1G) alignment */
    /* 
     * 内存区域尾部不足 1GB 的部分,优先选择映射 2MB 的页
     * 将 pfn 向上对齐到 2MB,保存为 start_pfn;limit_pfn 向下对齐到 2MB,保存为 end_pfn
     * 如果 start_pfn == end_pfn,说明地址空间不足 2MB,不能映射 2MB 的页,只能映射 4KB 的页;否则,需要映射 2MB 的页
     * save_mr 函数负责将数据写入到 mr 数组中,该函数的最后一个参数表示页大小的掩码,
     * 由于当前的地址范围映射的是 2MB 的页,所以掩码中包含 PG_LEVEL_2M 对应的掩码位,表示这段内存支持 4KB、2MB 两种页大小
     * mr 保存成功后,将当前的 end_pfn 赋值给 pfn,作为下一阶段 4KB 页映射的起始页帧
     */
	start_pfn = round_up(pfn, PFN_DOWN(PMD_SIZE));
	end_pfn = round_down(limit_pfn, PFN_DOWN(PMD_SIZE));
	if (start_pfn < end_pfn) {
		nr_range = save_mr(mr, nr_range, start_pfn, end_pfn,
				page_size_mask & (1<<PG_LEVEL_2M));
		pfn = end_pfn;
	}
#endif

	/* tail is not big page (2M) alignment */
    /* 
     * 内存区域尾部不足 2MB 的部分,只能映射 4KB 的页
     */
	start_pfn = pfn;
	end_pfn = limit_pfn;
	nr_range = save_mr(mr, nr_range, start_pfn, end_pfn, 0);
    
    /* 
     * after_bootmem 是一个 int 类型的全局变量(默认值为 0),定义在文件 arch/x86/mm/init.c 中,指示程序是否处于后 bootmem 时代
     * 该变量在 mem_init 函数中被设置为 1,此时已经通过 free_all_bootmem 函数将早期内存管理器的内存释放给了伙伴系统
     * mem_init 函数的调用链:start_kernel() -> mm_init() -> mem_init()
     * 我们此时的位置在 start_kernel() -> setup_arch() -> initmem_init(),由于 setup_arch() 函数会先于 mm_init() 执行,
     * 所以此时 after_bootmem 的值为 0,会执行 adjust_range_page_size_mask 函数
     * 
     * 通过 adjust_range_page_size_mask 函数调整不同内存区域的分页大小
     */
	if (!after_bootmem)
		adjust_range_page_size_mask(mr, nr_range);

	/* try to merge same page size and continuous */
    /* 
     * 调整后,如果两个相邻的内存区间能够合并,则合并它们 
     * 合并的原则有 2 个:
     * 1、两个内存区间在地址上必须是相邻的,即前一个区域的结束地址必须等于下一个区的起始地址
     * 2、两个内存区间的页大小掩码必须一致
     * 如果能够合并,则将下一个内存区间合并到前一个中,然后通过 mommove 函数之后的成员整体向前移动一个位置,并将 nr_range 减一。
     */
	for (i = 0; nr_range > 1 && i < nr_range - 1; i++) {
		unsigned long old_start;
		if (mr[i].end != mr[i+1].start ||
		    mr[i].page_size_mask != mr[i+1].page_size_mask)
			continue;
		/* move it */
		old_start = mr[i].start;
		memmove(&mr[i], &mr[i+1],
			(nr_range - 1 - i) * sizeof(struct map_range));
		mr[i--].start = old_start;
		nr_range--;
	}
	
    /*  
     * 最后,打印出每个内存区间的信息:
     * 1、起始地址
     * 2、结束地址
     * 3、页大小: 1G、2M 或者 4K
     */
	for (i = 0; i < nr_range; i++)
		printk(KERN_DEBUG " [mem %#010lx-%#010lx] page %s\n",
				mr[i].start, mr[i].end - 1,
			(mr[i].page_size_mask & (1<<PG_LEVEL_1G))?"1G":(
			 (mr[i].page_size_mask & (1<<PG_LEVEL_2M))?"2M":"4k"));

	return nr_range;
}
5.3.1.1 PFN_DOWN

PFN_DOWN 用于获取指定物理地址的页帧号。

// file: include/linux/pfn.h
#define PFN_DOWN(x)	((x) >> PAGE_SHIFT)
5.3.1.2 round_up、round_down

round_upround_down 分别用于向上和向下圆整。

// file: include/linux/kernel.h
#define __round_mask(x, y) ((__typeof__(x))((y)-1))         // y - 1
#define round_up(x, y) ((((x)-1) | __round_mask(x, y))+1)	// ((x-1) | (y-1)) + 1
#define round_down(x, y) ((x) & ~__round_mask(x, y))		// x & (~(y-1))
5.3.1.3 save_mr

save_mr 函数将 start_pfnend_pfn 这段地址区间写入以 nr_range 为下标的 mr 数组成员中。函数的最后一个参数 page_size_mask 表示这段内存区间可映射的页大小的掩码。由于 4KB 页是默认支持的,所以不管 page_size_maskPG_LEVEL_4K 对应的比特位是否置位,4KB 大小的页总是可用的。另外,页大小具有向下兼容性,如果 1GB 页可用,那么 2MB 和 4KB 的页也是可用的;如果 2MB 页可用,那么 4KB 页也是可用的。

该函数返回下一个可用的 mr 数组下标。

// file: arch/x86/mm/init.c
static int __meminit save_mr(struct map_range *mr, int nr_range,
			     unsigned long start_pfn, unsigned long end_pfn,
			     unsigned long page_size_mask)
{
	if (start_pfn < end_pfn) {
		if (nr_range >= NR_RANGE_MR)
			panic("run out of range for init_memory_mapping\n");
		mr[nr_range].start = start_pfn<<PAGE_SHIFT;
		mr[nr_range].end   = end_pfn<<PAGE_SHIFT;
		mr[nr_range].page_size_mask = page_size_mask;
		nr_range++;
	}

	return nr_range;
}

其中,宏 NR_RANGE_MR 扩展为 5,宏 PAGE_SHIFT 扩展为 12。

5.1.3.4 adjust_range_page_size_mask

adjust_range_page_size_mask 函数用于调整待映射内存区域的页大小掩码。也就是说,看看原先只能映射 4KB 页的内存区域能不能映射成 2MB 页 或 1GB 页;原先只能映射 2MB 页的内存区域,能不能映射成 1GB 页。

调整的原则是什么呢?

首先当前系统要支持 2MB 或 1GB 页。这个是通过全局变量 page_size_mask 来判断的。我们在 probe_page_size_mask 函数中已经探测到了系统所支持的各种分页大小,并将它们保存到全局页大小掩码 page_size_mask 中。

另外,如果要将页大小提升到 2MB 或 1GB,那么内存区间的起始地址和结束地址就必须对齐到 2MB 或 1GB 。对齐后的内存区间就可能比原内存区间要大,相当于对原内存区间进行了扩展。这就要求扩展后的内存区间必须被可用内存类型( memory 类型)的某个内存块(memblock_region)完全覆盖,换句话说,不能扩展到内存空洞里去,这是通过 memblock_is_region_memory 函数进行检查的。如果检查通过,memblock_is_region_memory 函数会返回非 0 值;否则,返回 0。

// file: arch/x86/mm/init.c
/*
 * adjust the page_size_mask for small range to go with
 *	big page size instead small one if nearby are ram too.
 */
static void __init_refok adjust_range_page_size_mask(struct map_range *mr,
							 int nr_range)
{
	int i;

    /* 遍历 mr 数组所有成员,检查内存区域是否能够调整分页大小 */
	for (i = 0; i < nr_range; i++) {
        /*
         * 如果系统支持 2MB 页,但当前内存区域未设置 2MB 页掩码(内存区域大小不足 2MB),检查是否能够将该内存区域的页大小调整到 2MB
         * 首先,将内存区域的起始(向下调整)和结束地址(向上调整)对齐到 2MB
         * 如果调整后的内存区域能够被 memory 类型的某个内存块完全覆盖(扩展后不包含内存空洞),则该内存区域的可以映射为 2MB 页
         */
		if ((page_size_mask & (1<<PG_LEVEL_2M)) &&
		    !(mr[i].page_size_mask & (1<<PG_LEVEL_2M))) {
			unsigned long start = round_down(mr[i].start, PMD_SIZE);
			unsigned long end = round_up(mr[i].end, PMD_SIZE);

#ifdef CONFIG_X86_32
			......
#endif

			if (memblock_is_region_memory(start, end - start))
				mr[i].page_size_mask |= 1<<PG_LEVEL_2M;
		}
        /*
         * 如果系统支持 1GB 页,但当前内存区域未设置 1GB 页掩码(区域大小不足 1GB),检查是否能够将该内存区域的页大小调整到 1GB
         * 首先,将内存区域的起始地址(向下调整)和结束地址(向上调整)对齐到 1GB
         * 如果,调整后的内存区域能够被 memory 类型的某个内存块完全覆盖(扩展后不包含内存空洞),则该内存区域可以映射为 1GB 页
         */
		if ((page_size_mask & (1<<PG_LEVEL_1G)) &&
		    !(mr[i].page_size_mask & (1<<PG_LEVEL_1G))) {
			unsigned long start = round_down(mr[i].start, PUD_SIZE);
			unsigned long end = round_up(mr[i].end, PUD_SIZE);

			if (memblock_is_region_memory(start, end - start))
				mr[i].page_size_mask |= 1<<PG_LEVEL_1G;
		}
	}
}
5.3.1.5 memblock_is_region_memory

memblock_is_region_memory 函数检查指定内存区域是否有效的 memory 类型内存块。如果检查失败,返回 0;否则,返回非 0 值。

该函数接收 2 个参数:

  • @base:物理内存的基地址
  • @size:内存大小
// file: mm/memblock.c
/**
 * memblock_is_region_memory - check if a region is a subset of memory
 * @base: base of region to check
 * @size: size of region to check
 *
 * Check if the region [@base, @base+@size) is a subset of a memory block.
 *
 * RETURNS:
 * 0 if false, non-zero if true
 */
int __init_memblock memblock_is_region_memory(phys_addr_t base, phys_addr_t size)
{
	int idx = memblock_search(&memblock.memory, base);
	phys_addr_t end = base + memblock_cap_size(base, &size);

	if (idx == -1)
		return 0;
	return memblock.memory.regions[idx].base <= base &&
		(memblock.memory.regions[idx].base +
		 memblock.memory.regions[idx].size) >= end;
}

函数内部,首先通过 memblock_search 函数查找内存地址 base 所在的 memory 类型的内存块索引。如果找到,memblock_search 函数返回内存块的索引;否则,返回 -1。

然后,计算内存的结束地址 end。为了防止 end 溢出,使用 memblock_cap_size 函数修正 *size 的值,修正后 base + *size 的值不超过最大地址 ULLONG_MAX

// file: include/linux/kernel.h
#define ULLONG_MAX	(~0ULL)

如果 idx == -1,说明地址 base 不属于任何内存块,查找失败,返回 0。

否则,需要检查搜索到的内存块是否能够覆盖整个内存区间,如果能够完全覆盖,返回 1;否则,返回 0。

5.3.1.6 memblock_search

memblock_search 函数的功能就是在指定的内存类型中,查找物理地址 addr 所在的内存块。函数内部使用了二分查找法,如果指定的物理地址在某个内存块范围内,则返回该内存块的索引;否则,返回 -1。

// file: mm/memblock.c
static int __init_memblock memblock_search(struct memblock_type *type, phys_addr_t addr)
{
	unsigned int left = 0, right = type->cnt;

	do {
		unsigned int mid = (right + left) / 2;

		if (addr < type->regions[mid].base)
			right = mid;
		else if (addr >= (type->regions[mid].base +
				  type->regions[mid].size))
			left = mid + 1;
		else
			return mid;
	} while (left < right);
	return -1;
}
5.3.1.7 memblock_cap_size

memblock_cap_size 函数接收 2 个参数:物理基地址 base 以及内存大小 size

该函数的功能,注释里已经说的很明白了,就是调整 *size 的大小以防止 (base + *size) 的值溢出,即防止内存区间超出最大地址 ULLONG_MAX

// file: mm/memblock.c
/* adjust *@size so that (@base + *@size) doesn't overflow, return new size */
static inline phys_addr_t memblock_cap_size(phys_addr_t base, phys_addr_t *size)
{
	return *size = min(*size, (phys_addr_t)ULLONG_MAX - base);
}

5.3.2 kernel_physical_mapping_init

kernel_physical_mapping_init 函数接收 3 个参数:

  • @start:物理起始地址
  • @end:物理结束地址
  • @page_size_mask:该区域可映射的页大小掩码

该函数为指定范围的物理内存建立页表,将其映射到直接映射区,页大小通过掩码 page_size_mask 指定。

注:该函数会涉及到大量分页相关内容,分页相关请参考《Linux Kernel:内存管理之分页(Paging)》。

// file: arch/x86/mm/init_64.c
unsigned long __meminit
kernel_physical_mapping_init(unsigned long start,
			     unsigned long end,
			     unsigned long page_size_mask)
{
	bool pgd_changed = false;
	unsigned long next, last_map_addr = end;
	unsigned long addr;

    /*
     * 将物理内存地址转换成虚拟地址
     */
	start = (unsigned long)__va(start);
	end = (unsigned long)__va(end);
	addr = start;

    /*
     * 通过一个循环为 start 到 end 之间的虚拟地址建立页表
     * 由于每个全局页目录项控制 PGDIR_SIZE(扩展为 512GB)大小的内存,所以每次迭代后,起始地址增加 PGDIR_SIZE
     */
	for (; start < end; start = next) {
        /*
         * pgd_offset_k 宏获取虚拟地址对应的全局页目录项地址
         * pgd_offset_k 是 pgd_offset 的简化形式,用于获取内核空间的全局页目录项虚拟地址
         * pgd_offset_k 的实现请参考 《Linux Kernel:内存管理之分页(Paging)》
         */
		pgd_t *pgd = pgd_offset_k(start);
		pud_t *pud;
        
        /*
         * next 是下一轮要映射内存区间的起始地址
         * 每个全局页目录项控制着 39 位的内存空间,所以:
         * 1、PGDIR_SIZE 表示每个全局页目录项控制的内存大小,扩展为 512GB,即 2 的 39 次方
         * 2、PGDIR_MASK 是全局页目录掩码,该掩码会屏蔽掉低 39 位(将低 39 位置 0)
         */
		next = (start & PGDIR_MASK) + PGDIR_SIZE;

        /*
         * pgd_val 用于获取全局页目录项的值
         * 如果全局页目录项的值为真,说明该全局页目录项指向的上层页目录是存在的,
         * 通过 pgd_page_vaddr 可以获取到全局页目录项指向的上层页目录的虚拟地址,
         * 然后通过 phys_pud_init 函数对上层页目录进行初始化
         * phys_pud_init 函数会参考页大小掩码 page_size_mask 来选择映射的页大小,该函数返回最后映射的地址
         */
		if (pgd_val(*pgd)) {
			pud = (pud_t *)pgd_page_vaddr(*pgd);
			last_map_addr = phys_pud_init(pud, __pa(start),
						 __pa(end), page_size_mask);
			continue;
		}

        /*
         * 如果全局页目录项的值为 0,说明该全局页目录项未使用过,其对应的上层页目录肯定是不存在的
         * 所以先通过 alloc_low_page 函数分配一个页,作为上层页目录
         * 然后通过 phys_pud_init 函数对上层页目录进行初始化,返回最后映射的地址
         * phys_pud_init 函数会参考页大小掩码 page_size_mask 来选择映射的页大小
         * phys_pud_init 函数最多填充 512 个上层页目录项
         * 
         * 宏 __pa 将虚拟地址转换成物理地址
         */
		pud = alloc_low_page();
		last_map_addr = phys_pud_init(pud, __pa(start), __pa(end),
						 page_size_mask);

        /*
         * 在自旋锁的保护下,通过 pgd_populate 函数,填充全局页目录项
         * pgd_populate 函数会获取上层页目录的物理地址,将其与页标志组装成符合全局页目录项的格式,然后写入全局页目录项中
         */
		spin_lock(&init_mm.page_table_lock);
		pgd_populate(&init_mm, pgd, pud);
		spin_unlock(&init_mm.page_table_lock);
        
        /*
         * 我们在 pgd_populate 函数中,写入了新的全局页目录项,全局页目录被修改了,所以 pgd_changed 被设置为 true
         */
		pgd_changed = true;
	}

    /*
     * 如果我们修改了全局页目录项,则需要把新的内核空间的全局页目项同步到所有用户进程的全局页目录中
     * 同步过程是通过 sync_global_pgds 函数执行的,该函数的实现见 “5.3.2.8” 小节
     */
	if (pgd_changed)
		sync_global_pgds(addr, end - 1);

    /*
     * 页表修改后,需要将所有的 TLB 失效
     */
	__flush_tlb_all();

    /*
     * 返回映射的最大地址
     */
	return last_map_addr;
}
5.3.2.1 phys_pud_init

phys_pud_init 函数的作用是为指定的物理内存区间填充对应的上层页目录项,由于每个上层页目录有 512 个目录项,所以在该函数中最多填充 512 个上层页目录项。

// file: arch/x86/mm/init_64.c
static unsigned long __meminit
phys_pud_init(pud_t *pud_page, unsigned long addr, unsigned long end,
			 unsigned long page_size_mask)
{
    /*
     * pages 保存映射的 1GB 页的数量
     * last_map_addr 保存映射的最大地址
     * i 用作循环变量,初始化为起始地址 addr 对应的上层页目录项索引
     * pud_index 获取虚拟地址对应的上层页目录(Page Upper Directory,PUD)项索引
     */
	unsigned long pages = 0, next;
	unsigned long last_map_addr = end;
	int i = pud_index(addr);

     /*
     * 通过一个循环为来初始化上层页目录项
     * 每个上层页目录包含 PTRS_PER_PUD(扩展为 512)个上层页目录项,所以 i 必须小于 PTRS_PER_PUD
     * 由于每个上层页目录项控制 PUD_SIZE(扩展为 1GB)大小的内存区域,所以每次循环后 addr 增加 PUD_SIZE
     */
	for (; i < PTRS_PER_PUD; i++, addr = next) {
        /*
         * pud_page 是上层页目录的基地址,pud_index(addr) 计算出 addr 对应的上层页目录项索引
         * 两者相加,就得到 addr 对应的上层页目录项指针
         * PAGE_KERNEL 是页面分配标志的组合,在分配新页面时使用
         */
		pud_t *pud = pud_page + pud_index(addr);
		pmd_t *pmd;
		pgprot_t prot = PAGE_KERNEL;

        /*
         * next 计算出下一次迭代时,要映射的起始地址
         * 在迭代过程中,如果 addr >= end,说明此时映射的地址超出了指定的区间
         * 那么超出地址区间的上层页目项如何处理呢?
         * 如果处于系统初始化阶段,伙伴系统还未就绪并且从 addr 到 next 之间的内存是空洞,那么就将该上层页目项设置为 0;否则,忽略掉,什么也不做。
         * E820_RAM 和 E820_RESERVED_KERN 都是可用内存类型,e820_any_mapped 函数检查指定范围的内存与指定类型的内存块之间是否有交集。如果从 addr(向下对齐到 PUD_SIZE)到 next 之间的内存,与可用内存块之间没有任何交集,说明这整段内存都是空洞,将上层页目录项设置为 0。
         *
         * 宏 __pud 用于生成上层页目录项
         */
		next = (addr & PUD_MASK) + PUD_SIZE;
		if (addr >= end) {
			if (!after_bootmem &&
			    !e820_any_mapped(addr & PUD_MASK, next, E820_RAM) &&
			    !e820_any_mapped(addr & PUD_MASK, next, E820_RESERVED_KERN))
				set_pud(pud, __pud(0));
			continue;
		}

        /*
         * pud_val 用于获取上层页目录项的值,如果该值为真,说明地址 addr 对应的上层页目录项已经创建了
         */
		if (pud_val(*pud)) {
            /*
             * pud_large 用于检测上层页目录项是否直接映射到 1GB 的页。如果是,返回 true;否则,返回 false。
             * 上层页目录项和中层页目录项的位 7(PS 位)指示该页表项是直接映射到页还是指向下一级页表。
             * 对于上层页目录项来说,如果 PS 位置位,说明该表项直接映射 1GB 的页
             * 对于中层页目录项来说,如果 PS 位置位,说明该表项直接映射到 2MB 的页
             * 这里处理的是没有直接映射到 1GB 页的情况
             */
			if (!pud_large(*pud)) {
                /*
                 * 获取到第 0 个中层页目录项的地址(同时也是中层页目录的基地址)并赋值给 pmd
                 * 然后通过 phys_pmd_init 函数填充该中层页目录,填充时会参考页大小掩码 page_size_mask
                 * 填充后,通过 __flush_tlb_all 函数使所有 TLB 缓存失效
                 */
				pmd = pmd_offset(pud, 0);
				last_map_addr = phys_pmd_init(pmd, addr, end,
							 page_size_mask, prot);
				__flush_tlb_all();
				continue;
			}
			/*
			 * If we are ok with PG_LEVEL_1G mapping, then we will
			 * use the existing mapping.
			 *
			 * Otherwise, we will split the gbpage mapping but use
			 * the same existing protection  bits except for large
			 * page, so that we don't violate Intel's TLB
			 * Application note (317080) which says, while changing
			 * the page sizes, new and old translations should
			 * not differ with respect to page frame and
			 * attributes.
			 */
			/*
			 * 程序来到这里,说明当前的上层页目录项已经映射到 1GB 内存页
			 * 如果页大小掩码 page_size_mask 中包含 1GB 页的掩码位,
			 * 那么我们直接利用现有的上层页目录项就可以了
			 * 此时,需要更新映射的页数 pages 及 last_map_addr 的值
			 * 
			 */
			if (page_size_mask & (1 << PG_LEVEL_1G)) {
				if (!after_bootmem)
					pages++;
				last_map_addr = next;
				continue;
			}
            /*
             * 如果当前上层页目录项本身已经映射到 1GB 的页,而 page_size_mask 中未包含 1GB 页的比特位,
             * 说明要求映射成更细粒度的页,那么就需要对当前 1GB 页进行拆分。
             * 既然需要拆分,那么就要清除当前上层页目录项中的 PS 位(位 7)
             * pte_clrhuge 函数负责清除上层页目录项中的 PS 位
             * pte_pgprot 获取清除巨页标志后的页属性,并赋值给 prot
             */
			prot = pte_pgprot(pte_clrhuge(*(pte_t *)pud));
		}

        /*
         * 这里处理上层页目录项为 0(说明没映射过)但要求映射 1GB 页的情况
         * 先将映射的页数量 pages 加 1,然后将 addr 对应的物理地址及页标志组合成页表项格式后,通过 set_pte 函数写入上层页目录项中
         * 再更新 last_map_addr 的值
         */
		if (page_size_mask & (1<<PG_LEVEL_1G)) {
			pages++;
			spin_lock(&init_mm.page_table_lock);
			set_pte((pte_t *)pud,
				pfn_pte((addr & PUD_MASK) >> PAGE_SHIFT,
					PAGE_KERNEL_LARGE));
			spin_unlock(&init_mm.page_table_lock);
			last_map_addr = next;
			continue;
		}

        /*
         * 程序来到这里,肯定是不能映射 1GB 的页了
         * 不管是原先映射到 1GB 的页,还是原先没有映射,都需要重新建立页表
         * 重新建立页表,就需要为中层页目录分配一页内存,分配工作是通过 alloc_low_page 函数完成的
         * 有了中层页目录,通过 phys_pmd_init 函数填充中层页目录项,
         * 然后,在自旋锁的保护下,调用 pud_populate 函数将中层页目录项的的物理地址以及页标志组合成上层页目录项写入上层页目录中
         */
		pmd = alloc_low_page();
		last_map_addr = phys_pmd_init(pmd, addr, end, page_size_mask,
					      prot);

		spin_lock(&init_mm.page_table_lock);
		pud_populate(&init_mm, pud, pmd);
		spin_unlock(&init_mm.page_table_lock);
	}
    
    /*
     * 修改页表后,需要刷新全部的 TLB 缓存
     * __flush_tlb_all 函数使所有 TLB 缓存失效
     */
	__flush_tlb_all();

    /*
     * 更新 1GB 页的数量,用于信息统计
     */
	update_page_count(PG_LEVEL_1G, pages);

	return last_map_addr;
}
5.3.2.2 phys_pmd_init

phys_pmd_init 函数对中层页目录进行初始化,即填充中层页目录项。

// file: arch/x86/mm/init_64.c
static unsigned long __meminit
phys_pmd_init(pmd_t *pmd_page, unsigned long address, unsigned long end,
	      unsigned long page_size_mask, pgprot_t prot)
{
    /*
     * pages 保存映射的 2MB 页的数量
     * last_map_addr 保存映射的最大地址
     * i 用作循环变量,初始化为起始地址 address 对应的中层页目录项索引
     * pmd_index 获取虚拟地址对应的中层页目录项(Page Middle Directory Entry,PMDE)索引
     */
	unsigned long pages = 0, next;
	unsigned long last_map_addr = end;

	int i = pmd_index(address);

     /*
     * 通过一个循环来初始化中层页目录项
     * 每个中层页目录包含 PTRS_PER_PMD(扩展为 512)个中层页目录项,所以 i 必须小于 PTRS_PER_PMD 次
     * 由于每个中层页目录项控制 PMD_SIZE(扩展为 2MB)大小的内存区域,所以每次循环后 addr 增加 PMD_SIZE
     */
	for (; i < PTRS_PER_PMD; i++, address = next) {
        /*
         * pmd_page 是中层页目录的基地址,pmd_index(address) 计算出 address 对应的中层页目录项索引
         * 两者相加,就得到 address 对应的中层页目录项指针
         * prot 是页面分配标志的组合
         */
		pmd_t *pmd = pmd_page + pmd_index(address);
		pte_t *pte;
		pgprot_t new_prot = prot;

        /*
         * next 计算出下一次迭代时,要映射的起始地址
         * 在迭代过程中,如果 address >= end,说明当前地址超出了指定的内存区间
         * 那么超出地址区间的中层页目项如何处理呢?
         * 如果处于系统初始化阶段并且从 address 到 next 之间的内存是空洞,那么就将该中层页目项设置为 0;否则,忽略掉,什么也不做。
         * E820_RAM 和 E820_RESERVED_KERN 都是可用内存类型,e820_any_mapped 函数检查指定范围的内存与指定类型的内存块之间是否有交集。如果从 address(向下对齐到 PMD_SIZE)到 next 之间的内存,与可用内存块之间没有任何交集,说明这整段内存都是空洞,则将对应的中层页目录项设置为 0。
         */
		next = (address & PMD_MASK) + PMD_SIZE;
		if (address >= end) {
			if (!after_bootmem &&
			    !e820_any_mapped(address & PMD_MASK, next, E820_RAM) &&
			    !e820_any_mapped(address & PMD_MASK, next, E820_RESERVED_KERN))
				set_pmd(pmd, __pmd(0));
			continue;
		}
        
        /*
         * pmd_val 用于获取中层页目录项的值,如果该值为真,说明虚拟地址 address 对应的中层页目录项已经创建了
         */
		if (pmd_val(*pmd)) {
            /*
             * pmd_large 用于检测中层页目录项是否直接映射到 2MB 的页。如果是,返回 true;否则,返回 false。
             * 上层页目录项和中层页目录项的 PS 位(位 7)用于指示该页表项是直接映射到页还是指向下一级页表。
             * 对于上层页目录项来说,如果 PS 位置位,说明该表项直接映射 1GB 的页
             * 对于中层页目录项来说,如果 PS 位置位,说明该表项直接映射到 2MB 的页
             * 这里处理的是未直接映射到 2MB 页的情况
             */
			if (!pmd_large(*pmd)) {
				spin_lock(&init_mm.page_table_lock);
                /*
                 * 获取到中层页目录项指向的页表基地址,并赋值给 pte
                 * 然后通过 phys_pte_init 函数填充页表的各页表项,由于页表项只能映射到 4KB 的页,所以就不需要页大小掩码 page_size_mask 了
                 */
				pte = (pte_t *)pmd_page_vaddr(*pmd);
				last_map_addr = phys_pte_init(pte, address,
								end, prot);
				spin_unlock(&init_mm.page_table_lock);
				continue;
			}
			/*
			 * If we are ok with PG_LEVEL_2M mapping, then we will
			 * use the existing mapping,
			 *
			 * Otherwise, we will split the large page mapping but
			 * use the same existing protection bits except for
			 * large page, so that we don't violate Intel's TLB
			 * Application note (317080) which says, while changing
			 * the page sizes, new and old translations should
			 * not differ with respect to page frame and
			 * attributes.
			 */
			/*
			 * 程序来到这里,说明当前的中层页目录项直接映射到 2MB 内存页
			 * 如果页大小掩码 page_size_mask 中包含 2MB 页的掩码位,
			 * 那么我们直接利用现有的中层页目录项就可以了
			 * 此时,只需要更新映射的页数 pages 及 last_map_addr 的值
			 */
			if (page_size_mask & (1 << PG_LEVEL_2M)) {
				if (!after_bootmem)
					pages++;
				last_map_addr = next;
				continue;
			}
            
            /*
             * 如果本身是 2MB 的页,而要求映射成更细粒度的页,那么就需要对 2MB 页进行拆分
             * 既然需要拆分,那么就要清除当前中层页目录项中的 PS 位(位 7)
             * pte_clrhuge 函数负责清除中层页目录项中的 PS 位
             * pte_pgprot 获取清除 PS 标志后的页属性,并赋值给 new_prot
             */
			new_prot = pte_pgprot(pte_clrhuge(*(pte_t *)pmd));
		}

        /*
         * 这里处理中层页目录项为 0(说明没映射过),但要求映射 2MB 页的情况
         * 因为直接映射到页,所以中层页目录就是最低一级的页表了
         * 这里先将映射的页数量 pages 加 1,然后将 address 对应的物理地址及页标志组合成页表项格式后,通过 set_pte 函数写入中层页表项中
         * 再更新 last_map_addr 的值
         */
		if (page_size_mask & (1<<PG_LEVEL_2M)) {
			pages++;
			spin_lock(&init_mm.page_table_lock);
			set_pte((pte_t *)pmd,
				pfn_pte((address & PMD_MASK) >> PAGE_SHIFT,
					__pgprot(pgprot_val(prot) | _PAGE_PSE)));
			spin_unlock(&init_mm.page_table_lock);
			last_map_addr = next;
			continue;
		}

        /*
         * 程序来到这里,肯定是不能映射 2MB 的页了
         * 不管是原先映射到 2MB 的页需要拆分成更细粒度的页,还是原先没有映射,都需要建立新的页表
         * 建立新页表,就需要为页表分配一页内存,分配工作是通过 alloc_low_page 函数完成的
         * 有了页表,通过 phys_pte_init 函数填充页表项,来进行映射
         * last_map_addr 表示映射的最大地址
         * 然后,在自旋锁的保护下,调用 pmd_populate_kernel 函数将页表的的物理地址以及页标志组合成中层页目录项写入中层页目录中
         */
		pte = alloc_low_page();
		last_map_addr = phys_pte_init(pte, address, end, new_prot);

		spin_lock(&init_mm.page_table_lock);
		pmd_populate_kernel(&init_mm, pmd, pte);
		spin_unlock(&init_mm.page_table_lock);
	}
    
    /*
     * 更新 2MB 页的数量,用于信息统计
     * 对比 phys_pud_init 函数,phys_pmd_init 函数中并没有刷新 TLB 的代码,
     * 这是因为只需要在最外层刷新就可以了
     */
	update_page_count(PG_LEVEL_2M, pages);
	return last_map_addr;
}
5.3.2.3 phys_pte_init

phys_pte_init 函数对页表进行初始化,即填充页表项。

// file: arch/x86/mm/init_64.c
static unsigned long __meminit
phys_pte_init(pte_t *pte_page, unsigned long addr, unsigned long end,
	      pgprot_t prot)
{
    /*
     * pages 保存映射的 4KB 页的数量
     * last_map_addr 保存映射的最大地址
     * i 用作循环变量,初始化为起始虚拟地址 addr 对应的页表项索引
     * pte_index 获取虚拟地址对应的页表项索引
     */
	unsigned long pages = 0, next;
	unsigned long last_map_addr = end;
	int i;
     
    /*
     * pte_page 是页表基地址,pte_index(addr) 计算出 addr 对应的页表项索引
     * 两者相加,就得到 addr 对应的页表项指针
     */
	pte_t *pte = pte_page + pte_index(addr);
     
    /*
     * 通过一个循环为来初始化页表项
     * 每个页表包含 PTRS_PER_PTE(扩展为 512)个页表项,所以 i 必须小于 PTRS_PER_PTE
     * 由于每个页表项控制 PAGE_SIZE(扩展为 4KB)大小的内存范围,所以每次循环后 addr 增加 PAGE_SIZE
     */
	for (i = pte_index(addr); i < PTRS_PER_PTE; i++, addr = next, pte++) {
        /*
         * next 计算出下一次迭代时,要映射的起始地址
         * 在迭代过程中,如果 addr >= end,说明此时映射的内存已经超出了指定的内存范围
         * 那么超出地址范围的页表项如何处理呢?
         * 如果处于系统初始化阶段并且从 addr 到 next 之间的内存是空洞,那么就将该页表项设置为 0;否则,忽略掉,什么也不做。
         * E820_RAM 和 E820_RESERVED_KERN 都是可用内存类型,e820_any_mapped 函数检查指定范围的内存与指定类型的内存块之间是否有交集。如果从 address(向下对齐到 PAGE_SIZE)到 next 之间的内存,与可用内存块之间没有任何交集,说明这整段内存都是空洞,则将已报销设置为 0。
         */
		next = (addr & PAGE_MASK) + PAGE_SIZE;
		if (addr >= end) {
			if (!after_bootmem &&
			    !e820_any_mapped(addr & PAGE_MASK, next, E820_RAM) &&
			    !e820_any_mapped(addr & PAGE_MASK, next, E820_RESERVED_KERN))
				set_pte(pte, __pte(0));
			continue;
		}

		/*
		 * We will re-use the existing mapping.
		 * Xen for example has some special requirements, like mapping
		 * pagetable pages as RO. So assume someone who pre-setup
		 * these mappings are more intelligent.
		 */
		/*
		 * 如果页表项已经存在了,直接使用现有的页表项,
		 * 如果是在系统初始化阶段,则增加 pages 的值
		 */
		if (pte_val(*pte)) {
			if (!after_bootmem)
				pages++;
			continue;
		}

		if (0)
			printk("   pte=%p addr=%lx pte=%016lx\n",
			       pte, addr, pfn_pte(addr >> PAGE_SHIFT, PAGE_KERNEL).pte);
		
        /*
		 * 如果页表项没有映射过,则设置页表项并增加 pages 的值
		 * 然后,还要更新 last_map_addr 的值,即映射的最大物理地址
		 */
        pages++;
		set_pte(pte, pfn_pte(addr >> PAGE_SHIFT, prot));
		last_map_addr = (addr & PAGE_MASK) + PAGE_SIZE;
	}

    /*
     * 更新 4KB 页的数量,用于信息统计
     */
	update_page_count(PG_LEVEL_4K, pages);

	return last_map_addr;
}
5.3.2.4 alloc_low_page

alloc_low_page 函数会分配一页内存,并返回内存的起始虚拟地址。其内部直接调用了 alloc_low_pages 函数。

// file: arch/x86/mm/mm_internal.h
static inline void *alloc_low_page(void)
{
	return alloc_low_pages(1);
}
5.3.2.5 alloc_low_pages

alloc_low_pages 函数会分配指定数量的内存页,并返回已分配内存的起始虚拟地址。如果伙伴系统已经就绪,会通过 __get_free_pages 函数从伙伴系统申请内存;否则,从 MemBlock 分配器申请内存。

// file: arch/x86/mm/init.c
/*
 * Pages returned are already directly mapped.
 *
 * Changing that is likely to break Xen, see commit:
 *
 *    279b706 x86,xen: introduce x86_init.mapping.pagetable_reserve
 *
 * for detailed information.
 */
__ref void *alloc_low_pages(unsigned int num)
{
	unsigned long pfn;
	int i;
	
    /*
     * 我们在 split_mem_range 函数中介绍过,after_bootmem 变量的默认值为 0,当伙伴系统就绪后,在 mem_init 函数中被修改为 1
     * 在进行直接映射区页表的构建时,伙伴系统还未就绪,after_bootmem 的值依旧为 0,所以这段代码不会执行
     */
	if (after_bootmem) {
		unsigned int order;

		order = get_order((unsigned long)num << PAGE_SHIFT);
		return (void *)__get_free_pages(GFP_ATOMIC | __GFP_NOTRACK |
						__GFP_ZERO, order);
	}

     /*
     * 在系统初始化时,在堆区(.brk)保留了 INIT_PGT_BUF_SIZE(扩展为 5 个页)的空间作为页表的 buffer,详见 “5.3.2.6” 节
     * 然后会在 early_alloc_pgt_buf 函数中将 pgt_buf_end 初始化为 buffer 的起始地址  将 pgt_buf_top 初始化为 buffer 的结束地址
     * 当为页表分配内存时,会优先从该 buffer 中获取,每成功获取一次,pgt_buf_end 都会增加对应的数值
     * 如果 buffer 中剩余的空间不足以再分配 num 数量的页,或者不能使用堆缓存来分配(由 can_use_brk_pgt 控制),则通过 memblock_find_in_range 函数在 MemBlock 中分配内存,并将分配的内存区域通过 memblock_reserve 函数添加到 MemBlock 分配器的保留区间里。
     * 如果堆中的页表 buffer 空间充足且允许从 buffer 中分配,则直接从 buffer 中分配空间,此时 pgt_buf_end 需要记录分配后的位置
     * pfn 指示已分配内存的起始页帧号
     */
	if ((pgt_buf_end + num) > pgt_buf_top || !can_use_brk_pgt) {
		unsigned long ret;
		if (min_pfn_mapped >= max_pfn_mapped)
			panic("alloc_low_page: ran out of memory");
		ret = memblock_find_in_range(min_pfn_mapped << PAGE_SHIFT,
					max_pfn_mapped << PAGE_SHIFT,
					PAGE_SIZE * num , PAGE_SIZE);
		if (!ret)
			panic("alloc_low_page: can not alloc memory");
		memblock_reserve(ret, PAGE_SIZE * num);
		pfn = ret >> PAGE_SHIFT;
	} else {
		pfn = pgt_buf_end;
		pgt_buf_end += num;
		printk(KERN_DEBUG "BRK [%#010lx, %#010lx] PGTABLE\n",
			pfn << PAGE_SHIFT, (pgt_buf_end << PAGE_SHIFT) - 1);
	}
    
     /*
     * 分配成功后,调用 clear_page 函数,将每个页的内容用 0 来填充
     */
	for (i = 0; i < num; i++) {
		void *adr;

		adr = __va((pfn + i) << PAGE_SHIFT);
		clear_page(adr);
	}

     /*
     * 返回已分配内存的起始虚拟地址
     */
	return __va(pfn << PAGE_SHIFT);
}
5.3.2.6 pgt_buf_end、pgt_buf_top

pgt_buf_endpgt_buf_top 以及 can_use_brk_pgt 都是静态变量,can_use_brk_pgt 被初始化为 true

// file: arch/x86/mm/init.c
static unsigned long __initdata pgt_buf_start;
static unsigned long __initdata pgt_buf_end;
static unsigned long __initdata pgt_buf_top;

static bool __initdata can_use_brk_pgt = true;

编译时,宏 RESERVE_BRK 会在.brk_reservation 节保留 INIT_PGT_BUF_SIZE(扩展为 5 个页大小)的空间作为页表的 buffer。

// file: arch/x86/mm/init.c
/* need 4 4k for initial PMD_SIZE, 4k for 0-ISA_END_ADDRESS */
#define INIT_PGT_BUF_SIZE	(5 * PAGE_SIZE)
RESERVE_BRK(early_pgt_alloc, INIT_PGT_BUF_SIZE);

RESERVE_BRK 定义如下:

// file: arch/x86/include/asm/setup.h
/*
 * Reserve space in the brk section.  The name must be unique within
 * the file, and somewhat descriptive.  The size is in bytes.  Must be
 * used at file scope.
 *
 * (This uses a temp function to wrap the asm so we can pass it the
 * size parameter; otherwise we wouldn't be able to.  We can't use a
 * "section" attribute on a normal variable because it always ends up
 * being @progbits, which ends up allocating space in the vmlinux
 * executable.)
 */
#define RESERVE_BRK(name,sz)						\
	static void __section(.discard.text) __used notrace		\
	__brk_reservation_fn_##name##__(void) {				\
		asm volatile (						\
			".pushsection .brk_reservation,\"aw\",@nobits;" \
			".brk." #name ":"				\
			" 1:.skip %c0;"					\
			" .size .brk." #name ", . - 1b;"		\
			" .popsection"					\
			: : "i" (sz));					\
	}

该宏接收 2 个参数:

  • @name:名称,该名称会嵌入到函数中,要求文件域唯一。
  • @sz:保留空间大小

RESERVE_BRK 扩展为一个静态函数,函数内部是一段内联汇编代码,这段代码会被放置在 .brk_reservation 节。代码的功能是在 .brk_reservation 节中保留 @sz 大小的空间。

在链接阶段,链接脚本会将所有文件中 .brk_reservation 节的内容输出到 .brk 节中。相应的,就会在 .brk 节中保留 @sz 大小的空间。

// file: arch/x86/kernel/vmlinux.lds.S
	.brk : AT(ADDR(.brk) - LOAD_OFFSET) {
		__brk_base = .;
		. += 64 * 1024;		/* 64k alignment slop space */
		*(.brk_reservation)	/* areas brk users have reserved */
		__brk_limit = .;
	}

在系统初始化时,会调用 early_alloc_pgt_buf 函数中将 pgt_buf_end 初始化为 buffer 的起始地址的页帧号 将 pgt_buf_top 初始化为 buffer 的结束地址的页帧号。

// file: arch/x86/mm/init.c
void  __init early_alloc_pgt_buf(void)
{
	unsigned long tables = INIT_PGT_BUF_SIZE;
	phys_addr_t base;

	base = __pa(extend_brk(tables, PAGE_SIZE));

	pgt_buf_start = base >> PAGE_SHIFT;
	pgt_buf_end = pgt_buf_start;
	pgt_buf_top = pgt_buf_start + (tables >> PAGE_SHIFT);
}

函数调用链:start_kernel() -> setup_arch() -> early_alloc_pgt_buf()

其中,extend_brk 函数用于在堆内分配指定大小的内存并进行边界对齐。在本例中,就是在堆中分配 INIT_PGT_BUF_SIZE 大小的内存,且内存的边界要对齐到 PAGE_SIZE

// file: arch/x86/kernel/setup.c
void * __init extend_brk(size_t size, size_t align)
{
	size_t mask = align - 1;
	void *ret;

	BUG_ON(_brk_start == 0);
	BUG_ON(align & mask);

    /*
     * _brk_end 指示堆中已分配内存的结束地址,此处将 _brk_end 向上圆整对齐到 align
     */
	_brk_end = (_brk_end + mask) & ~mask;
	BUG_ON((char *)(_brk_end + size) > __brk_limit);
    /*
     * ret 指示新分配的堆空间的起始地址
     * _brk_end 指示分配后的结束地址,也是下一次分配的起始地址
     */
	ret = (void *)_brk_end;
	_brk_end += size;

    /* 将分配的空间用 0 填充 */
	memset(ret, 0, size);

	return ret;
}
5.3.2.7 clear_page

clear_page 函数将页的内容填充为 0,其中 page 指示页结构的起始地址。

// file: arch/x86/include/asm/page_32.h
static inline void clear_page(void *page)
{
	memset(page, 0, PAGE_SIZE);
}
5.3.2.8 sync_global_pgds

sync_global_pgds 函数将指定内存区间对应的内核空间的全局页目录项同步到用户空间的进程。

sync_global_pgds 函数接收 2 个参数:

  • @start:需要同步的内存空间的起始虚拟地址
  • @end:需要同步的内存空间的结束虚拟地址

同步逻辑如下:

1、根据起始地址和结束地址,能够计算出一共要同步多少个全局页目录项。由于每个全局页目录项控制着 PGDIR_SIZE(扩展为 512GB)的内存,所以实际需要同步的全局页目录项数量不会很多。

2、所有用户进程的全局页目录对应的 page 通过 page->lru 链入双向链表 pgd_list 中,所以遍历 pgd_list 就可以获取到所有用户进程的全局页目录对应的 page。每一个进程的全局页目录都是 4KB 大小的一个页,而每个物理页对应一个 page 结构体实例,通过该 page 实例,就可以找到全局页目录的虚拟地址。

3、对于要同步的每一个全局页目录项,遍历双向链表 pgd_list,获取到每个用户进程的全局页目录项地址后,用新的的全局页目录项覆盖掉旧值。

// file: arch/x86/mm/init_64.c
/*
 * When memory was added/removed make sure all the processes MM have
 * suitable PGD entries in the local PGD level page.
 */
void sync_global_pgds(unsigned long start, unsigned long end)
{
	unsigned long address;

    /*
     * 外层循环,每次循环同步一个全局页目录项
     * 由于每个全局页目录项控制着 PGDIR_SIZE(扩展为 512GB)大小的内存区域,所以每次循环后,地址增加 PGDIR_SIZE
     */
	for (address = start; address <= end; address += PGDIR_SIZE) {
        /*
         * pgd_offset_k 宏获取虚拟地址对应的全局页目录项地址
         * pgd_offset_k 是 pgd_offset 的简化形式,用于获取内核空间的全局页目录项地址
         */
		const pgd_t *pgd_ref = pgd_offset_k(address);
		struct page *page;

        /*
         * 如果全局页目录项的值为 0,说明该全局页目录项未使用,不需要同步
         */
		if (pgd_none(*pgd_ref))
			continue;

		spin_lock(&pgd_lock);
        
        /*
         * 遍历 pgd_list,获取到用户进程的全局页目录对应的 page 地址,保存在参数 page 中
         */
		list_for_each_entry(page, &pgd_list, lru) {
			pgd_t *pgd;
			spinlock_t *pgt_lock;
            /*
             * page_address 函数获取到 page 对应的虚拟地址,也是全局页目录的基地址
             * pgd_index 获取到指定地址所对应的全局页目录项索引
             * 两者相加,就得到指定地址对应的全局页目录项的指针
             */
			pgd = (pgd_t *)page_address(page) + pgd_index(address);
			/* the pgt_lock only for Xen */
            /*
             * 对于全局页目录所对应的 page 来说,其 page->index 保存的是进程的内存描述符 mm
             * pgd_page_get_mm 函数从 page->index 中获取内存描述符 mm
             * mm->page_table_lock 是保护页表的自旋锁
             */
			pgt_lock = &pgd_page_get_mm(page)->page_table_lock;
			spin_lock(pgt_lock);
            
            /*
             * 如果用户进程的全局页目录项的值为 0,则将内核空间的全局页目录项拷贝过来
             * 如果不为 0,说明已经同步过了,此时内核空间和用户进程的的页目录项所对应 page 的虚拟地址应该是一致的;如果不一致,说明是内核 bug
             */
			if (pgd_none(*pgd))
				set_pgd(pgd, *pgd_ref);
			else
				BUG_ON(pgd_page_vaddr(*pgd)
				       != pgd_page_vaddr(*pgd_ref));

			spin_unlock(pgt_lock);
		}
		spin_unlock(&pgd_lock);
	}
}

说明:对于一个普通的物理页来说,其对应的 page 结构中,page->index 字段用于文件映射,指示该页相对于文件的偏移;page->lru 字段用于将 page 结构体接入 lru 链表(active_list 或者 inactive_list),用于页面的换出。对于进程来说,其全局页目录是常驻内存的,既不会映射到文件也不会被换出,所以全局页目录对应的 page 结构中,其 indexlru 字段被用作了其它用途。其中,index 字段保存了进程的内存描述符 mm_struct 的实例;lru 字段接入了全局页目录链表 pgd_list

5.3.2.9 pgd_list

pgd_list 是一个双向链表的表头,这是一个 struct list_head 类型的全局变量,通过宏 LIST_HEAD 定义:

// file: arch/x86/mm/fault.c
LIST_HEAD(pgd_list);

LIST_HEAD 扩展如下:

// file: include/linux/list.h
#define LIST_HEAD(name) \
	struct list_head name = LIST_HEAD_INIT(name)

#define LIST_HEAD_INIT(name) { &(name), &(name) }

struct list_head {
	struct list_head *next, *prev;
};

由上述代码可知,初始化时,pgd_list 的成员 prevnext 都指向自身。

用户进程全局页目录所对应的 page 结构,通过 page->lru 字段链入双向链表pgd_list,这是通过 pgd_list_add 函数完成的。

5.3.2.10 pgd_list_add
static inline void pgd_list_add(pgd_t *pgd)
{
	struct page *page = virt_to_page(pgd);

	list_add(&page->lru, &pgd_list);
}

virt_to_page 函数获取到全局页目录对应的 page 实例地址,然后将 page->lru 加入到双向链表 pgd_list 中。

5.3.2.11 page_address

page_address 宏获取 page 对应的物理页在直接映射区的虚拟地址。该宏扩展为 lowmem_page_address 函数。

// file: include/linux/mm.h
#define page_address(page) lowmem_page_address(page)
5.3.2.12 lowmem_page_address

lowmem_page_address 函数获取 page 对应的物理页在直接映射区的虚拟地址。

// file: include/linux/mm.h
static __always_inline void *lowmem_page_address(const struct page *page)
{
	return __va(PFN_PHYS(page_to_pfn(page)));
}

page_to_pfn 宏计算 page 实例对应的页帧号。内核会为每一个物理页分配一个 page 结构体实例,page 实例和页帧号可以相互转换。根据内存模型的不同,page_to_pfn 宏的实现也不相同。本文不会详细讲解 page_to_pfn 宏的实现,当前我们只要知道 page_to_pfn 宏会获取 page 实例对应的页帧号就可以了。

PFN_PHYS 会将页帧号左移 PAGE_SHIFT (扩展为12)位,得到物理地址:

// file: include/linux/pfn.h
#define PFN_PHYS(x)	((phys_addr_t)(x) << PAGE_SHIFT)

最后,通过宏 __va 将物理地址转换成直接映射区的虚拟地址。

5.3.2.13 pgd_set_mm、pgd_page_get_mm

pgd_set_mm 函数将 page 实例的 index 字段设置为进程的内存描述符。

pgd_page_get_mm 函数从 page 实例的 index 字段获取到进程的内存描述符。

virt_to_page 函数获取虚拟地址对应的 page 实例。

// file: arch/x86/mm/pgtable.c
static void pgd_set_mm(pgd_t *pgd, struct mm_struct *mm)
{
	BUILD_BUG_ON(sizeof(virt_to_page(pgd)->index) < sizeof(mm));
	virt_to_page(pgd)->index = (pgoff_t)mm;
}

struct mm_struct *pgd_page_get_mm(struct page *page)
{
	return (struct mm_struct *)page->index;
}
5.3.2.14 e820_any_mapped

e820_any_mapped 函数检查指定类型的内存块中,是否有任何一个内存块与 startend 的内存区间有重叠。如果有重叠,返回 1;否则,返回 0。换句话说,如果 startend 之间没有可用内存,全是内存空洞,返回 0;否则,返回 1。

E820 内存相关请参考 “1.4 E820内存” 小节。

/*
 * This function checks if any part of the range <start,end> is mapped
 * with type.
 */
int
e820_any_mapped(u64 start, u64 end, unsigned type)
{
	int i;

	for (i = 0; i < e820.nr_map; i++) {
		struct e820entry *ei = &e820.map[i];

		if (type && ei->type != type)
			continue;
		if (ei->addr >= end || ei->addr + ei->size <= start)
			continue;
		return 1;
	}
	return 0;
}
5.3.2.15 update_page_count

update_page_count 函数定义如下:

// file: arch/x86/mm/pageattr.c
static unsigned long direct_pages_count[PG_LEVEL_NUM];

void update_page_count(int level, unsigned long pages)
{
	/* Protect against CPA */
	spin_lock(&pgd_lock);
	direct_pages_count[level] += pages;
	spin_unlock(&pgd_lock);
}

该函数接收 2 个参数:

  • @levelpg_level 中不同页大小对应的枚举值
  • @pages:新增的页面数量

update_page_count 函数用于记录直接映射区不同大小的的页的数量,即 1GB、2MB 和 4KB 的页的数量。不同大小的页数量记录在数组 direct_pages_count 中。该数组包含 PG_LEVEL_NUM 个成员,其中 PG_LEVEL_NUM 是一个枚举值,其值为 4。每个数组成员保存着不同大小的页数量

// file: arch/x86/include/asm/pgtable_types.h
enum pg_level {
	PG_LEVEL_NONE,
	PG_LEVEL_4K,
	PG_LEVEL_2M,
	PG_LEVEL_1G,
	PG_LEVEL_NUM
};

直接映射区不同大小的页的内存数量,可通过 cat /proc/meminfo 命令查看。在我的虚拟机( 1核2G )中,各种页的内存数量如下所示:

meminfo_directmap.png

5.3.3 add_pfn_range_mapped

add_pfn_range_mapped 函数保存已经映射的内存区间 (保存在数组pfn_mapped 中),并计算已经映射的页帧数量 nr_pfn_mapped、映射的最大页帧 max_pfn_mapped以及映射的 4GB 以下区域的最大页帧 max_low_pfn_mapped

// file: arch/x86/mm/init.c
static void add_pfn_range_mapped(unsigned long start_pfn, unsigned long end_pfn)
{
	nr_pfn_mapped = add_range_with_merge(pfn_mapped, E820_X_MAX,
					     nr_pfn_mapped, start_pfn, end_pfn);
	nr_pfn_mapped = clean_sort_range(pfn_mapped, E820_X_MAX);

	max_pfn_mapped = max(max_pfn_mapped, end_pfn);

	if (start_pfn < (1UL<<(32-PAGE_SHIFT)))
		max_low_pfn_mapped = max(max_low_pfn_mapped,
					 min(end_pfn, 1UL<<(32-PAGE_SHIFT)));
}

该函数内部使用了几个全局变量:pfn_mappednr_pfn_mappedmax_pfn_mappedmax_low_pfn_mapped

其中,max_pfn_mapped 用来保存映射的最大页帧号,max_low_pfn_mapped 用来保存映射的 4GB 以下的最大页帧号。

// file: arch/x86/kernel/setup.c
/*
 * max_low_pfn_mapped: highest direct mapped pfn under 4GB
 * max_pfn_mapped:     highest direct mapped pfn over 4GB
 *
 * The direct mapping only covers E820_RAM regions, so the ranges and gaps are
 * represented by pfn_mapped
 */
unsigned long max_low_pfn_mapped;
unsigned long max_pfn_mapped;

nr_pfn_mapped 用来保存已经映射的页帧数量;pfn_mapped 用来保存映射的内存区域,这是一个 struct range 结构的数组,该结构只有 startend 两个字段,用来指示内存区域的起始和结束地址。

// file: arch/x86/mm/init.c
struct range pfn_mapped[E820_X_MAX];
int nr_pfn_mapped;
// file: include/linux/range.h
struct range {
	u64   start;
	u64   end;
};

add_range_with_merge 函数将新映射的内存区域添加到 pfn_mapped 数组中,添加过程中如果新区域与原有区域有重叠,则合并成一个内存区域。合并时,先将重叠区间之后的数组成员整体前移一个位置,然后将合并后内存区域放置到 pfn_mapped 数组中其它有效成员之后,成为最后一个有效成员。

不管是否需要合并,都会将新的内存区域插入到其它有效成员之后,成为最后一个有效成员,这将导致 1 个问题:

  • pfn_mapped 数组是无序的,并没有按地址从低到高排序

所以,在合并完成后,需要通过 clean_sort_range 函数来解决这个问题。在 clean_sort_range 函数中,会将所有有效成员放置到数组头部,然后调用 sort 函数对 pfn_mapped 数组进行排序,保证内存区间是从低到高排序的。

add_range_with_merge 函数和 clean_sort_range 函数都会更新 pfn_mapped 数组的有效成员数量,即 nr_pfn_mapped 的值,并将该值返回。

最后,更新 max_pfn_mappedmax_low_pfn_mapped 的值。

5.3.3.1 add_range_with_merge

add_range_with_merge 函数将已经映射的内存区域添加到 pfn_mapped 数组中,添加过程中如果当前的内存区域与原有区域重叠,则合并成一个内存区域。如果能够合并,先将原有的可合并区域的之后的数组成员整体前移一个位置,然后将合并后内存区域放置到 pfn_mapped 数组的最后一个有效成员中。

该函接收 5 个参数:

  • @range:已映射内存区域的数组
  • @az:数组的成员数量
  • @nr_range:已映射的内存区域数量
  • @start:新映射内存区域的起始地址
  • @end:新映射内存区域的结束地址

返回合并后的已映射的内存区域数量。

// file: kernel/range.c
int add_range_with_merge(struct range *range, int az, int nr_range,
		     u64 start, u64 end)
{
	int i;
	/* 
	 * 如果 start >= end, 这是一个无效区间
	 * 直接返回原有的区间数量
	 */
	if (start >= end)
		return nr_range;

	/* get new start/end: */
	/* 
	 * nr_range 是已映射的内存区域数量,也是数组 range 的有效成员数量
	 * 在一个循环中,遍历 range 数组的每一个成员,检测原有成员与新区间是否有重叠
	 */
	for (i = 0; i < nr_range; i++) {
		u64 common_start, common_end;

        /*
         * 如果成员的 end 字段为 0,说明这是一个无效成员,跳过
         */
		if (!range[i].end)
			continue;
        
        /*
         * 检测新区间与原有成员是否重叠
         * 如果 common_start > common_end,说明未重叠,跳过
         */
		common_start = max(range[i].start, start);
		common_end = min(range[i].end, end);
		if (common_start > common_end)
			continue;

		/* new start/end, will add it back at last */
        /*
         * 处理区间重叠的情况
         * start 和 end 是合并后区间的起始和结束地址
         */
		start = min(range[i].start, start);
		end = max(range[i].end, end);

        /*
         * range[i] 与新区间有重叠,
         * 先将 range[i] 之后的所有成员向前移动一个单位
         * 移动后,原最后一个有效的成员现在变成无效了,所以将其起始和结束地址初始化为 0
         * 移动后,数组有效成员数量少了一个,所以 nr_range 自减一
         * 移动后,range[i] 的值变了,导致下次循环仍旧要 range[i],
         * 所以先让 i--,此次循环结束后会执行 i++,这样下次循环执行时,处理的还是 range[i]
         */
		memmove(&range[i], &range[i + 1],
			(nr_range - (i + 1)) * sizeof(range[i]));
		range[nr_range - 1].start = 0;
		range[nr_range - 1].end   = 0;
		nr_range--;
		i--;
	}

	/* Need to add it: */
    /*  
     * 将新区间(合并或未合并都有可能)添加到数组最后一个成员中
     * 同时,将成员数量 nr_range 加一
     */
	return add_range(range, az, nr_range, start, end);
}
5.3.3.2 add_range

add_range 函数将映射的内存区间添加到数组 range 中,新添加的成员放置在其它有效成员之后,成为最后一个有效成员。随后,将有效成员数量 nr_range 加一。

// file: kernel/range.c
int add_range(struct range *range, int az, int nr_range, u64 start, u64 end)
{
	/* 
	 * 如果 start >= end, 这是一个无效区间
	 * 直接返回原有的区间数量
	 */
	if (start >= end)
		return nr_range;

	/* Out of slots: */
	/* 
	 * 如果超过数组成员数量,
	 * 直接返回原有的区间数量
	 */
	if (nr_range >= az)
		return nr_range;

    /* 
	 * 将内存区间保存到以 nr_range 为下标的成员中
	 * nr_range 是原有效成员数量
	 */
	range[nr_range].start = start;
	range[nr_range].end = end;

	nr_range++;

	return nr_range;
}
5.3.3.3 clean_sort_range

clean_sort_range 函数清除夹杂在有效成员之间的无效成员,清除后所有有效成员连续放置且位于数组头部,然后按照 struct range 的起始地址从小到大排序。

// file: kernel/range.c
int clean_sort_range(struct range *range, int az)
{
	int i, j, k = az - 1, nr_range = az;

    /*
     * 1、外层循环从前往后找无效的数组成员,找到后停止
     * 2、内存循环从后往前找有效的数组成员,找到后停止
     * 3、将有效成员的值赋值给无效成员,并将原有效成员的字段都设置为 0,变成无效成员(简单理解就是交换两个成员的值)
     * 4、重复 1 ~ 3 步,直到内、外层指针相遇
     * 
     */
	for (i = 0; i < k; i++) {
		if (range[i].end)
			continue;
		for (j = k; j > i; j--) {
			if (range[j].end) {
				k = j;
				break;
			}
		}
		if (j == i)
			break;
		range[i].start = range[k].start;
		range[i].end   = range[k].end;
		range[k].start = 0;
		range[k].end   = 0;
		k--;
	}
	/* count it */
    /*
     * 统计有效成员数量
     * 第一个无效成员的下标就是有效成员数量
     */
	for (i = 0; i < az; i++) {
		if (!range[i].end) {
			nr_range = i;
			break;
		}
	}

	/* sort them */
    /*
     * 对数组成员按地址大小进行排序
     * 使用 cmp_range 函数对成员进行比较
     */
	sort(range, nr_range, sizeof(struct range), cmp_range, NULL);

	return nr_range;
}
5.3.3.4 cmp_range

cmp_range 函数比较 2 个 struct range 类型数据的大小。按照起始地址 start 来比较,start 越小则数据越小。

// file: kernel/range.c
static int cmp_range(const void *x1, const void *x2)
{
	const struct range *r1 = x1;
	const struct range *r2 = x2;
	s64 start1, start2;

	start1 = r1->start;
	start2 = r2->start;

	return start1 - start2;
}

5.4 init_range_memory_mapping

init_mem_mapping 函数适用于小范围内存映射。当使用 init_mem_mapping进行映射时,不管指定的内存范围是否有空洞,都会进行映射。

init_range_memory_mapping 函数适用于大范围内存区间的映射。当内存范围较大时,该区间内就很可能存在空洞。init_range_memory_mapping 函数会排除掉内存空洞,只对实际存在的内存进行映射。

该函数定义如下:

// file: arch/x86/mm/init.c
/*
 * We need to iterate through the E820 memory map and create direct mappings
 * for only E820_RAM and E820_KERN_RESERVED regions. We cannot simply
 * create direct mappings for all pfns from [0 to max_low_pfn) and
 * [4GB to max_pfn) because of possible memory holes in high addresses
 * that cannot be marked as UC by fixed/variable range MTRRs.
 * Depending on the alignment of E820 ranges, this may possibly result
 * in using smaller size (i.e. 4K instead of 2M or 1G) page tables.
 *
 * init_mem_mapping() calls init_range_memory_mapping() with big range.
 * That range would have hole in the middle or ends, and only ram parts
 * will be mapped in init_range_memory_mapping().
 */
static unsigned long __init init_range_memory_mapping(
					   unsigned long r_start,
					   unsigned long r_end)
{
	unsigned long start_pfn, end_pfn;
	unsigned long mapped_ram_size = 0;
	int i;

	for_each_mem_pfn_range(i, MAX_NUMNODES, &start_pfn, &end_pfn, NULL) {
		u64 start = clamp_val(PFN_PHYS(start_pfn), r_start, r_end);
		u64 end = clamp_val(PFN_PHYS(end_pfn), r_start, r_end);
		if (start >= end)
			continue;

		/*
		 * if it is overlapping with brk pgt, we need to
		 * alloc pgt buf from memblock instead.
		 */
		can_use_brk_pgt = max(start, (u64)pgt_buf_end<<PAGE_SHIFT) >=
				    min(end, (u64)pgt_buf_top<<PAGE_SHIFT);
		init_memory_mapping(start, end);
		mapped_ram_size += end - start;
		can_use_brk_pgt = true;
	}

	return mapped_ram_size;
}

在本函数中,for_each_mem_pfn_range 宏会遍历 MemBlock 管理的所有 memory 类型的内存块,并将内存块的起始页帧和结束页帧分别保存到 &start_pfn&end_pfn 中。然后通过 clamp_val 函数,将起始地址和结束地址限制在 r_startr_end 之间。如果 start >= end ,说明该内存块与指定范围没有交集,不需要映射,则进入下一轮循环。如果有交集,则 startend 表示交集的起始和结束地址,这段内存区间是可以映射的。

接下来,计算 can_use_brk_pgt 的值。我们在上文中已经介绍过,can_use_brk_pgt 指示在为页表分配内存时,能否使用堆中的 buffer 。buffer 的大小为 5 个页,pgt_buf_end 指示 buffer 中可用空间起始地址的页帧号;pgt_buf_top 指示 buffer 的最大地址的页帧号。

如果要映射的内存区间与 buffer 区间有重叠,那就不能再 buffer 中为页表分配内存,此时应该通过 MemBlock 分配内存, can_use_brk_pgt 为 false;其它情况下,can_use_brk_pgt 为 true。

接下来,调用 init_memory_mapping 函数映射 startend 之间的内存。然后,更新 mapped_ram_size 的值,该值指示实际映射的内存大小。最后,将 can_use_brk_pgt 恢复为 true。

循环完成后,从 r_startr_end 范围内的所有可用内存(不包括内存空洞)全部已经映射完成了。最后,返回 mapped_ram_size,即实际映射的内存大小。

5.4.1 clamp_val

clamp_val 宏用于将输出值限定在指定范围内。该函数接收 3 个参数:

  • @val:输入值
  • @min:限定的最小值
  • @max:限定的最大值
// file: include/linux/kernel.h
/**
 * clamp_val - return a value clamped to a given range using val's type
 * @val: current value
 * @min: minimum allowable value
 * @max: maximum allowable value
 *
 * This macro does no typechecking and uses temporary variables of whatever
 * type the input argument 'val' is.  This is useful when val is an unsigned
 * type and min and max are literals that will otherwise be assigned a signed
 * integer type.
 */
#define clamp_val(val, min, max) ({		\
	typeof(val) __val = (val);		\
	typeof(val) __min = (min);		\
	typeof(val) __max = (max);		\
	__val = __val < __min ? __min: __val;	\
	__val > __max ? __max: __val; })

如果 min < val < max,则返回 val 的值;如果 val < min,返回 min;否则,返回 max

六、参考资料

1、Linux Kernel:内存管理之分页(Paging)

2、x86-64架构:内存分页机制

3、Linux Kernel:物理内存布局探测

4、Linux Kernel:启动时内存管理(MemBlock 分配器)