从虚拟地址到物理页框:Linux 页表与内存管理全解析
(以 x86_64 架构为主,2025–2026 年主流 Linux 内核视角)
Linux 内存管理的核心目标:隔离进程 + 高效利用物理内存 + 按需分配 + 保护。实现这一切的关键机制就是分页(Paging) + 多级页表 + MMU(Memory Management Unit)硬件。
1. 为什么需要虚拟地址?
- 物理内存地址(PA)是全局的,所有进程共享。
- 虚拟地址(VA)让每个进程认为自己独占整个地址空间(通常 0 ~ 2⁶⁴-1)。
- 好处:
- 进程隔离(一个进程无法直接访问另一个进程的内存)
- 地址空间布局随机化(ASLR)
- 写时拷贝(COW)、按需分页、内存映射文件、huge page 等高级特性
x86_64 常用虚拟地址位宽:48 位(有效范围 0x0000_0000_0000_0000 ~ 0x0000_ffff_ffff_ffff 和 0xffff_0000_0000_0000 ~ 0xffff_ffff_ffff_ffff)
2. 经典 4KB 页的多级页表结构(x86_64 四级页表)
Linux 在 x86_64 上默认使用 4 级页表(CONFIG_PGTABLE_LEVELS=4 或 5):
| 级别 | 英文全称 | 中文常用称呼 | 每项大小 | 每表条目数 | 覆盖虚拟地址范围 | 位段(48位 VA) |
|---|---|---|---|---|---|---|
| 第1级 | Page Global Directory | 全局页目录 PGD | 4KB | 512 | 256TB | 47–39 位(9 bit) |
| 第2级 | Page Upper Directory | 上层目录 PUD | 4KB | 512 | 512GB | 38–30 位(9 bit) |
| 第3级 | Page Middle Directory | 中间目录 PMD | 4KB | 512 | 1GB | 29–21 位(9 bit) |
| 第4级 | Page Table | 页表 PT / PTE | 4KB | 512 | 2MB → 4KB | 20–12 位(9 bit) |
| 页内偏移 | — | — | — | — | 4KB | 11–0 位(12 bit) |
- 每级页表都是 4KB 大小(一页),正好能被上一级指向。
- 每个页表项(Entry)占 8 字节(64 位),所以 4KB / 8 = 512 项。
- 总虚拟地址拆分:9+9+9+9+12 = 48 位(高 16 位用于 canonical 形式)。
3. 虚拟地址 → 物理地址 的完整转换过程(Page Table Walk)
以虚拟地址 0x00007f1234567890 为例(简化后):
- CPU 拿到虚拟地址 VA。
- 从 CR3 寄存器 读取当前进程的 PGD 基地址(物理地址)。
- 用 VA 的 47–39 位 作为索引,在 PGD 中找到对应条目 → 得到 PUD 的物理基地址。
- 用 VA 的 38–30 位 在 PUD 中索引 → 得到 PMD 基地址。
- 用 VA 的 29–21 位 在 PMD 中索引 → 得到 PTE 基地址(或直接是大页)。
- 用 VA 的 20–12 位 在 PTE 中索引 → 得到 物理页框号(PFN) + 页属性。
- 物理地址 PA = PFN << 12 | (VA 的 11–0 位偏移)。
整个过程由 MMU 硬件 自动完成,软件(内核)只负责填充页表。
4. 页表项(PTE / PMD / PUD / PGD)常见位含义(64 位)
| 位 | 含义 | 说明 |
|---|---|---|
| 0 | Present (P) | 1 = 页存在,0 = 缺页(触发 page fault) |
| 1 | Read/Write (R/W) | 1 = 可写,0 = 只读(写时拷贝、写保护关键位) |
| 2 | User/Supervisor (U/S) | 1 = 用户态可访问,0 = 仅内核态 |
| 7 | Page Size (PS) | 在 PMD 级为 1 表示 2MB 大页,在 PUD 级为 1 表示 1GB 大页 |
| 63 | NX / XD | No-eXecute,防止代码执行(安全防护) |
| 51–12 | Page Frame Number (PFN) | 物理页框号(高位通常为 0 或用于扩展) |
| 其他 | Dirty、Accessed、PAT 等 | 脏页标记、访问位、页面属性表等 |
5. 大页(Huge Page / THP)如何改变页表结构?
Linux 支持两种大页:
- 传统 Huge Pages(hugetlbfs):2MB / 1GB,手动预分配。
- 透明大页(THP):内核自动尝试合并 4KB 页为 2MB(甚至 1GB),对应用透明。
| 大页大小 | 生效级别 | 页表减少到几级 | 优势 | 缺点 |
|---|---|---|---|---|
| 2MB | PMD | 3 级(PGD → PUD → PMD) | TLB 命中率大幅提升,页表内存少 | 内存碎片化更严重 |
| 1GB | PUD | 2 级(PGD → PUD) | 极致性能(数据库、大数据、虚拟化) | 分配极难,碎片化严重 |
THP 默认开启(/sys/kernel/mm/transparent_hugepage/enabled),在匿名内存、page cache 中自动尝试。
6. 关键加速机制:TLB(Translation Lookaside Buffer)
纯页表遍历需要 4 次内存访问(PGD → PUD → PMD → PTE),太慢!
- TLB:CPU 内部高速缓存,保存最近的 VA → PA 映射。
- 命中:1 个周期。
- 未命中:走完整 page walk(几十~上百周期)。
- TLB 大小:几十到几千条(L1/L2 TLB)。
- 大页(2MB/1GB)大幅减少 TLB miss,因为一条 TLB 条目覆盖更大范围。
7. 进程切换时页表怎么变?
- 每个进程有独立的 mm_struct → pgd(CR3 指向的基地址)。
- 切换进程时,内核修改 CR3 寄存器 → 加载新进程的 PGD 物理地址。
- 现代 CPU 支持 PCID(Process Context Identifiers)(ASID),可以避免每次切换都 flush TLB。
8. 常见内核函数与工具(调试 / 理解用)
| 功能 | 内核函数 / 宏 / 命令 | 说明 |
|---|---|---|
| 虚拟地址 → 物理地址 | __virt_to_phys() / virt_to_phys() | 内核地址转换 |
| 物理地址 → 页结构 | pfn_to_page() | PFN → struct page * |
| 打印进程页表 | /proc//pagemap / smaps | 用户态查看 |
| 内核调试 | crash / drgn / gdb + vmlinux | 查看 CR3、页表项 |
| 强制大页 | echo always > /sys/kernel/mm/transparent_hugepage/enabled | 开启 THP 激进模式 |
总结:一句话记忆流程
虚拟地址(48位) → 分段索引(9+9+9+9) → 4 次查表(PGD → PUD → PMD → PTE) → 取出 PFN + 偏移 → 物理地址
硬件(MMU + TLB) + 内核(页表填充 + 缺页中断处理)共同完成。
想看哪一部分更详细?
- 5 级页表(5-level paging)区别
- 内核地址空间布局(直接映射区、高端内存、vmalloc 等)
- 缺页异常(page fault)处理流程
- ARM64 页表与 x86_64 的主要不同
- 实际用 gdb / crash 看页表的例子
随时告诉我,继续深入~