实用指南:Linux内核架构浅谈38-Linux页表结构:四级页表(PGD、PUD、PMD、PTE)的实现解析

1. 页表的核心作用:虚拟地址到物理地址的桥梁

在Linux系统中,CPU通过虚拟地址访问内存,但物理内存的实际寻址依赖物理地址。页表的核心功能就是建立这两种地址之间的映射关系,同时解决两个关键问题:

  • 地址空间隔离:每个进程拥有独立的虚拟地址空间,通过页表确保进程只能访问自身授权的物理内存区域,避免进程间数据干扰。
  • 内存高效利用:通过多级页表(而非单级页表),避免存储大量冗余的地址映射信息(例如32位系统单级页表需1MB空间,而多级页表仅需KB级)。

注意:Linux内核统一采用四级页表结构(PGD → PUD → PMD → PTE),但不同CPU架构可能通过“空页表级”模拟该结构(例如IA-32默认仅用两级页表,PUD和PMD级被简化为“单一项”)。

2. 四级页表的层级结构与地址拆分

虚拟地址会被拆分为5个部分,分别对应四级页表的索引和页内偏移。以64位AMD64架构(实际仅用48位虚拟地址)为例,地址拆分规则如下:

各字段含义如下:

  • PGD索引(9位):用于定位全局页目录(Page Global Directory)中的表项。
  • PUD索引(9位):用于定位上层页目录(Page Upper Directory)中的表项。
  • PMD索引(9位):用于定位中间页目录(Page Middle Directory)中的表项。
  • PTE索引(9位):用于定位页表项(Page Table Entry),最终指向物理页帧。
  • 页内偏移(12位):指定物理页帧内的字节位置(12位对应4KB页大小,2¹²=4096)。
页表层级作用AMD64索引位数管理地址范围
PGD(全局页目录)最高级页表,指向PUD表9512 × 512GB = 256TB
PUD(上层页目录)中间级页表,指向PMD表9512 × 1GB = 512GB
PMD(中间页目录)次中级页表,指向PTE表9512 × 2MB = 1GB
PTE(页表项)最低级页表,指向物理页帧9512 × 4KB = 2MB

表1:四级页表各层级功能与管理范围(AMD64架构)

3. 四级页表的核心数据结构

Linux内核通过4个核心数据结构表示四级页表项,定义在include/asm-x86/pgtable_64.h(以AMD64为例),本质是对unsigned long的封装,确保页表项操作的类型安全性。

3.1 页表项数据结构定义

// AMD64架构页表项定义(简化版)
typedef struct { unsigned long pte; } pte_t;    // 页表项(PTE)
typedef struct { unsigned long pmd; } pmd_t;    // 中间页目录项(PMD)
typedef struct { unsigned long pud; } pud_t;    // 上层页目录项(PUD)
typedef struct { unsigned long pgd; } pgd_t;    // 全局页目录项(PGD)
// 页表项与unsigned long的转换函数
#define pte_val(x)    ((x).pte)
#define __pte(x)      ((pte_t){(x)})
#define pmd_val(x)    ((x).pmd)
#define __pmd(x)      ((pmd_t){(x)})
#define pud_val(x)    ((x).pud)
#define __pud(x)      ((x).pud)
#define pgd_val(x)    ((x).pgd)
#define __pgd(x)      ((x).pgd)

3.2 页表项的关键标志位

页表项(尤其是PTE)包含多个标志位,用于描述物理页帧的属性。以AMD64为例,常用标志位如下:

标志位含义作用场景
_PAGE_PRESENT (0x001)页帧是否在物理内存中缺页异常触发条件之一(未置位则触发缺页)
_PAGE_WRITE (0x002)是否允许写入页帧控制页的可写性(配合权限检查)
_PAGE_USER (0x004)是否允许用户态访问区分内核态/用户态可访问的页
_PAGE_DIRTY (0x040)页帧是否被修改(脏页)页回写时判断是否需要写入磁盘
_PAGE_ACCESSED (0x080)页帧是否被访问过页面回收算法判断页的活跃度
_PAGE_NX (0x8000000000000000)页帧是否不可执行防止代码注入攻击(栈页通常置位)

表2:AMD64架构PTE常用标志位

4. 四级页表的核心操作:创建、查找与销毁

Linux内核提供一系列API用于操作四级页表,以下为关键操作的实现逻辑与示例。

4.1 页表创建:从PGD到PTE的层级创建

创建页表时需从最高级(PGD)到最低级(PTE)逐层分配页表空间,核心函数为pgd_alloc()pud_alloc()pmd_alloc()pte_alloc()

// 简化的页表创建示例(AMD64架构)
#include
#include
// 为虚拟地址addr创建完整的四级页表映射
int create_page_mapping(struct mm_struct *mm, unsigned long addr,
                       phys_addr_t phys_addr, pgprot_t prot) {
    pgd_t *pgd;
    pud_t *pud;
    pmd_t *pmd;
    pte_t *pte;
    unsigned long pfn = phys_addr >> PAGE_SHIFT;
    // 1. 查找或创建PGD表项
    pgd = pgd_offset(mm, addr);  // 根据虚拟地址获取PGD表项指针
    if (pgd_none(*pgd)) {        // 若PGD表项为空,分配新的PUD表
        pud = pud_alloc(mm, pgd, addr);
        if (!pud) return -ENOMEM;
    }
    // 2. 查找或创建PUD表项
    pud = pud_offset(pgd, addr);  // 根据PGD和虚拟地址获取PUD表项指针
    if (pud_none(*pud)) {        // 若PUD表项为空,分配新的PMD表
        pmd = pmd_alloc(mm, pud, addr);
        if (!pmd) return -ENOMEM;
    }
    // 3. 查找或创建PMD表项
    pmd = pmd_offset(pud, addr);  // 根据PUD和虚拟地址获取PMD表项指针
    if (pmd_none(*pmd)) {        // 若PMD表项为空,分配新的PTE表
        pte = pte_alloc_map(mm, pmd, addr);
        if (!pte) return -ENOMEM;
    }
    // 4. 创建PTE表项(映射虚拟地址到物理页帧)
    pte = pte_offset_kernel(pmd, addr);  // 获取PTE表项指针
    if (pte_present(*pte)) {              // 若已存在映射,返回错误
        return -EEXIST;
    }
    set_pte(pte, pfn_pte(pfn, prot));    // 设置PTE表项(物理页帧+属性)
    return 0;
}

4.2 地址映射查找:从虚拟地址到物理地址

查找虚拟地址对应的物理地址时,需逐层遍历四级页表,核心逻辑如下:

// 简化的虚拟地址到物理地址转换(内核态使用)
phys_addr_t virt_to_phys_kernel(unsigned long virt_addr) {
    pgd_t *pgd;
    pud_t *pud;
    pmd_t *pmd;
    pte_t *pte;
    phys_addr_t phys_addr = 0;
    // 1. 遍历PGD
    pgd = pgd_offset_k(virt_addr);  // 内核页表的PGD查找(用户态用pgd_offset(mm, addr))
    if (pgd_none(*pgd) || !pgd_present(*pgd)) {
        return 0;  // PGD表项不存在或页不在内存
    }
    // 2. 遍历PUD
    pud = pud_offset(pgd, virt_addr);
    if (pud_none(*pud) || !pud_present(*pud)) {
        return 0;
    }
    // 3. 遍历PMD
    pmd = pmd_offset(pud, virt_addr);
    if (pmd_none(*pmd) || !pmd_present(*pmd)) {
        return 0;
    }
    // 4. 遍历PTE并计算物理地址
    pte = pte_offset_kernel(pmd, virt_addr);
    if (pte_none(*pte) || !pte_present(*pte)) {
        return 0;
    }
    // 物理地址 = 页帧号 << 页偏移 + 虚拟地址的页内偏移
    phys_addr = (pte_pfn(*pte) << PAGE_SHIFT) | (virt_addr & ~PAGE_MASK);
    return phys_addr;
}

4.3 页表销毁:从PTE到PGD的层级释放

销毁页表时需从最低级(PTE)到最高级(PGD)逐层释放,避免内存泄漏。核心函数为pte_free()pmd_free()pud_free()pgd_free()

// 简化的页表销毁示例(释放虚拟地址addr对应的页表项)
void destroy_page_mapping(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)) return;
    // 2. 查找PUD
    pud = pud_offset(pgd, addr);
    if (pud_none(*pud)) return;
    // 3. 查找PMD
    pmd = pmd_offset(pud, addr);
    if (pmd_none(*pmd)) return;
    // 4. 释放PTE表项并回收页表
    pte = pte_offset_map(mm, pmd, addr);
    if (pte_present(*pte)) {
        pte_clear(mm, addr, pte);  // 清除PTE表项
    }
    pte_unmap(pte);
    pmd_clear(pmd);              // 清除PMD表项
    pmd_free(mm, pmd);           // 释放PMD表
    pud_clear(pud);              // 清除PUD表项
    pud_free(mm, pud);           // 释放PUD表
    pgd_clear(pgd);              // 清除PGD表项
    pgd_free(mm, pgd);           // 释放PGD表
}

5. 架构兼容性:不同CPU的页表适配

Linux内核通过“空页表级”机制,使四级页表结构适配不同CPU架构(如IA-32、ARM、RISC-V)。以下为常见架构的适配方式:

5.1 IA-32架构(32位)

IA-32默认使用两级页表(PGD + PTE),PUD和PMD级被简化为“单一项”,通过宏定义屏蔽层级差异:

// IA-32架构的PUD/PMD简化(include/asm-x86/pgtable_32.h)
#define PTRS_PER_PUD    1  // PUD表仅1项,无实际意义
#define PTRS_PER_PMD    1  // PMD表仅1项,无实际意义
// PUD偏移计算(直接返回PGD表项)
static inline pud_t *pud_offset(pgd_t *pgd, unsigned long address) {
    return (pud_t *)pgd;
}
// PMD偏移计算(直接返回PUD表项)
static inline pmd_t *pmd_offset(pud_t *pud, unsigned long address) {
    return (pmd_t *)pud;
}

5.2 ARM64架构(64位)

ARM64支持三级或四级页表(取决于页大小和虚拟地址位数),Linux内核通过配置选项CONFIG_ARM64_PGTABLE_LEVELS控制层级,例如:

  • 4KB页 + 48位虚拟地址:使用四级页表(PGD → PUD → PMD → PTE)。
  • 64KB页 + 48位虚拟地址:使用三级页表(PGD → PMD → PTE),PUD级被简化。

设计思想:Linux的四级页表结构并非“固定四级”,而是通过“可配置层级”和“空页表级”实现架构无关性,使内核通用代码无需修改即可适配不同CPU。

6. 实战:查看进程页表与性能优化

6.1 查看进程页表信息

Linux提供/proc/[pid]/pagemap接口,用于查看进程虚拟地址对应的物理页帧信息。例如,查看PID为1234的进程中虚拟地址0x7f0000000000的映射:

# 计算虚拟地址的页号(4KB页)
page_num=$((0x7f0000000000 / 4096))
# 读取pagemap文件(每个页表项占8字节)
offset=$((page_num * 8))
dd if=/proc/1234/pagemap of=pageinfo bs=8 count=1 skip=$((offset / 8))
# 解析页表项(提取物理页帧号和标志位)
# 物理页帧号:bit 0-54(AMD64),标志位:bit 55-63
hexdump -C pageinfo

6.2 页表性能优化技巧

  • 大页(Huge Page):使用2MB/1GB大页减少页表层级(例如2MB大页可跳过PTE级,直接通过PMD映射),降低TLB(地址转换后备缓冲器)缺失率。
  • TLB刷新优化:使用invpcid(AMD64)或tlbi(ARM64)指令,精准刷新TLB条目,避免全量TLB刷新导致的性能损耗。
  • 页表缓存:内核通过pgd_cachepte_cache等slab缓存,预分配页表空间,加速页表创建。

7. 总结:四级页表的设计价值

Linux四级页表结构是“架构无关性”与“性能效率”平衡的典范,其核心价值体现在:

  • 灵活性:通过可配置层级适配不同CPU架构,从32位IA-32到64位AMD64/ARM64均无需修改通用代码。
  • 高效性:多级页表减少冗余映射信息,结合TLB和大页机制,降低地址转换的时间开销。
  • 安全性:通过页表项标志位(如_PAGE_USER_PAGE_NX)实现内核态/用户态隔离、执行权限控制,提升系统安全性。

理解四级页表的实现,是深入Linux内存管理(如虚拟内存、页面回收、进程地址空间)的关键基础。后续可进一步研究页表与mm_struct(进程内存描述符)的关联,以及缺页异常处理中的页表修复逻辑。

posted @ 2025-11-10 10:24  gccbuaa  阅读(57)  评论(0)    收藏  举报