每个进程通过一个指针(即进程的mm_struct→pgd)指向其专属的页全局目录(PGD),该目录本身存储在一个物理页框中。这个页框包含一个类型为pgd_t的数组,该类型是与架构相关的数据结构,定义在<asm/page.h>头文件中。页表的加载方式因架构而异:在x86架构中,进程页表通过将mm_struct→pgd复制到cr3寄存器来加载,这一操作会附带刷新TLB(事实上,这正是架构相关代码中flush_tlb()函数的实现原理)。
PGD表中的每个有效表项指向一个物理页框,其中包含类型为pmd_t的页中间目录(PMD)条目数组。每个PMD条目进一步指向另一个页框,其中包含类型为pte_t的页表项(PTE),而这些PTE最终指向存储实际用户数据的物理页框。若页面已被换出到后备存储,交换条目将存储在PTE中,并由页错误处理例程do_swap_page()使用,以定位包含页面数据的交换条目。页表布局如图所示。
任意给定的线性地址可被分解为三部分:三个页表层级内的偏移量以及实际页面内的偏移量。为辅助地址分解,系统为每个页表层级提供了一组三元宏定义——SHIFT(XX_SHIFT表示XX段的bit位数)、SIZE(XX_SIZE表示XX段的大小,一般等于2^XX_SHIF)和MASK(XX_MASK表示从线性地址中获取XX段的掩码,XX段以外的bit位都是0)。其中,SHIFT宏定义了该层级页表映射的地址位长度
MASK宏可通过与线性地址进行按位与(AND)操作,屏蔽掉所有高位比特,常用于判断地址是否在页表某一层级对齐。SIZE宏则揭示了每个层级的表项所覆盖的字节范围。SIZE与MASK宏的关系如图3.3所示。在这组三元宏中,仅需定义SHIFT值,其余两者可通过计算得出。例如,x86架构的页面层级宏定义如下:
#define PAGE_SHIFT 12
#define PAGE_SIZE (1UL << PAGE_SHIFT)
#define PAGE_MASK (~(PAGE_SIZE-1))
其中:
- PAGE_SHIFT表示线性地址中页内偏移部分的比特长度(x86为12位)
- PAGE_SIZE通过2^PAGE_SHIFT计算得出(即页大小)
- PAGE_MASK通过对PAGE_SIZE-1取反得到,用于对齐页边界时清零偏移位。若需强制地址按页对齐,可使用PAGE_ALIGN()宏,其原理是先将地址加上PAGE_SIZE-1,再与PAGE_MASK按位与操作以消除页偏移比特。
类似地:
- PMD_SHIFT表示页中间目录(PMD)层级映射的地址比特数,其对应的PMD_SIZE与PMD_MASK通过相同方式计算。
- PGDIR_SHIFT表示页全局目录(PGD)顶层映射的地址比特数,PGDIR_SIZE和PGDIR_MASK遵循相同规则。
最后,关键的三个宏是PTRS_PER_*系列,它们定义了各层级页表的表项数量:
- PTRS_PER_PGD表示PGD中的指针数量(x86无PAE时为1,024)
- PTRS_PER_PMD对应PMD层(x86无PAE时为1)
- PTRS_PER_PTE对应底层PTE(x86无PAE时为1,024)
这些宏共同构建了Linux页表体系的核心寻址机制。