我们很容易从一些Linux内核的书籍中知道X86架构使用2级( 10-10-12 )页表,X86-64架构使用4级( 9-9-9-9-12 )页表甚至是5级(在pgd_t与pud_t中间加了一层p4d_t),但是一些隐藏的问题却往往被忽略,如每一个进程的页表存储在内核空间吗?为什么内核中页表所在页框物理地址转化为虚拟地址只需要加个偏置 PAGE_OFFSET?CR3寄存器内容和task_struct->mm->pgd都是全局页表的物理地址吗?一些页表操作函数如pud_offset为什么使用的是经过__va()的地址以及为什么有了MMU还需要这些函数?
首先,如果你忘了多级页表内存寻址的细节,下面这张图可以很快让你回忆起来。
以上以X86-64架构为例描述了一个4级页表,需要注意的是Linux下逻辑地址与虚拟地址是一致的(各个段描述符Base均为0),p**_index()用于计算虚拟地址中每一级相对于页目录/表基址的索引或者偏置,而基址存储在CR3寄存器或者上一级页目录项或者页表项的物理地址字段中。
以下描述了内存寻址的特性:
- 内核给每一个进程分配页表,页表存储在内核空间,当发生进程切换或者其它特定时间时,CR3寄存器装载当前活动进程的全局页表的物理基址。所以后续寻址虚拟地址使用的就是当前进程的页表。
- CR3寄存器写入值时会自动刷新TLB(转换后援缓冲器)表项。
- CR3寄存器存储的是进程页全局目录的物理基址,然而 task_struct->mm->pgd存储的是进程页全局目录的虚拟地址。
- 每一个页目录项或者页表项中的有一个40Bit(视内核版本不同稍有差异)的字段用于存储下一级目录的物理地址,然而如果在内核中要遍历页表,在开启了MMU后,由于不能再使用物理地址,需要使用 "__va(x)" 将物理地址转化为虚拟地址方可寻址,这个过程由MMU来完成。
- 虚拟地址的最低12位(4KB的页大小)和物理地址的最低12位相同。虚拟地址的4个页表段page_index可以看做在页表中的索引。
- 上图中的页操作函数可供我们遍历页表,如通过 "current" 指针就可以得到进程描述符,然后得到内存描述符下的 "pgd" 指针,从而可以得到该虚拟地址对应的物理地址(即上图中最后一层就是 page number + offset 得到物理地址),通过物理地址的前52位可以得到该物理地址所在页的页描述符,因为所有页框的页描述符是数组 mem_map[] 中的元素,数组的线性特性使得通过 page number 得到页描述符变成可能。
- 我们可以使用一个简单的例子做一个 a page table walk:
1 static unsigned long vaddr2paddr(unsigned long vaddr)
2 {
3 pgd_t *pgd;
4 p4d_t *p4d;
5 pud_t *pud;
6 pmd_t *pmd;
7 pte_t *pte;
8 unsigned long paddr = 0;
9 unsigned long page_addr = 0;
10 unsigned long page_offset = 0;
11
12 pgd = pgd_offset(current->mm, vaddr);
13 if (!pgtable_l5_enabled())
14 printk("pgtable_l5 is not enabled\n");
15 p4d = p4d_offset(pgd, vaddr);
16 pud = pud_offset(p4d, vaddr);
17 pmd = pmd_offset(pud, vaddr);
18 pte = pte_offset_kernel(pmd, vaddr);
19 page_addr = pte_val(*pte) & PAGE_MASK;
20 page_offset = vaddr & ~PAGE_MASK;
21 paddr = page_addr | page_offset;
22
23 return paddr;
24 }
1 #include <linux/module.h>
2 #include <linux/init.h>
3 #include <linux/kernel.h>
4 #include <asm/pgtable.h>
5 #include <asm/page.h>
6 #include <linux/sched.h>
7
8 unsigned long vaddr = 0;
9
10 MODULE_LICENSE("GPL");
11 MODULE_AUTHOR("ShieldQiQi");
12 MODULE_DESCRIPTION("Test page table walk");
13
14 static void get_pgtable_macro(void)
15 {
16 printk("PAGE_OFFSET = 0x%lx\n", PAGE_OFFSET);
17 printk("PGDIR_SHIFT = %d\n", PGDIR_SHIFT);
18 printk("P4D_SHIFT = %d\n", P4D_SHIFT);
19 printk("PUD_SHIFT = %d\n", PUD_SHIFT);
20 printk("PMD_SHIFT = %d\n", PMD_SHIFT);
21 printk("PAGE_SHIFT = %d\n", PAGE_SHIFT);
22
23 printk("PTRS_PER_PGD = %d\n", PTRS_PER_PGD);
24 printk("PTRS_PER_P4D = %d\n", PTRS_PER_P4D);
25 printk("PTRS_PER_PUD = %d\n", PTRS_PER_PUD);
26 printk("PTRS_PER_PMD = %d\n", PTRS_PER_PMD);
27 printk("PTRS_PER_PTE = %d\n", PTRS_PER_PTE);
28
29 printk("PAGE_MASK = 0x%lx\n", PAGE_MASK);
30 }
31
32 static unsigned long vaddr2paddr(unsigned long vaddr)
33 {
34 pgd_t *pgd;
35 p4d_t *p4d;
36 pud_t *pud;
37 pmd_t *pmd;
38 pte_t *pte;
39 unsigned long paddr = 0;
40 unsigned long page_addr = 0;
41 unsigned long page_offset = 0;
42
43 pgd = pgd_offset(current->mm, vaddr);
44 printk("current->mm->pgd = 0x%lx\n", (unsigned long)current->mm->pgd);
45 printk("pgd = 0x%lx\n", (unsigned long)pgd);
46 printk("pgd_val = 0x%lx\n", pgd_val(*pgd));
47 printk("pgd_index = %lu\n", pgd_index(vaddr));
48 if (pgd_none(*pgd)) {
49 printk("not mapped in pgd\n");
50 return -1;
51 }
52
53 if (!pgtable_l5_enabled())
54 printk("pgtable_l5 is not enabled\n");
55
56 p4d = p4d_offset(pgd, vaddr);
57 printk("p4d_val = 0x%lx\n", p4d_val(*p4d));
58 printk("p4d_index = %lu\n", p4d_index(vaddr));
59 if (p4d_none(*p4d)) {
60 printk("not mapped in p4d\n");
61 return -1;
62 }
63
64 pud = pud_offset(p4d, vaddr);
65 printk("p4d_pfn_mask = 0x%lx\n", p4d_pfn_mask(*p4d));
66 printk("p4d_page_vaddr = 0x%lx\n", p4d_page_vaddr(*p4d));
67 printk("pud_index = 0x%lx\n", pud_index(vaddr));
68 printk("pud = 0x%lx\n", (unsigned long)pud);
69
70 printk("pud_val = 0x%lx\n", pud_val(*pud));
71 if (pud_none(*pud)) {
72 printk("not mapped in pud\n");
73 return -1;
74 }
75
76 pmd = pmd_offset(pud, vaddr);
77 printk("pmd_val = 0x%lx\n", pmd_val(*pmd));
78 printk("pmd_index = %lu\n", pmd_index(vaddr));
79 printk("pmd = 0x%lx\n", (unsigned long)pmd);
80 if (pmd_none(*pmd)) {
81 printk("not mapped in pmd\n");
82 return -1;
83 }
84
85 pte = pte_offset_kernel(pmd, vaddr);
86 printk("pte = 0x%lx\n", (unsigned long)pte);
87 printk("pte_val = 0x%lx\n", pte_val(*pte));
88 printk("pte_index = %lu\n", pte_index(vaddr));
89 if (pte_none(*pte)) {
90 printk("not mapped in pte\n");
91 return -1;
92 }
93
94 /* Page frame physical address mechanism | offset */
95 page_addr = pte_val(*pte) & PAGE_MASK;
96 page_offset = vaddr & ~PAGE_MASK;
97 paddr = page_addr | page_offset;
98 printk("page_addr = %lx, page_offset = %lx\n", page_addr, page_offset);
99 printk("vaddr = %lx, paddr = %lx\n", vaddr, paddr);
100
101 return paddr;
102 }
103
104 static int __init v2p_init(void)
105 {
106
107 printk("vaddr to paddr module is running..\n");
108 get_pgtable_macro();
109 printk("\n");
110
111 vaddr = (unsigned long)vmalloc(1000 * sizeof(char));
112 if (vaddr == 0) {
113 printk("vmalloc failed..\n");
114 return 0;
115 }
116 printk("vmalloc_vaddr=0x%lx\n", vaddr);
117 vaddr2paddr(vaddr);
118 vfree((void *)vaddr);
119
120 printk("\n\n");
121 vaddr = __get_free_page(GFP_KERNEL);
122 if (vaddr == 0) {
123 printk("__get_free_page failed..\n");
124 return 0;
125 }
126 printk("get_page_vaddr=0x%lx\n", vaddr);
127 vaddr2paddr(vaddr);
128 free_page(vaddr);
129
130 return 0;
131 }
132
133 static void __exit v2p_exit(void)
134 {
135 printk("vaddr to paddr module is leaving..\n");
136 }
137
138 module_init(v2p_init);
139 module_exit(v2p_exit);
- 如果你深入的看 "p**_offset" 是如何实现的就会发现,它由一个当前页表所在页框的虚拟地址加上 "p**_index" 得到,这里有一个误区,在Linux内核5.4.0中, "p**_offset" 实现如下:
1 static inline unsigned long pud_page_vaddr(pud_t pud)
2 {
3 return (unsigned long)__va(pud_val(pud) & pud_pfn_mask(pud));
4 }
5
6 /*
7 * Currently stuck as a macro due to indirect forward reference to
8 * linux/mmzone.h's __section_mem_map_addr() definition:
9 */
10 #define pud_page(pud) pfn_to_page(pud_pfn(pud))
11
12 /* Find an entry in the second-level page table.. */
13 static inline pmd_t *pmd_offset(pud_t *pud, unsigned long address)
14 {
15 return (pmd_t *)pud_page_vaddr(*pud) + pmd_index(address);
16 }
- 可以看到 "pmd_offset" 最终返回的是__va()之后的虚拟地址,所以函数体内的入参 "pud_t *pud" 其实也是虚拟地址,函数 "pud_page_vaddr"用于取出页上级目录项存储的内容,可以看到使用的是取地址符号 " * ",看到这里有些小伙伴可能会问为什么已经知道的物理地址,还要使用经过__va()得到的虚拟地址 "pud" 呢,甚至可能会说我们现在做的不就是虚拟地址转化为物理地址吗,那么怎么可以直接使用虚拟地址呢?其实,当我们用到这些宏的时候,系统早已正常工作,cpu所处理的一切地址都应该是虚拟地址,转化的事情就交给MMU好了,之所以使用虚拟地址,是因为只能使用虚拟地址。
- 上述代码通过
page_offset = vaddr & ~PAGE_MASK
- 得到的物理地址其实是不对的,因为只有中间的40位才是物理地址 page number
[598341.980621] vaddr to paddr module is running..
[598341.980622] PAGE_OFFSET = 0xffff90a240000000
[598341.980623] PGDIR_SHIFT = 39
[598341.980623] P4D_SHIFT = 39
[598341.980623] PUD_SHIFT = 30
[598341.980624] PMD_SHIFT = 21
[598341.980624] PAGE_SHIFT = 12
[598341.980624] PTRS_PER_PGD = 512
[598341.980624] PTRS_PER_P4D = 1
[598341.980625] PTRS_PER_PUD = 512
[598341.980625] PTRS_PER_PMD = 512
[598341.980625] PTRS_PER_PTE = 512
[598341.980626] PAGE_MASK = 0xfffffffffffff000
[598341.980628] vmalloc_vaddr=0xffffbaddc02cb000
[598341.980628] current->mm->pgd = 0xffff90a3c83e4000
[598341.980629] pgd = 0xffff90a3c83e4ba8
[598341.980629] pgd_val = 0x2b5155067
[598341.980629] pgd_index = 373
[598341.980630] pgtable_l5 is not enabled
[598341.980630] p4d_val = 0x2b5155067
[598341.980630] p4d_index = 0
[598341.980631] p4d_pfn_mask = 0xffffffffff000
[598341.980631] p4d_page_vaddr = 0xffff90a4f5155000
[598341.980631] pud_index = 0x177
[598341.980631] pud = 0xffff90a4f5155bb8
[598341.980632] pud_val = 0x2b5158067
[598341.980632] pmd_val = 0x2b4a9e067
[598341.980632] pmd_index = 1
[598341.980633] pmd = 0xffff90a4f5158008
[598341.980633] pte = 0xffff90a4f4a9e658
[598341.980633] pte_val = 0x8000000204610063
[598341.980634] pte_index = 203
[598341.980634] page_addr = 8000000204610000, page_offset = 0
[598341.980634] vaddr = ffffbaddc02cb000, paddr = 8000000204610000
[598341.980635]
[598341.980635] get_page_vaddr=0xffff90a444610000
[598341.980636] current->mm->pgd = 0xffff90a3c83e4000
[598341.980636] pgd = 0xffff90a3c83e4908
[598341.980636] pgd_val = 0x1cfe01067
[598341.980636] pgd_index = 289
[598341.980637] pgtable_l5 is not enabled
[598341.980637] p4d_val = 0x1cfe01067
[598341.980637] p4d_index = 0
[598341.980638] p4d_pfn_mask = 0xffffffffff000
[598341.980638] p4d_page_vaddr = 0xffff90a40fe01000
[598341.980638] pud_index = 0x91
[598341.980638] pud = 0xffff90a40fe01488
[598341.980639] pud_val = 0x24baa9063
[598341.980639] pmd_val = 0x204680063
[598341.980639] pmd_index = 35
[598341.980640] pmd = 0xffff90a48baa9118
[598341.980640] pte = 0xffff90a444680080
[598341.980640] pte_val = 0x8000000204610063
[598341.980640] pte_index = 16
[598341.980641] page_addr = 8000000204610000, page_offset = 0
[598341.980641] vaddr = ffff90a444610000, paddr = 8000000204610000
[598346.531714] vaddr to paddr module is leaving..
深入理解Linux内核页表管理(Page Table Management)_linux page table-CSDN博客
https://www.kernel.org/doc/gorman/html/understand/understand006.html
======= 以下是博主自己添加的 =======
eg,qemu 进 程 pgd 页 表 (4k 空 间 大 小 ) 中 512 个 entry(每 个 8字 节 ) 的 内 容 , 未 显 示 的 entry 表 示 没 有 映 射 ( 内 容 为 0)。可 见 qemu 总 共 映 射 了 最 多 10 * 512G( 4级 9pgd-9pud-9pmd-9pte-12 页 表 , 一 个 entry 映 射 512G: 4K * 512 * 512 * 512) 。
0x67 = 0b01100111