3. CR3 控制寄存器:分页的“根”与进程切换的“锚点”

CR3 控制寄存器是 x86_64 架构中分页机制的核心寄存器,它保存了当前地址空间页表层次结构的物理基地址。在 Linux 内核中,CR3 的切换直接对应进程的切换——每次切换进程,内核都会加载新进程的 CR3 值,从而将 CPU 的虚拟地址翻译环境切换到新进程的页表。

3.1 CR3 寄存器的位定义

在 x86_64 长模式下,CR3 寄存器的格式与传统的 32 位模式有显著不同。其位定义如下表所示:

位域 宽度 名称 描述
63:12 52 位 物理地址基址 (PML4 Base) 页映射表第 4 级 (PML4) 的物理基地址,页对齐(低 12 位为 0)
11:5 7 位 保留 (Reserved) 必须为 0,否则 #GP 异常
4 1 位 PWT (Page-level Write-Through) 页级写透缓存控制(通常为 0)
3 1 位 PCD (Page-level Cache Disable) 页级缓存禁用(通常为 0)
2:0 3 位 保留 (Reserved) 必须为 0
关键点:CR3 的低 12 位在 x86_64 长模式下被忽略(硬件强制页对齐),因此 PML4 基址实际上使用 52 位物理地址中的高 40 位(位 51:12)。这允许系统支持最多 4 PB 的物理地址空间。

3.2 PML4 基址:四级页表的根节点

CR3 中存储的物理地址指向 PML4 (Page Map Level 4) 表的起始位置。PML4 表是 x86_64 四级页表层次结构的顶层,每个表项 (PML4E) 指向下一级页目录指针表 (PDPT)。

下图展示了 CR3 如何作为根节点连接整个页表层次结构:

CR3 与四级页表层次结构 CR3 寄存器 PML4 物理基址 PML4 表 (Page Map Level 4) 512 个 PML4E,每个指向一个 PDPT PDPT 表 页目录指针表 PDPT 表 页目录指针表 PDPT 表 页目录指针表 PD 表 (页目录) PD 表 (页目录) PD 表 (页目录)
物理地址扩展:虽然 CR3 只存储 PML4 的物理基址,但通过四级页表层次结构,CPU 可以访问整个 48 位虚拟地址空间(256 TB)。每个 PML4E 覆盖 512 GB 的虚拟地址范围。

3.3 CR3 在进程切换中的作用

在 Linux 内核中,进程切换的核心操作之一就是 切换 CR3 寄存器。每个进程拥有独立的地址空间,即独立的页表层次结构。当调度器决定从进程 A 切换到进程 B 时,必须执行以下关键步骤:

  1. 保存当前进程上下文:将当前 CPU 寄存器状态(包括 CR3)保存到进程 A 的 task_struct 中。
  2. 加载新进程页表基址:从进程 B 的 task_struct 中读取其 PML4 物理基址,写入 CR3 寄存器。
  3. TLB 刷新:CR3 写入操作会触发硬件自动刷新整个 TLB(Translation Lookaside Buffer),清除所有缓存的地址翻译条目。
  4. 恢复新进程上下文:加载进程 B 的其他寄存器状态,继续执行。

下图展示了 CR3 切换在进程上下文切换中的位置:

CR3 切换与进程上下文切换流程 进程 A 运行中 CR3 = PML4_A 中断 / 系统调用 进入内核态 调度器 (schedule()) 选择下一个进程 B context_switch() 保存 A 上下文,加载 B 上下文 switch_mm() → 写 CR3 CR3 = PML4_B,TLB 自动刷新
性能陷阱:每次 CR3 切换都会导致 TLB 完全刷新,这对性能有显著影响。Linux 内核通过 ASID (Address Space Identifier) 机制来优化——在支持 ASID 的硬件上,CR3 切换可以保留部分 TLB 条目,避免完全刷新。x86_64 从 Haswell 微架构开始支持 PCID (Process Context Identifier),即 ASID 的 Intel 实现。

3.4 Linux 内核中的 CR3 操作代码

在 Linux 内核源码中,CR3 的读写操作通过内联汇编实现。以下是关键代码片段:

// arch/x86/include/asm/special_insns.h

// 读取 CR3 寄存器
static inline unsigned long read_cr3(void)
{
    unsigned long val;
    asm volatile("mov %%cr3, %0\n\t" : "=r" (val) : : "memory");
    return val;
}

// 写入 CR3 寄存器(触发 TLB 刷新)
static inline void write_cr3(unsigned long val)
{
    asm volatile("mov %0, %%cr3\n\t" : : "r" (val) : "memory");
}

在进程切换的核心函数 switch_mm_irqs_off() 中,CR3 的写入逻辑如下:

// arch/x86/mm/tlb.c

void switch_mm_irqs_off(struct mm_struct *prev, struct mm_struct *next,
                        struct task_struct *tsk)
{
    // ... 省略其他逻辑 ...

    // 加载新进程的页表基址
    write_cr3(__pa(next->pgd));

    // 如果硬件支持 PCID,设置 CR3 的 PCID 位
    // 否则,CR3 写入会刷新所有 TLB 条目
}
内核视角:在 Linux 中,每个进程的 mm_struct 结构体中的 pgd 字段存储了该进程的 PML4 表的虚拟地址。内核通过 __pa() 宏将其转换为物理地址后写入 CR3。所有进程的内核空间页表部分(高地址区域)是共享的,只有用户空间部分(低地址区域)是独立的。

3.5 CR3 切换的硬件行为总结

下表总结了 CR3 切换时硬件自动执行的操作及其影响:

硬件行为 触发条件 影响范围 性能影响
TLB 完全刷新 CR3 写入(无 PCID) 所有 TLB 条目 高(后续翻译需重新遍历页表)
TLB 选择性刷新 CR3 写入(带 PCID) 仅非当前 PCID 的条目 低(保留当前进程的 TLB 条目)
页表遍历重启 CR3 写入后首次访问 当前访问的虚拟地址 中等(需从 PML4 开始逐级查找)
全局页 (Global Page) 保留 CR3 写入(PGE 标志置位) 标记为 Global 的页(如内核空间) 正面(减少内核地址翻译开销)
总结:CR3 寄存器是 x86_64 分页机制的“根锚点”,它决定了 CPU 如何将虚拟地址翻译为物理地址。在 Linux 内核中,CR3 的切换是进程切换的核心操作之一,直接影响 TLB 刷新行为和系统性能。理解 CR3 的位定义、PML4 基址的作用以及切换时的硬件行为,是掌握 x86_64 内存管理的关键一步。