Linux 内核的 x86_64 页表使用和缺页异常相关源码分析

以下分析基于 kernel 版本:6.1.121

Linux x86_64 下四级页表的创建、页表内容填充功能和相关结构体分析:

在 kernel 中,/arch/x86/include/asm/pgtable.h 包含了x86架构通用的页表管理接口,包括页表的设置和清除清除函数、页表条目的标志位操作函数、页表条目到物理地址的转换函数,/arch/x86/include/asm/pgtable_64.h 文件包含修改和使用 x86-64 四级页表树所需的函数和定义。另外,二级页表和三级页表的相关函数和定义在同一目录下的pgtable-2level.h 和pgtable-3level.h中。

arch/x86/include/asm/pgtable_types.h 中记录了页表中不同比特位代表的含义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define _PAGE_BIT_PRESENT	0	/* is present */
#define _PAGE_BIT_RW 1 /* writeable */
#define _PAGE_BIT_USER 2 /* userspace addressable */
#define _PAGE_BIT_PWT 3 /* page write through */
#define _PAGE_BIT_PCD 4 /* page cache disabled */
#define _PAGE_BIT_ACCESSED 5 /* was accessed (raised by CPU) */
#define _PAGE_BIT_DIRTY 6 /* was written to (raised by CPU) */
#define _PAGE_BIT_PSE 7 /* 4 MB (or 2MB) page */
#define _PAGE_BIT_PAT 7 /* on 4KB pages */
#define _PAGE_BIT_GLOBAL 8 /* Global TLB entry PPro+ */
#define _PAGE_BIT_SOFTW1 9 /* available for programmer */
#define _PAGE_BIT_SOFTW2 10 /* " */
#define _PAGE_BIT_SOFTW3 11 /* " */
#define _PAGE_BIT_PAT_LARGE 12 /* On 2MB or 1GB pages */
#define _PAGE_BIT_SOFTW4 58 /* available for programmer */
#define _PAGE_BIT_PKEY_BIT0 59 /* Protection Keys, bit 1/4 */
#define _PAGE_BIT_PKEY_BIT1 60 /* Protection Keys, bit 2/4 */
#define _PAGE_BIT_PKEY_BIT2 61 /* Protection Keys, bit 3/4 */
#define _PAGE_BIT_PKEY_BIT3 62 /* Protection Keys, bit 4/4 */
#define _PAGE_BIT_NX 63 /* No execute: only valid after cpuid check */

这些比特位定义在 pgtable.h 中进一步封装成了函数,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static inline int pte_dirty(pte_t pte)
{
return pte_flags(pte) & _PAGE_DIRTY;
}

static inline int pte_young(pte_t pte)
{
return pte_flags(pte) & _PAGE_ACCESSED;
}

static inline int pmd_dirty(pmd_t pmd)
{
return pmd_flags(pmd) & _PAGE_DIRTY;
}

// ......

内核页表的初始化是在 /arch/x86/mm/init_64.c 的 __kernel_physical_mapping_init 函数完成的,该函数用于将物理地址范围映射到虚拟地址范围,并设置相应的页表条目。

函数接受需要映射的物理地址paddr_startpaddr_start、页面大小page_size_mask、页面的保护属性prot和是否在内核初始化时创建的映射init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
static unsigned long __meminit
__kernel_physical_mapping_init(unsigned long paddr_start,
unsigned long paddr_end,
unsigned long page_size_mask,
pgprot_t prot, bool init)
{
bool pgd_changed = false;
unsigned long vaddr, vaddr_start, vaddr_end, vaddr_next, paddr_last;

// 将物理地址转换为虚拟地址
paddr_last = paddr_end;
vaddr = (unsigned long)__va(paddr_start);
vaddr_end = (unsigned long)__va(paddr_end);
vaddr_start = vaddr;

// 遍历虚拟地址范围,设置页表条目
for (; vaddr < vaddr_end; vaddr = vaddr_next) {
pgd_t *pgd = pgd_offset_k(vaddr);
p4d_t *p4d;

// 计算下一个 PGDIR 边界
vaddr_next = (vaddr & PGDIR_MASK) + PGDIR_SIZE;

// 如果 PGD 条目已经存在,初始化 P4D
if (pgd_val(*pgd)) {
p4d = (p4d_t *)pgd_page_vaddr(*pgd);
paddr_last = phys_p4d_init(p4d, __pa(vaddr),
__pa(vaddr_end),
page_size_mask,
prot, init);
continue;
}

// 分配一个新的 P4D 页
p4d = alloc_low_page();
paddr_last = phys_p4d_init(p4d, __pa(vaddr), __pa(vaddr_end),
page_size_mask, prot, init);

// 加锁并填充 PGD 条目
spin_lock(&init_mm.page_table_lock);
if (pgtable_l5_enabled())
pgd_populate_init(&init_mm, pgd, p4d, init);
else
p4d_populate_init(&init_mm, p4d_offset(pgd, vaddr),
(pud_t *) p4d, init);

spin_unlock(&init_mm.page_table_lock);
pgd_changed = true;
}

// 如果 PGD 条目发生变化,同步全局 PGD
if (pgd_changed)
sync_global_pgds(vaddr_start, vaddr_end - 1);

return paddr_last;
}

在内核初始化过程中,prot 传入的是 PAGE_KERNEL,这个宏展开之后是 __PP|__RW| 0|___A|__NX|___D| 0|___G,这些位会被直接设置在页表项上。

缺页异常处理程序和相关结构体分析:

内核定义了一个 swap_info_struct 数据结构,用以描述和管理用于页交换的文件或设备。swap_map 指向一个数组,该数组中每个 unsigned long 即代表盘上(或普通文件中)的一个物理页,数组的下标决定了该页在磁盘或文件中的位置数组的大小存放在 pages 中。

1
2
3
4
5
6
struct swap_info_struct {

unsigned char *swap_map; /* vmalloc'ed array of usage counts */
unsigned int pages; /* total of usable pages of swap */

}

include/linux/swap.h中定义的swap_info_struct数据结构

1
2
3
typedef struct {
unsigned long val;
} swp_entry_t;

include/linux/mm_types.h 中定义的 swap_entry_t 数据结构

swp_entry_t实际上只有一个成员val,其类型为64位无符号整数,当P=1时,PTE里存放的是对应的物理页号及其属性;P=0时,操作系统存放的是swp_entry_t类型的表项,指示出该虚拟页在外存中的位置,包括在哪一个文件或设备,以及在其上的相对位置,从而将硬件忽略不处理的63位充分的利用了起来。

/arch/x86/include/asm/pgtable_64.h 中中定义的与swp_entry_t及其和pte_t有关的操作,注释说明了 x86_64 中 swp_entry_t 的结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/*
* Encode and de-code a swap entry
*
* | ... | 11| 10| 9|8|7|6|5| 4| 3|2| 1|0| <- bit number
* | ... |SW3|SW2|SW1|G|L|D|A|CD|WT|U| W|P| <- bit names
* | TYPE (59-63) | ~OFFSET (9-58) |0|0|X|X| X| E|F|SD|0| <- swp entry
*
* G (8) is aliased and used as a PROT_NONE indicator for
* !present ptes. We need to start storing swap entries above
* there. We also need to avoid using A and D because of an
* erratum where they can be incorrectly set by hardware on
* non-present PTEs.
*
* SD Bits 1-4 are not used in non-present format and available for
* special use described below:
*
* SD (1) in swp entry is used to store soft dirty bit, which helps us
* remember soft dirty over page migration
*
* F (2) in swp entry is used to record when a pagetable is
* writeprotected by userfaultfd WP support.
*
* E (3) in swp entry is used to rememeber PG_anon_exclusive.
*
* Bit 7 in swp entry should be 0 because pmd_present checks not only P,
* but also L and G.
*
* The offset is inverted by a binary not operation to make the high
* physical bits set.
*/
#define SWP_TYPE_BITS 5

#define SWP_OFFSET_FIRST_BIT (_PAGE_BIT_PROTNONE + 1)

/* We always extract/encode the offset by shifting it all the way up, and then down again */
#define SWP_OFFSET_SHIFT (SWP_OFFSET_FIRST_BIT+SWP_TYPE_BITS)

#define MAX_SWAPFILES_CHECK() BUILD_BUG_ON(MAX_SWAPFILES_SHIFT > SWP_TYPE_BITS)

/* Extract the high bits for type */
#define __swp_type(x) ((x).val >> (64 - SWP_TYPE_BITS))

/* Shift up (to get rid of type), then down to get value */
#define __swp_offset(x) (~(x).val << SWP_TYPE_BITS >> SWP_OFFSET_SHIFT)

/*
* Shift the offset up "too far" by TYPE bits, then down again
* The offset is inverted by a binary not operation to make the high
* physical bits set.
*/
#define __swp_entry(type, offset) ((swp_entry_t) { \
(~(unsigned long)(offset) << SWP_OFFSET_SHIFT >> SWP_TYPE_BITS) \
| ((unsigned long)(type) << (64-SWP_TYPE_BITS)) })
#define __pte_to_swp_entry(pte) ((swp_entry_t) { pte_val((pte)) })
#define __pmd_to_swp_entry(pmd) ((swp_entry_t) { pmd_val((pmd)) })

swp_entry_t.valpte_t.val 的类型均为 unsigned long,在 x86_64 编译器中,该类型表示64位无符号整数,对应着一个 x86_64 页表项的结构,所以 __pte_to_swp_entry__pmd_to_swp_entry 可以直接进行类型转换。

在include/linux/swapops.h中,使用上面的宏定义实现了 pte 和 swp_entry 相互转换的函数,由于 linux 的 pte_t 和 swp_entry_t 结构都是 arch-independent 的,这个函数自然也是 arch-independent 的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* Convert the arch-dependent pte representation of a swp_entry_t into an
* arch-independent swp_entry_t.
*/
static inline swp_entry_t pte_to_swp_entry(pte_t pte)
{
swp_entry_t arch_entry;

pte = pte_swp_clear_flags(pte);
arch_entry = __pte_to_swp_entry(pte);
return swp_entry(__swp_type(arch_entry), __swp_offset(arch_entry));
}

/*
* Convert the arch-independent representation of a swp_entry_t into the
* arch-dependent pte representation.
*/
static inline pte_t swp_entry_to_pte(swp_entry_t entry)
{
swp_entry_t arch_entry;

arch_entry = __swp_entry(swp_type(entry), swp_offset(entry));
return __swp_entry_to_pte(arch_entry);
}

这个函数被mm模块中的多个函数调用,如在 /mm/madvise.c 中处理 madvise 系统调用中的 MADV_WILLNEED 建议时,会调用 swapin_walk_pmd_entry 函数,该函数作用是遍历一个 PMD 范围内的 PTE,并尝试将 swap 中的页面异步读入内存中。MADV_WILLNEED 建议内核预取指定范围内的页面,以便将来访问时减少缺页中断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
static int swapin_walk_pmd_entry(pmd_t *pmd, unsigned long start,
unsigned long end, struct mm_walk *walk)
{
struct vm_area_struct *vma = walk->private; // 获取 VMA(虚拟内存区域)结构体
unsigned long index;
struct swap_iocb *splug = NULL; // 用于异步交换 I/O 操作的控制块

if (pmd_none_or_trans_huge_or_clear_bad(pmd))
return 0;

// 遍历指定范围内的 PTE
for (index = start; index != end; index += PAGE_SIZE) {
pte_t pte;
swp_entry_t entry;
struct page *page;
spinlock_t *ptl;
pte_t *ptep;

ptep = pte_offset_map_lock(vma->vm_mm, pmd, index, &ptl);
pte = *ptep;
pte_unmap_unlock(ptep, ptl);

// 检查 PTE 是否为被回收
if (!is_swap_pte(pte))
continue;
entry = pte_to_swp_entry(pte); // 将 PTE 转换为swp_entry
if (unlikely(non_swap_entry(entry)))
continue;

// 异步读取交换缓存中的页面
page = read_swap_cache_async(entry, GFP_HIGHUSER_MOVABLE,
vma, index, false, &splug);
if (page)
put_page(page);
}
swap_read_unplug(splug); // 完成异步交换 I/O 操作

return 0;
}