Linux 页表机制详解(x86_64 架构)

前言

在 AI 计算和推理加速的内核驱动开发中,高效的内存管理是构建高性能计算系统的核心基础。无论是 GPU 显存映射、DMA 缓冲区管理,还是大规模张量数据的虚拟地址分配,都离不开对页表机制的深入理解。本文系统梳理 x86_64 架构下 Linux 的四级页表机制,为 AI 计算相关的内核驱动开发提供理论基础和实践参考。

1. 概述

在 x86_64 架构的 Linux 系统中,页表采用四级分页模型来实现虚拟地址到物理地址的转换。虽然现代硬件支持五级页表,但 Linux 默认启用四级分页,以多级页表项的形式分层存储在物理内存中。

2. 四级页表结构

Linux 对 x86_64 的虚拟地址(默认 48 位有效地址)进行分段,依次索引四级页表, 页表层级说明如下:

页表层级 全称 索引位宽 虚拟地址位段 作用
PGD 页全局目录 (Page Global Directory) 9 位 47~39 索引 PGD 项,指向 PUD 页的物理地址
PUD 页上级目录 (Page Upper Directory) 9 位 38~30 索引 PUD 项,指向 PMD 页的物理地址
PMD 页中间目录 (Page Middle Directory) 9 位 29~21 索引 PMD 项,指向 PT 页或 2MB 大页面
PT 页表 (Page Table) 9 位 20~12 索引 PT 项,指向 4KB 物理页面
页内偏移 - 12 位 11~0 物理页面内的字节偏移

2.1. 各级页表详解

  1. PGD(页全局目录)

    • 进程的 mm_struct 中保存 PGD 物理地址
    • 虚拟地址最高 9 位索引 PGD 项
    • 指向 PUD 页的物理地址
  2. PUD(页上级目录)

    • 中间层级,9 位索引 PUD 项
    • 指向 PMD 页物理地址
    • 在 4KB 页面且地址范围较小时常与 PGD 合并
  3. PMD(页中间目录)

    • 9 位索引 PMD 项
    • 可指向 PT 页物理地址
    • 或直接映射 2MB 大页面(此时跳过 PT 层级)
  4. PT(页表)

    • 最低层级,9 位索引 PT 项
    • 直接指向 4KB 物理页面的起始地址
    • 虚拟地址最低 12 位为页内偏移量

3. 页表项(PTE)存储格式

每个页表项占 8 字节(64 位),包含两部分核心信息:

复制代码
页表项(64 位 / 8 字节)结构:
┌────────────────────────────────────────────────────────────────┐
│ 63                                                           0 │
├──────────────────────────────────────────┬─────────────────────┤
│    物理页面基地址 (52 位)                   │  标志位 (12 位)      │
│    [63:12]                               │  [11:0]             │
└──────────────────────────────────────────┴─────────────────────┘

详细位段分布:
┌──┬──┬──┬──┬──────────────────────────────────┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│N │PK│PK│PK│                                  │  │P │  │  │P │P │U │R │  │  │  │P │
│X │3 │2 │1 │      物理页面基地址                │G │A │D │A │C │W │/ │/ │  │  │  │  │
│  │  │  │  │  (Physical Page Base Address)    │  │T │  │  │D │T │S │W │  │  │  │  │
│  │  │  │  │                                  │  │  │  │  │  │  │  │  │  │  │  │  │
├──┼──┼──┼──┼──────────────────────────────────┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤
│63│62│61│60│59                              12│11│10│ 9│ 8│ 7│ 6│ 5│ 4│ 3│ 2│ 1│ 0│
└──┴──┴──┴──┴──────────────────────────────────┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘

3.1. 物理地址部分(位 63-12)

  • 高 52 位:目标物理页的基地址
  • 物理地址仅低 52 位有效,由 CPU 架构决定
  • 由于页面按 4KB 对齐,低 12 位始终为 0,因此不需要存储

3.2. 权限与状态标志位(位 11-0)

标志位 名称 作用
0 P 存在位 (Present) 标记页是否在物理内存中
1 R/W 读写权限 (Read/Write) 0=只读,1=可读写
2 U/S 用户/内核空间 (User/Supervisor) 0=仅内核态,1=用户态可访问
3 PWT 写通 (Page Write-Through) 控制写缓存策略
4 PCD 禁用缓存 (Page Cache Disable) 1=禁用页面缓存
5 A 访问位 (Accessed) 标记页是否被访问过
6 D 脏位 (Dirty) 标记页是否被修改过
7 PAT 页面属性 (Page Attribute Table) 与 PWT/PCD 配合控制缓存
8 G 全局位 (Global) 1=全局页,TLB 刷新时保留
9-11 AVL 可用位 (Available) 操作系统自定义使用
63 NX 禁止执行 (No eXecute) 1=禁止从该页执行代码

4. 页表空间占用

在 x86_64 架构的 Linux 四级分页模型中,每级页表默认占用 4KB 物理内存

4.1. 空间计算逻辑

  • 每个页表项(PTE/PMD/PUD/PGD 项)占 8 字节
  • 一个 4KB 页面可容纳:4096 ÷ 8 = 512 个页表项
  • 虚拟地址中每级页表的索引位宽为 9 位, 2^9 = 512,刚好匹配一个 4KB 页面能存储的页表项数量

因此,PGD、PUD、PMD、PT 各级页表只要独立存在,单级占用空间都是 4KB

4.2. 特殊情况

  1. 2MB 大页面

    • PMD 项直接映射物理大页
    • 跳过 PT 层级,节省 4KB 的 PT 页空间
  2. 1GB 超大页面

    • PUD 项直接映射物理超大页
    • 跳过 PMD 和 PT 层级,节省两级页表空间
  3. 地址范围较小时

    • PUD 层会与 PGD 层合并
    • PUD 无需单独占用 4KB 空间

5. 内核与用户页表隔离

5.1. 用户进程页表

  • 每个进程有独立的 PGD
  • 用户空间(0x0000000000000000 ~ 0x00007FFFFFFFFFFF)的页表项由进程私有
  • 进程切换时只需切换 CR3 寄存器指向新进程的 PGD 物理地址

5.2. 内核页表

  • 内核空间(0xFFFF800000000000 及以上)的页表项在所有进程的 PGD/PUD/PMD 中共享
  • 实现内核地址空间的全局映射
  • 避免每次进程切换时重新映射内核空间

6. 硬件关联与管理

6.1. 硬件支持

  • CR3 寄存器:存储当前进程 PGD 的物理地址
  • CPU 地址转换:自动遍历四级页表完成虚拟地址到物理地址的转换
  • TLB(Translation Lookaside Buffer):缓存虚拟地址到物理地址的映射,减少页表遍历开销

6.2. 内核管理

6.2.1. 关键数据结构
c 复制代码
/* 进程内存描述符 - 定义在 include/linux/mm_types.h */
struct mm_struct {
    pgd_t *pgd;                    /* 指向进程的页全局目录(页表根) */
    /* ... 更多字段 ... */
};

/* 页表项类型定义 - 定义在 arch/x86/include/asm/pgtable_types.h */
//所有的页表项都是8字节的一个定义
typedef struct { unsigned long pgd; } pgd_t;    /* 页全局目录项 */
typedef struct { unsigned long pud; } pud_t;    /* 页上级目录项 */
typedef struct { unsigned long pmd; } pmd_t;    /* 页中间目录项 */
typedef struct { unsigned long pte; } pte_t;    /* 页表项 */
6.2.2. 关键管理函数

页表分配函数

c 复制代码
/* 分配各级页表 */
pgd_t *pgd_alloc(struct mm_struct *mm);          /* 分配页全局目录 */
int __pud_alloc(struct mm_struct *mm,
         pgd_t *pgd, unsigned long address);
int __pmd_alloc(struct mm_struct *mm,
         pud_t *pud, unsigned long address);
int __pte_alloc(struct mm_struct *mm, pmd_t *pmd);

/* 页面分配 */
struct page *alloc_pages(gfp_t gfp_mask,
                    unsigned int order);
void __free_pages(struct page *page, unsigned int order);

页表遍历与查找函数

c 复制代码
/* 根据虚拟地址查找各级页表项 */
pgd_t *pgd_offset(struct mm_struct *mm, unsigned long address);
pud_t *pud_offset(pgd_t *pgd, unsigned long address);
pmd_t *pmd_offset(pud_t *pud, unsigned long address);
pte_t *pte_offset_map(pmd_t *pmd, unsigned long address);

/* 页表项状态检查 */
int pte_present(pte_t pte);     /* 检查页是否在内存中 */
int pte_young(pte_t pte);       /* 检查访问位 */
int pte_dirty(pte_t pte);       /* 检查脏位 */
int pte_write(pte_t pte);       /* 检查写权限 */

页表项修改函数

c 复制代码
/* 设置页表项 */
void set_pte(pte_t *ptep, pte_t pte);
void set_pmd(pmd_t *pmdp, pmd_t pmd);
void set_pud(pud_t *pudp, pud_t pud);
void set_pgd(pgd_t *pgdp, pgd_t pgd);

/* 页表项属性修改 */
pte_t pte_mkwrite(pte_t pte);   /* 设置可写 */
pte_t pte_wrprotect(pte_t pte); /* 设置写保护 */
pte_t pte_mkdirty(pte_t pte);   /* 设置脏位 */
pte_t pte_mkyoung(pte_t pte);   /* 设置访问位 */

页表释放函数

c 复制代码
/* 释放各级页表 */
void pgd_free(struct mm_struct *mm, pgd_t *pgd);
void pud_free(struct mm_struct *mm, pud_t *pud);
void pmd_free(struct mm_struct *mm, pmd_t *pmd);
void pte_free(struct mm_struct *mm, pgtable_t pte);

TLB 管理函数

c 复制代码
/* TLB 刷新操作 */
/* 刷新所有 CPU 的 TLB */
void flush_tlb_all(void);
/* 刷新指定进程的 TLB */
void flush_tlb_mm(struct mm_struct *mm);
void flush_tlb_page(struct vm_area_struct *vma,
             unsigned long addr);
void flush_tlb_range(struct vm_area_struct *vma, 
             unsigned long start, unsigned long end);

7. 地址转换实例

7.1. 示例参数设定

假设需要转换虚拟地址:0x0000 1234 5678 ABCD

已知条件:

  • 进程 PGD 物理地址:0x100000
  • 页表项标志位:P=1(存在)、RW=1(可读写)
  • 高 52 位为物理地址基址

7.2. 步骤 1:拆分虚拟地址位段

将 48 位虚拟地址 0x000012345678ABCD 按位段拆分:

复制代码
虚拟地址:0x0000 1234 5678 ABCD
         ↓     ↓    ↓    ↓
二进制:  0000 0000 0000 0001 0010 0011 0100 0101 0110 0111 1000 1010 1011 1100 1101

位段拆分:
├─ PGD 索引 [47:39]:0x000 (十进制: 0)
├─ PUD 索引 [38:30]:0x024 (十进制: 36)
├─ PMD 索引 [29:21]:0x08D (十进制: 141)
├─ PT 索引  [20:12]:0x171 (十进制: 369)
└─ 页内偏移 [11:0] :0xBCD (十进制: 3021)

7.3. 步骤 2:四级页表逐级寻址

第一级:寻址 PGD 项
复制代码
PGD 物理地址 = 0x100000
PGD 索引 = 0x000
PGD 项物理地址 = 0x100000 + (0x000 × 8) = 0x100000

假设 PGD 项值 = 0x110000_000000067
└─ 高 52 位 0x110000 → PUD 页的物理地址
第二级:寻址 PUD 项
复制代码
PUD 页物理地址 = 0x110000
PUD 索引 = 0x024
PUD 项物理地址 = 0x110000 + (0x024 × 8) = 0x110120

假设 PUD 项值 = 0x120000_000000067
└─ 高 52 位 0x120000 → PMD 页的物理地址
第三级:寻址 PMD 项
复制代码
PMD 页物理地址 = 0x120000
PMD 索引 = 0x08D
PMD 项物理地址 = 0x120000 + (0x08D × 8) = 0x120438

假设 PMD 项值 = 0x130000_000000067
└─ 高 52 位 0x130000 → PT 页的物理地址
第四级:寻址 PT 项
复制代码
PT 页物理地址 = 0x130000
PT 索引 = 0x171
PT 项物理地址 = 0x130000 + (0x171 × 8) = 0x130888

假设 PT 项值 = 0x140000_000000067
└─ 高 52 位 0x140000 → 最终物理页面的基地址

7.4. 步骤 3:计算最终物理地址

复制代码
物理地址 = 物理页面基地址 + 页内偏移
        = 0x140000 + 0xBCD
        = 0x140BCD

7.5. 转换流程图

复制代码
虚拟地址: 0x000012345678ABCD
    ↓
┌─────────────────────────────────────┐
│ CR3 寄存器: 0x100000 (PGD 基址)       │
└─────────────────────────────────────┘
    ↓ [PGD 索引: 0x000]
┌─────────────────────────────────────┐
│ PGD[0] = 0x110000_000000067         │
│ → PUD 基址: 0x110000                 │
└─────────────────────────────────────┘
    ↓ [PUD 索引: 0x024]
┌─────────────────────────────────────┐
│ PUD[36] = 0x120000_000000067        │
│ → PMD 基址: 0x120000                 │
└─────────────────────────────────────┘
    ↓ [PMD 索引: 0x08D]
┌─────────────────────────────────────┐
│ PMD[141] = 0x130000_000000067       │
│ → PT 基址: 0x130000                  │
└─────────────────────────────────────┘
    ↓ [PT 索引: 0x171]
┌─────────────────────────────────────┐
│ PT[369] = 0x140000_000000067        │
│ → 物理页基址: 0x140000                │
└─────────────────────────────────────┘
    ↓ [页内偏移: 0xBCD]
┌─────────────────────────────────────┐
│ 最终物理地址: 0x140BCD                │
└─────────────────────────────────────┘

8. 性能优化机制

8.1. TLB 缓存

  • TLB 是 CPU 内部的高速缓存,存储最近使用的虚拟地址到物理地址的映射
  • 命中 TLB 时无需遍历四级页表,大幅提升地址转换速度
  • TLB 失效时才触发页表遍历(Page Table Walk)

8.2. 大页面支持

  • 2MB 大页面:减少页表层级,降低 TLB 压力
  • 1GB 超大页面:适用于大内存应用,进一步减少页表开销
  • 通过 hugetlbfs 文件系统或 mmapMAP_HUGETLB 标志使用

8.3. 页表缓存

  • 内核维护页表缓存池,避免频繁分配和释放页表页
  • 使用 slab 分配器管理页表项

9. 总结

Linux 的四级页表机制通过分层索引的方式,实现了灵活、高效的虚拟地址到物理地址的转换。整个过程由 CPU 硬件自动完成,配合 TLB 缓存和大页面支持,在保证内存隔离和保护的同时,提供了出色的性能表现。

关键要点:

  1. 四级页表结构:PGD → PUD → PMD → PT → 物理页
  2. 每级页表占用 4KB 物理内存,每个页表项 8 字节
  3. 48 位虚拟地址分段索引,12 位页内偏移
  4. CR3 寄存器存储 PGD 基址,TLB 缓存加速转换
  5. 支持大页面优化,实现进程隔离和内存保护

这套机制是现代操作系统内存管理的基石,为应用程序提供了统一、安全、高效的内存访问接口。

相关推荐
Doro再努力11 分钟前
【Linux操作系统10】Makefile深度解析:从依赖推导到有效编译
android·linux·运维·服务器·编辑器·vim
senijusene16 分钟前
Linux软件编程:IO编程,标准IO(1)
linux·运维·服务器
忧郁的橙子.23 分钟前
02-本地部署Ollama、Python
linux·运维·服务器
醇氧32 分钟前
【linux】查看发行版信息
linux·运维·服务器
No8g攻城狮1 小时前
【Linux】Windows11 安装 WSL2 并运行 Ubuntu 22.04 详细操作步骤
linux·运维·ubuntu
XiaoFan0122 小时前
免密批量抓取日志并集中输出
java·linux·服务器
souyuanzhanvip2 小时前
ServerBox v1.0.1316 跨平台 Linux 服务器管理工具
linux·运维·服务器
HalvmånEver3 小时前
Linux:线程互斥
java·linux·运维
番茄灭世神3 小时前
Linux应用编程介绍
linux·嵌入式
wdfk_prog3 小时前
[Linux]学习笔记系列 -- [drivers][mmc][mmc_sdio]
linux·笔记·学习