3. 内核页表数据结构:struct page, pgd_t, pud_t, pmd_t, pte_t 的定义与层级关系

Linux 内核采用四级(或五级)分页机制管理虚拟地址到物理地址的映射。每一级页表对应一个特定的数据类型,这些数据结构共同构成了页表树的骨架。理解 struct page、pgd_t、pud_t、pmd_t、pte_t 的定义及其层级关系,是进行页表分配与回收性能调优的基础。

3.1 页表层级架构总览

现代 64 位处理器(如 x86_64、ARM64)通常支持 4 级分页:PGD → PUD → PMD → PTE。Linux 内核在此基础上抽象出通用的 5 级页表模型(含 P4D 层),但在大多数常见架构中,P4D 被折叠。下图展示了标准的 4 级页表层级关系:

Linux 内核四级页表层级关系图 四级页表层级关系(x86_64 / ARM64) 虚拟地址 全局页目录偏移 (PGD) | PUD 偏移 | PMD 偏移 | PTE 偏移 | 页内偏移 PGD (页全局目录) pgd_t 数组 PUD (页上级目录) pud_t 数组 PMD (页中间目录) pmd_t 数组 PTE pte_t 数组 物理页 说明: • 每一级页表本质上是一个数组,数组元素为对应的页表项类型。 • 虚拟地址被划分为多个位段,分别用于索引不同层级的页表。 • 最后一级 PTE 指向实际的物理页框(struct page)。 • 在 5 级页表中,PGD 与 PUD 之间会插入 P4D 层。

3.2 核心数据结构定义

每个页表项类型本质上是对一个无符号长整型(unsigned long)的封装,目的是提供类型安全和架构抽象。以下是各类型在 x86_64 架构下的典型定义:

// 页表项基础类型:对 unsigned long 的封装
typedef struct { unsigned long pte; } pte_t;
typedef struct { unsigned long pmd; } pmd_t;
typedef struct { unsigned long pud; } pud_t;
typedef struct { unsigned long p4d; } p4d_t;
typedef struct { unsigned long pgd; } pgd_t;

// 页全局目录(PGD)—— 顶层页表
// 每个进程拥有独立的 PGD,指向其完整的地址空间映射
pgd_t *pgd;  // 指向 PGD 页表基地址

// 页上级目录(PUD)—— 第二级
// 由 PGD 中的表项指向
pud_t *pud;

// 页中间目录(PMD)—— 第三级
// 由 PUD 中的表项指向
pmd_t *pmd;

// 页表项(PTE)—— 第四级(最后一级)
// 由 PMD 中的表项指向,PTE 直接映射物理页框
pte_t *pte;
架构差异提示: 在 ARM64 中,页表项的定义与 x86_64 类似,但页表层级数和页面大小配置更灵活(支持 4K/16K/64K 页)。RISC-V 架构则支持 Sv32/Sv39/Sv48 等多种分页模式。内核通过 #ifdef 条件编译和 pgtable.h 头文件实现架构无关的抽象。

3.3 struct page —— 物理页框描述符

struct page 是内核管理物理内存的最小单元描述符,每个物理页框对应一个 struct page 实例。它不直接参与页表遍历,但页表项(PTE)最终指向的物理地址对应的管理结构就是 struct page。

// struct page 核心字段(精简版)
struct page {
    unsigned long flags;          // 页状态标志(PG_locked, PG_dirty 等)
    atomic_t _refcount;           // 引用计数(_refcount)
    atomic_t _mapcount;           // 映射计数(页表映射次数)
    unsigned int private;         // 私有数据(用于文件页、匿名页等)
    struct address_space *mapping; // 指向所属的地址空间
    pgoff_t index;                // 页在文件或交换区中的偏移
    struct list_head lru;         // LRU 链表节点(用于页面回收)
    void *virtual;                // 内核虚拟地址(若映射)
};
数据结构 层级 存储内容 关键操作函数
pgd_t L1 (PGD) 指向 PUD 页表的物理地址 + 权限位 pgd_offset(), pgd_alloc()
pud_t L2 (PUD) 指向 PMD 页表的物理地址 + 权限位 pud_offset(), pud_alloc()
pmd_t L3 (PMD) 指向 PTE 页表的物理地址 + 权限位(或大页映射) pmd_offset(), pmd_alloc()
pte_t L4 (PTE) 物理页框号 (PFN) + 权限位 + 状态位 pte_offset_map(), mk_pte()
struct page 物理页 页状态、引用计数、映射信息、LRU 链表 alloc_pages(), free_pages()

3.4 页表遍历与层级关系

内核通过 pgd_offset() → pud_offset() → pmd_offset() → pte_offset_map() 的调用链完成虚拟地址到物理地址的解析。以下代码展示了典型的页表遍历流程:

// 页表遍历核心逻辑(简化版)
pte_t *walk_page_table(struct mm_struct *mm, unsigned long addr)
{
    pgd_t *pgd;
    pud_t *pud;
    pmd_t *pmd;
    pte_t *pte;

    // 1. 获取 PGD 条目
    pgd = pgd_offset(mm, addr);
    if (pgd_none(*pgd) || pgd_bad(*pgd))
        return NULL;

    // 2. 获取 PUD 条目
    pud = pud_offset(pgd, addr);
    if (pud_none(*pud) || pud_bad(*pud))
        return NULL;

    // 3. 获取 PMD 条目
    pmd = pmd_offset(pud, addr);
    if (pmd_none(*pmd) || pmd_bad(*pmd))
        return NULL;

    // 4. 获取 PTE 条目(最后一级)
    pte = pte_offset_map(pmd, addr);
    if (pte_none(*pte))
        return NULL;

    return pte;
}
性能调优要点: 页表遍历是 TLB 未命中时的慢路径操作。每次遍历需要 4~5 次内存访问(每级页表一次)。优化方向包括:
• 使用大页(HugeTLB / THP)减少页表层级深度。
• 利用 pmd_huge() / pud_huge() 提前终止遍历。
• 在热路径中缓存 pgd 指针,避免重复查找。

3.5 页表项标志位与权限控制

每个页表项的低位存储硬件标志位,控制页面的访问权限、缓存策略和状态。以下是 x86_64 架构下 PTE 的典型标志位布局:

// x86_64 PTE 标志位定义(arch/x86/include/asm/pgtable_types.h)
#define _PAGE_BIT_PRESENT   0   // 页是否在内存中
#define _PAGE_BIT_RW        1   // 可写
#define _PAGE_BIT_USER      2   // 用户态可访问
#define _PAGE_BIT_PWT       3   // Page Write Through
#define _PAGE_BIT_PCD       4   // Page Cache Disable
#define _PAGE_BIT_ACCESSED  5   // 已访问(由硬件置位)
#define _PAGE_BIT_DIRTY     6   // 已脏(由硬件置位)
#define _PAGE_BIT_PSE       7   // Page Size Extension(大页)
#define _PAGE_BIT_GLOBAL    8   // 全局页(不刷 TLB)
#define _PAGE_BIT_SOFTW1    9   // 软件可用位 1
#define _PAGE_BIT_SOFTW2    10  // 软件可用位 2
#define _PAGE_BIT_SOFTW3    11  // 软件可用位 3
#define _PAGE_BIT_PAT       12  // Page Attribute Table
#define _PAGE_BIT_SPECIAL   13  // 特殊页(内核使用)
#define _PAGE_BIT_CPA_TEST  14  // CPA 测试位

这些标志位在每一级页表中都可能出现,但只有 PTE 中的 _PAGE_BIT_PRESENT、_PAGE_BIT_RW、_PAGE_BIT_USER 等对最终访问权限起决定性作用。高层级页表的标志位用于快速过滤(如大页映射时 PMD 直接包含物理地址和权限)。

3.6 层级关系与内存开销

页表层级深度直接影响内存占用和 TLB 命中率。以下表格对比了不同配置下的页表内存开销:

配置 页大小 层级数 单进程页表内存(典型) TLB 覆盖范围
4K 页 + 4 级 4 KB 4 ~8 MB(512GB 地址空间) 小
2MB 大页 (THP) 2 MB 3 (PMD 直接映射) ~4 KB(减少 3 级页表) 中
1GB 大页 (HugeTLB) 1 GB 2 (PUD 直接映射) ~512 字节 大
调优警告: 虽然大页能显著减少页表层级和内存开销,但过度使用大页可能导致内存碎片化(无法找到连续的大块物理内存)。在生产环境中,建议结合 /proc/meminfo 中的 HugePages_Free 和 AnonHugePages 指标动态调整 THP 策略。

3.7 页表分配与回收的入口点

理解数据结构后,需要知道内核在何处分配和回收这些页表:

  • PGD 分配: pgd_alloc() —— 在进程创建(fork())时调用,分配一整个物理页框作为 PGD 页表。
  • PUD/PMD/PTE 分配: pud_alloc() / pmd_alloc() / pte_alloc_map() —— 在缺页异常处理路径中按需分配。
  • 页表回收: pgd_free() / pud_free() / pmd_free() / pte_free() —— 在进程退出或 munmap 时释放。
  • 批量回收: free_pgd_range() —— 用于释放一段虚拟地址区间对应的所有页表层级。