前言
主要描述的是Linux 中 x86_64平台下的页表管理,页表用于建立虚拟地址空间和系统物理内存之间的关联。
关于ARM64页表相关知识请参考:
https://zhuanlan.zhihu.com/p/373960777
https://blog.csdn.net/liyuewuwunaile/article/details/108632620
https://blog.csdn.net/u011649400/article/details/105984564
由于页表也会占用物理内存,Linux使用多级页表来完成地址转化,多级页表是一种时间换空间的思想,多级页表会增加物理地址查询时间,但会节约页表地址转换所占用的存放空间。
一、x86_64的分页
AMD64的long mode:long mode页面转换需要使用物理地址扩展 (PAE)。 在激活long mode之前,必须通过将 CR4.PAE 设置为 1 来启用 PAE。在启用 PAE 之前激活long mode,导致发生一般保护异常 (#GP)。
PAE 分页数据结构支持将 64 位虚拟地址映射到 52 位物理地址。 PAE 将传统page-directory entries (PDE) 和page-table entries (PTE) 的大小从 32 位扩展到 64 位,允许物理地址大小大于 32 位。
AMD64 架构通过定义先前为访问和保护控制保留的位来增强page-directory-pointer entry (PDPE)。 一个新的转换表被添加到 PAE 分页中,称为 page-map level-4 (PML4)。 PML4 表在页面转换层次结构中位于 PDP 表之前。
long mode下,物理页的大小可以是4 KB 、2 MB和1GB。
在long mode下,CR3 寄存器用于指向 PML4 基地址。 CR3 在long mode下扩展为 64 位,允许 PML4 表位于 52 位物理地址空间中的任何位置。
Table Base Address Field:位 51:12, 这40 位字段指向 PML4 基地址。 PML4 表在 4 KB 边界上对齐,低 12 位地址位 (11:0) 假定为 0。这产生了 52 位的总基地址大小。 在支持少于完整 52 位物理地址空间的处理器实现上运行的系统软件必须将未实现的高基地址位清除为 0。
物理页为4 KB:
通过将虚拟地址分成六个字段来执行 4 KB 物理页面转换。 其中四个字段用作级别页面翻译层次结构的索引:
位63:48是位47的符号扩展。
位47:39索引到512个条目page-map level-4 table。
位38:30索引到512个条目page-directory pointer table。
位29:21索引到512个条目page-directory table。
位20:12索引到512个条目page table。
位11:0提供到物理页的字节偏移量。
物理页为2MB:
物理页为 1GB:
二、Linux内核中的分页
主要讨论Linux中的四级页表,物理页为4KB的情况。
可以用getconf命令查看当前系统物理页大小情况:
getconf - Query system configuration variables
x86_64中, cr3 寄存器 里面存放当前进程的顶级 pgd,cr3 里面需要存放 pgd 在物理内存的地址,不能是虚拟地址。因为在任务调度context_switch 进行上下文切换 ,内存切换加载cr3寄存器时,里面会使用 __pa,将 mm_struct 里面的成员变量 pgd(mm_struct 里面存的都是虚拟地址)变为物理地址,才能加载到 cr3 里面去。
用户进程在运行的过程中,访问虚拟内存中的数据,会被 cr3 里面指向的页表转换为物理地址后,才在物理内存中访问数据,这个过程都是在用户态运行的,地址转换的过程无需进入内核态。
每个进程都有独立的地址空间,为了这个进程独立完成映射,每个进程都有独立的进程页表,这个页表的最顶级的 pgd 存放在 task_struct 中的 mm_struct 的 pgd 变量里面:
// linux-4.18/include/linux/sched.h
struct task_struct {
......
struct mm_struct *mm;
......
}
// linux-4.18/include/linux/mm_types.h
struct mm_struct {
......
pgd_t * pgd;
......
}
// linux-4.18/arch/x86/include/asm/pgtable_64_types.h
typedef unsigned long pgdval_t;
// linux-4.18/arch/x86/include/asm/pgtable_types.h
typedef struct { pgdval_t pgd; } pgd_t;
pgd_t 使用struct而不是unsigned long基本数据类型表示,以确保页表项的内容只能由相关的辅助函数处理,而不能直接访问pgd_t。使用struct结构体也方便扩展,可以让表项由几个基本类型变量构成。
三、CR3加载PGD
3.1 低版本内核
首先来看一下3.10.1内核版本cr3 寄存器加载 pgd过程,比较简单:
// linux-3.10.1/kernel/sched/core.c
context_switch()
-->switch_mm()
-->//linux-3.10.1/arch/x86/include/asm/mmu_context.h
/* Re-load page tables */
load_cr3(next->pgd);
调用__pa函数将struct mm_struct next->pgd的虚拟地址转化为物理地址:
// linux-3.10.1/arch/x86/include/asm/processor.h
static inline void load_cr3(pgd_t *pgdir)
{
write_cr3(__pa(pgdir));
}
其中__pa函数,其中phys_base = 0(3.10.0的内核版本没有引入kaslr):
#define __START_KERNEL_map _AC(0xffffffff80000000, UL)
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;
}
#define __phys_addr(x) __phys_addr_nodebug(x)
#define __pa(x) __phys_addr((unsigned long)(x))
对于x86_64,3.10.0的内核版本没有引入kaslr:
内核代码段物理地址 = 内核代码段虚拟地址 - __START_KERNEL_map。
关于__pa函数可以参考:Linux物理内存映射
3.2 高版本内核
接下来看4.18.0内核版本,相比较就复杂许多:
(1)其中TLB引入了ASID(Address Space ID),ASID就类似进程ID一样,用来区分不同进程的TLB表项。
这样在进程切换的时候根据ASID的分配情况来flush TLB,Linux内核需要软件管理和分配ASID。
对于x86_64: PCID(进程上下文标识符)
Intel的process context identifier 了解决进程切换时TLB flush导致的性能下降问题。
PCID,Process Context IDentifiers, 即针对每个进程分配专用的ID标识,用于区分TLB中不同进程对应的entry。
(2)引入了kaslr
// linux-4.18/kernel/sched/core.c
context_switch()
-->switch_mm_irqs_off()
-->// linux-4.18/arch/x86/mm/tlb.c
switch_mm_irqs_off()
-->// linux-4.18/arch/x86/mm/tlb.c
load_new_mm_cr3()
-->// linux-4.18/arch/x86/include/asm/mem_encrypt.h
__sme_pa(x)
--> /*
* Caution: many callers of this function expect
* that load_cr3() is serializing and orders TLB
* fills with respect to the mm_cpumask writes.
*/
write_cr3(new_mm_cr3);
其中:
pgd_t *pgd;
unsigned long new_mm_cr3 = __sme_pa(pgd);
/*
* The __sme_pa() and __sme_pa_nodebug() macros are meant for use when
* writing to or comparing values from the cr3 register. Having the
* encryption mask set in cr3 enables the PGD entry to be encrypted and
* avoid special case handling of PGD allocations.
*/
#define __sme_pa(x) (__pa(x) | sme_me_mask)
#define __sme_pa_nodebug(x) (__pa_nodebug(x) | sme_me_mask)
3.3 小结
cr3寄存器,专门用于保存页全局目录的基地址,内核的主内核页全局目录的基地址保存在swapper_pg_dir全局变量中,但需要使用主内核页表时系统会把这个变量的值放入cr3寄存器,进程们自己的页全局目录基地址保存在自己的进程描述符的pgd中(struct task_struct->mm->pgd),当进程切换时,进程的页表也是需要切换的,就是把新的进程的进程描述符的pgd存入cr3中。
3.10.1内核中调用__pa将pgd虚拟地址转化为物理地址加载到cr3中。
4.18.0内核中调用_sme_pa将pgd虚拟地址转化为物理地址加载到cr3中。
参考资料
Linux内核3.10.1
Linux内核4.18.0
AMD官方手册
极客时间:趣谈Linux操作系统
https://fanlv.wiki/2021/07/25/linux-mem/
https://www.cnblogs.com/tolimit/p/4585803.html
https://blog.csdn.net/weixin_45030965/article/details/126995123
https://www.jianshu.com/p/6533f55c4401