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 级页表层级关系:
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;
#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;
}
• 使用大页(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()—— 用于释放一段虚拟地址区间对应的所有页表层级。