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 只存储 PML4 的物理基址,但通过四级页表层次结构,CPU 可以访问整个 48 位虚拟地址空间(256 TB)。每个 PML4E 覆盖 512 GB 的虚拟地址范围。
3.3 CR3 在进程切换中的作用
在 Linux 内核中,进程切换的核心操作之一就是 切换 CR3 寄存器。每个进程拥有独立的地址空间,即独立的页表层次结构。当调度器决定从进程 A 切换到进程 B 时,必须执行以下关键步骤:
- 保存当前进程上下文:将当前 CPU 寄存器状态(包括 CR3)保存到进程 A 的 task_struct 中。
- 加载新进程页表基址:从进程 B 的 task_struct 中读取其 PML4 物理基址,写入 CR3 寄存器。
- TLB 刷新:CR3 写入操作会触发硬件自动刷新整个 TLB(Translation Lookaside Buffer),清除所有缓存的地址翻译条目。
- 恢复新进程上下文:加载进程 B 的其他寄存器状态,继续执行。
下图展示了 CR3 切换在进程上下文切换中的位置:
性能陷阱:每次 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 内存管理的关键一步。