p4d_populate、pgd_populate、pud_populate、pmd_populate_kernel
Linux 内核用来逐层构建页表Page Table的“建筑工人”负责将更底层的内存页挂载到上一级页表的目录项中。根据处理层级的不同它们会接收不同“零件”PMD表或PTE页表并完成“安装”工作。页表层级构建pgd_populate() 处理PGD层级挂载PUD/PWD页表-p4d_populate() 处理P4D层级挂载PUD页表 (仅当启用了五级页表时存在)-pud_populate() 处理PUD层级挂载PMD页表-pmd_populate_kernel() 处理PMD层级挂载PTE页表它们的核心区别体现在所在的页表层级和操作的数据类型上函数所属层级主要参数核心作用p4d_populateP4D (Page 4th Directory)p4d_t *p4d,pud_t *pud在启用了5级页表的系统中将PUD页表挂载到P4D目录项中。pgd_populatePGD (Page Global Directory)pgd_t *pgd,pud_t *pud在未启用5级页表的系统即4级页表中将PUD页表直接挂载到PGD目录项。pud_populatePUD (Page Upper Directory)pud_t *pud,pmd_t *pmd将PMD页表挂载到PUD目录项中。pmd_populate_kernelPMD (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_LEVELS4PGD直接指向PUD。pgd_populate的实现正如上面定义所示将一个PUD页表挂载到PGD项中。5级页表 (5-Level Paging)当启用5级页表时CONFIG_PGTABLE_LEVELS5PGD与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的风险。