Linux 内核用来逐层构建页表(Page Table)的“建筑工人”,负责将更底层的内存页,挂载到上一级页表的目录项中。根据处理层级的不同,它们会接收不同“零件”(PMD表或PTE页表)并完成“安装”工作。
| 页表层级构建 | ||||||
| pgd_populate() 处理PGD层级挂载PUD/PWD页表 | -> | p4d_populate() 处理P4D层级挂载PUD页表 (仅当启用了五级页表时存在) | -> | pud_populate() 处理PUD层级挂载PMD页表 | -> | pmd_populate_kernel() 处理PMD层级挂载PTE页表 |
它们的核心区别体现在所在的页表层级和操作的数据类型上:
| 函数 | 所属层级 | 主要参数 | 核心作用 |
|---|---|---|---|
p4d_populate | P4D (Page 4th Directory) | p4d_t *p4d,pud_t *pud | 在启用了5级页表的系统中,将PUD页表挂载到P4D目录项中。 |
pgd_populate | PGD (Page Global Directory) | pgd_t *pgd,pud_t *pud | 在未启用5级页表的系统(即4级页表)中,将PUD页表直接挂载到PGD目录项。 |
pud_populate | PUD (Page Upper Directory) | pud_t *pud,pmd_t *pmd | 将PMD页表挂载到PUD目录项中。 |
pmd_populate_kernel | PMD (Page Middle Directory) | pmd_t *pmd,pte_t *pte | 将内核空间的PTE页表挂载到PMD目录项中。 |
函数的两种变体
pmd_populate_kernel和其兄弟函数pmd_populate的主要区别在于处理的数据类型:
pmd_populate_kernel:用于将内核空间分配的PTE页表(一个连续的物理页,pte_t *类型)挂载到PMD项中。pmd_populate:通常用于用户空间进程,它接收的是一个struct page *类型的数据,会先获取该页的物理地址再挂载。
设计考量:统一接口与地址传递
内核社区曾有过一个重要的补丁,为pmd_populate_kernel函数添加了一个vaddr(虚拟地址)参数。这个修改的意义在于:
为架构提供灵活性:某些架构(如ARM64)在填充PMD页表时,需要知道目标虚拟地址(
vaddr)来做出正确决策。避免逆向页表遍历:如果不传递
vaddr,内核可能需要在运行时通过复杂的计算来反查这个地址,这会消耗性能。
因此,这个看似微小的参数传递,实际上是为了让页表的构建过程更高效、更稳健。
pgd_populate 实现原理:
pgd_populate的实现原理可以概括为:获取下级页表(如PUD或P4D)的物理地址,将其与页表项属性标志组合,然后写入到对应的PGD目录项中。
核心实现:封装底层硬件操作
在 x86_64 架构的源码中,pgd_populate通常被实现为一个宏或内联函数,其核心逻辑非常简洁:
static inline void pgd_populate(struct mm_struct *mm, pgd_t *pgd, p4d_t *p4d) { if (!pgtable_l5_enabled()) return; paravirt_alloc_p4d(mm, __pa(p4d) >> PAGE_SHIFT); set_pgd(pgd, __pgd(_PAGE_TABLE | __pa(p4d))); }这个实现可以拆解为三个关键步骤:
获取物理地址 (
__pa(pud)):将指向下级页表(这里以PUD为例)的虚拟地址指针pud转换为物理地址。这是硬件最终需要的地址形式。组合页表项属性 (
_PAGE_TABLE | __pa(pud)):将物理地址与_PAGE_TABLE标志位进行按位或运算。_PAGE_TABLE是一个关键的属性标志,它告诉CPU这个页表项不仅指向物理内存,还包含了“存在”(Present)、“读/写”(Read/Write)等基本权限位,确保这个PGD条目是有效的。写入PGD条目 (
set_pgd):将组合好的完整页表项值写入到pgd指针所指向的PGD目录项中。这是一个体系结构相关的操作,用于将数据写入到指定的内存地址。
实现变体:多层级页表支持
pgd_populate的具体实现会因内核配置的页表层级数而有所不同,这是为了支持从4级到5级页表的平滑过渡。
4级页表 (4-Level Paging):在标准的4级页表中(
CONFIG_PGTABLE_LEVELS=4),PGD直接指向PUD。pgd_populate的实现正如上面定义所示,将一个PUD页表挂载到PGD项中。5级页表 (5-Level Paging):当启用5级页表时(
CONFIG_PGTABLE_LEVELS=5),PGD与PUD之间插入了一个P4D层级。在这种情况下,pgd_populate的实现会相应地变为将一个P4D页表挂载到PGD项中。相关的补丁和讨论也说明了,为了让代码能正确处理这种层级变化,内核引入了如pgd_populate_kernel()这样的封装,并通过宏来确保这些调用在编译时能被正确优化或展开。
设计演进:统一接口与同步保障
pgd_populate的演进,特别是pgd_populate_kernel封装函数的引入,体现了内核开发中统一接口与保障同步的设计思想。
避免“忘记同步”:在早期实现中,每次修改顶层页表(如PGD)后,开发者需要手动调用架构特定的同步函数(如
sync_global_pgds())来将改动同步到其他页表副本。这个过程容易遗漏,曾导致过系统崩溃。封装同步逻辑:为了解决这个问题,内核引入了统一的
pgd_populate_kernel函数。它将实际的pgd_populate操作和必要的页表同步检查打包在一起。其核心逻辑可表示为:#define pgd_populate_kernel(addr, pgd, p4d) \ do { \ pgd_populate(&init_mm, pgd, p4d); \ if (ARCH_PAGE_TABLE_SYNC_MASK & PGTBL_PGD_MODIFIED) \ arch_sync_kernel_mappings(addr, addr); \ } while (0)通过这个封装,顶层页表更新的“填充”和“同步”两个步骤被组合成一个原子操作,极大地降低了因疏忽而引入bug的风险。