3. Linux内核页表管理:struct page、pgd/pmd/pte、页表分配与释放
页表是CPU内存管理单元(MMU)将虚拟地址转换为物理地址的核心数据结构。在Linux内核中,页表管理不仅涉及硬件页表的构建与维护,还涉及物理内存描述符struct page的分配与回收。本章深入剖析多级页表结构、内核页表操作接口以及页表页面的生命周期管理。
3.1 多级页表模型与硬件抽象
Linux内核采用与体系结构无关的页表抽象层,支持4级或5级页表(取决于CONFIG_PGTABLE_LEVELS)。以典型的4级页表为例,虚拟地址被划分为5个域:
| 页表级别 | 内核符号 | 描述 | 典型位数(ARM64 4KB页) |
|---|---|---|---|
| PGD | pgd_t | 页全局目录,顶级页表 | bits[47:39] |
| P4D | p4d_t | 页四级目录(5级页表时存在) | — |
| PUD | pud_t | 页上级目录 | bits[38:30] |
| PMD | pmd_t | 页中间目录 | bits[29:21] |
| PTE | pte_t | 页表项,指向物理页帧 | bits[20:12] |
体系结构适配: 内核通过
pgtable-nop4d.h和pgtable-nopud.h等头文件,在硬件不支持4级或5级页表时自动折叠中间层级,保证上层代码无需修改。
3.2 struct page:物理页面的内核描述符
内核使用struct page描述每个物理页帧(通常为4KB)。该结构体位于include/linux/mm_types.h,是内存管理的最小单元。关键字段如下:
| 字段 | 类型 | 用途 |
|---|---|---|
flags |
unsigned long | 页面状态标志(PG_locked、PG_dirty、PG_swapbacked等) |
_refcount |
atomic_t | 引用计数,决定页面是否可回收 |
_mapcount |
atomic_t | 映射到此页面的PTE数量 |
mapping |
struct address_space * | 指向所属文件映射或匿名映射 |
index |
pgoff_t | 在映射内的页偏移 |
lru |
struct list_head | LRU链表节点,用于页面回收 |
slab_cache |
struct kmem_cache * | SLAB分配器使用(slab页) |
关键概念:
struct page与页表项之间通过page_to_pfn()和pfn_to_page()宏相互转换。每个PTE中存储的物理页帧号(PFN)可直接映射到对应的struct page实例。
3.3 页表分配与释放接口
内核提供了一套体系结构无关的页表分配/释放API,底层调用struct page分配函数。核心接口如下:
- pgd_alloc() / pgd_free() — 分配/释放PGD页表页面。通常每个进程拥有独立的PGD。
- p4d_alloc() / p4d_free() — 分配/释放P4D页表页面(仅5级页表)。
- pud_alloc() / pud_free() — 分配/释放PUD页表页面。
- pmd_alloc() / pmd_free() — 分配/释放PMD页表页面。
- pte_alloc() / pte_free() — 分配/释放PTE页表页面。
这些接口内部通过alloc_pages()分配一个struct page,然后调用__get_free_page()获取虚拟地址,最后将页面清零并初始化页表项格式。
/* 以ARM64为例:分配一个PMD页表页面 */
pmd_t *pmd_alloc_one(struct mm_struct *mm, unsigned long addr)
{
struct page *page;
pmd_t *pmd;
page = alloc_pages(GFP_KERNEL_ACCOUNT, 0);
if (!page)
return NULL;
/* 确保页面引用计数正确 */
if (!pgtable_pmd_page_ctor(page)) {
__free_page(page);
return NULL;
}
pmd = (pmd_t *)page_address(page);
/* 清零页表内容 */
memset(pmd, 0, PAGE_SIZE);
return pmd;
}
注意事项: 页表页面属于内核关键资源,分配时必须使用
GFP_KERNEL_ACCOUNT标志以便计入进程内存账本。释放时需调用pgtable_pmd_page_dtor()清理struct page中的页表计数。
3.4 页表遍历与修改操作
内核提供walk_page_range()和底层宏来遍历进程地址空间的页表。常用操作宏包括:
| 宏/函数 | 功能 |
|---|---|
pgd_offset(mm, addr) |
获取addr在mm的PGD中的索引项 |
pud_offset(p4d, addr) |
从P4D项获取PUD表基址并索引 |
pmd_offset(pud, addr) |
从PUD项获取PMD表基址并索引 |
pte_offset_map(pmd, addr) |
从PMD项获取PTE表基址并映射到线性地址 |
set_pte(ptep, pte) |
写入一个PTE项(含内存屏障) |
ptep_clear_flush(mm, addr, ptep) |
清除PTE并刷TLB |
3.5 页表缓存与TLB协同
页表修改后,CPU的TLB(Translation Lookaside Buffer)可能缓存了旧的转换结果。内核通过以下机制保证一致性:
- 局部刷新:
flush_tlb_page(vma, addr)— 仅刷新指定虚拟地址的TLB项。 - 范围刷新:
flush_tlb_range(vma, start, end)— 刷新一段地址范围的TLB。 - 全局刷新:
flush_tlb_all()— 刷新所有TLB项(开销大,仅在内核页表切换时使用)。 - ASID/PCID: ARM64使用ASID(Address Space ID),x86使用PCID(Process Context ID),避免进程切换时完全刷新TLB。
性能优化: 在SMMU场景中,IOMMU的TLB(IOTLB)与CPU TLB独立。修改设备DMA映射的页表后,必须调用
arm_smmu_tlb_inv_range()等IOMMU特定接口刷新IOTLB,否则设备可能访问到错误的物理内存。
3.6 页表页面生命周期管理
页表页面本身也是物理内存页,其生命周期由struct page的引用计数和_mapcount管理:
| 阶段 | 操作 | 引用计数变化 |
|---|---|---|
| 分配 | alloc_pages() → pgtable_pte_page_ctor() | _refcount = 1, _mapcount = 0 |
| 映射 | set_pte() 建立映射 | _mapcount 增加 |
| 解除映射 | ptep_clear() + flush_tlb() | _mapcount 减少 |
| 释放 | pgtable_pte_page_dtor() → __free_page() | _refcount 归零 |
内核通过pmd_none()、pmd_bad()等宏检测页表项状态,在缺页异常处理中按需分配中间页表。当所有PTE被解除后,PMD/PUD/PGD页面可逐级释放。
/* 释放PTE页表页面的典型路径 */
void pte_free(struct mm_struct *mm, pgtable_t pte)
{
struct page *page = pte;
/* 清理struct page中的页表计数 */
pgtable_pte_page_dtor(page);
/* 归还给伙伴系统 */
__free_page(page);
}
实战要点: 在编写设备驱动或SMMU驱动时,若需要手动建立内核虚拟地址到物理地址的映射(如
ioremap_page_range()),必须显式调用pgd_alloc()→pud_alloc()→pmd_alloc()→pte_alloc()链,并在映射完成后调用flush_tlb_kernel_range()。切勿直接操作pte_t而跳过页表分配。