2. 分页机制原理:页框与页的概念、页表的作用、多级页表结构(x86_64 4级页表)
分页(Paging)是现代操作系统内存管理的核心机制,它将物理内存划分为固定大小的页框(Page Frame),并将进程的虚拟地址空间划分为相同大小的页(Page)。通过页表(Page Table)这一关键数据结构,CPU 能够将虚拟地址动态转换为物理地址,从而实现隔离、共享和按需加载。
2.1 页框与页的概念
在分页系统中,物理内存被分割成大小相等的块,称为页框(也称物理页)。每个页框是物理内存分配的最小单位。相应地,进程的虚拟地址空间也被分割成与页框大小相同的块,称为页(也称虚拟页)。
| 术语 | 所属空间 | 大小 | 说明 |
|---|---|---|---|
| 页框(Page Frame) | 物理内存 | 通常 4KB | 物理内存的固定大小分区,是内存分配的实际载体 |
| 页(Page) | 虚拟地址空间 | 通常 4KB | 虚拟地址空间的固定大小分区,与页框一一映射 |
| 页偏移(Offset) | 页内 | 12 位(4KB) | 用于在页内定位具体的字节地址 |
关键理解:页框是物理内存的“槽位”,页是虚拟内存的“容器”。分页机制的核心任务就是将虚拟页映射到物理页框上。页框大小通常为 4KB,但在大内存场景下,x86_64 也支持 2MB 和 1GB 的大页(Huge Pages),以减少页表层级和 TLB 缺失。
2.2 页表的作用
页表是存储在内存中的一种数据结构,用于记录虚拟页到物理页框的映射关系。每个进程拥有独立的页表,从而实现进程间的地址空间隔离。页表的核心作用包括:
- 地址转换:将虚拟地址中的页号转换为物理页框号,结合页内偏移得到最终物理地址。
- 权限控制:每个页表项(PTE)包含读、写、执行、用户/内核等权限位,实现细粒度的访问控制。
- 按需分页:通过“存在位”(Present Bit)标记页是否在物理内存中,支持缺页异常和页面换入换出。
- 共享内存:多个进程的页表项可以指向同一个物理页框,实现高效的数据共享。
页表项(PTE)关键字段:
- Present (P): 页是否在物理内存中。
- Read/Write (R/W): 是否可写。
- User/Supervisor (U/S): 用户态是否可访问。
- Dirty (D): 页是否被写入过。
- Accessed (A): 页是否被访问过。
- Page Frame Number (PFN): 物理页框号。
2.3 多级页表结构(x86_64 4级页表)
在 64 位系统中,如果使用单级页表,页表本身将占用巨大的物理内存(例如,48 位虚拟地址空间需要 2^36 个页表项,每个 8 字节,共 512GB)。为了解决这个问题,x86_64 架构采用了多级页表结构,通过分级索引来压缩页表体积,并支持按需分配中间级页表。
x86_64 在传统模式下使用 4 级页表,将虚拟地址划分为 5 个部分:
- PML4(Page Map Level 4): 第 1 级索引,9 位,指向 PML4 表项。
- PDPT(Page Directory Pointer Table): 第 2 级索引,9 位,指向页目录指针表项。
- PD(Page Directory): 第 3 级索引,9 位,指向页目录表项。
- PT(Page Table): 第 4 级索引,9 位,指向页表项。
- Offset: 页内偏移,12 位(4KB 页)。
虚拟地址总长度为 48 位(高 16 位为符号扩展),结构如下:
| 63 48 | 47 39 | 38 30 | 29 21 | 20 12 | 11 0 |
| 符号扩展 | PML4 | PDPT | PD | PT | Offset |
| (16位) | (9位) | (9位) | (9位) | (9位) | (12位) |
注意:虽然 x86_64 架构支持 64 位虚拟地址,但当前硬件实现仅使用低 48 位,高 16 位必须为第 47 位的符号扩展(全 0 或全 1),否则 CPU 会抛出异常。这形成了用户空间(0x0000000000000000 - 0x00007FFFFFFFFFFF)和内核空间(0xFFFF800000000000 - 0xFFFFFFFFFFFFFFFF)的经典划分。
4 级页表地址转换流程
当 CPU 访问一个虚拟地址时,MMU(内存管理单元)执行以下步骤:
- 从控制寄存器 CR3 中获取当前进程的 PML4 表基地址。
- 使用虚拟地址的 PML4 索引(位 47:39)在 PML4 表中查找对应的 PML4 表项,获取 PDPT 表的基地址。
- 使用 PDPT 索引(位 38:30)在 PDPT 表中查找对应的 PDPT 表项,获取 PD 表的基地址。
- 使用 PD 索引(位 29:21)在 PD 表中查找对应的 PD 表项,获取 PT 表的基地址。
- 使用 PT 索引(位 20:12)在 PT 表中查找对应的 PT 表项,获取物理页框号(PFN)。
- 将 PFN 与 Offset(位 11:0)拼接,形成最终的物理地址。
性能优化:多级页表虽然增加了地址转换的步骤,但通过 TLB(Translation Lookaside Buffer)缓存最近使用的页表项,可以大幅减少实际的内存访问次数。此外,Linux 内核在 4 级页表基础上,还支持 5 级页表(用于 57 位虚拟地址空间)和多种大页映射,以适应不同场景的性能需求。
4 级页表地址转换流程图
2.4 多级页表的优势与权衡
| 优势 | 说明 |
|---|---|
| 节省内存 | 只有实际使用的虚拟地址范围才需要分配中间级页表,未使用的区域无需分配,大幅降低页表占用的物理内存。 |
| 按需分配 | 进程创建时只需分配 PML4 表,其他级页表在缺页时动态分配,减少初始化开销。 |
| 支持大地址空间 | 4 级页表可覆盖 48 位虚拟地址空间(256TB),5 级页表可覆盖 57 位(128PB)。 |
| 灵活的权限控制 | 每级页表项均可设置权限位,支持细粒度的内存保护。 |
权衡:多级页表增加了地址转换的延迟(每次转换需要 4 次内存访问),但 TLB 缓存可以显著缓解这一问题。此外,TLB 缺失时的页表遍历开销也高于单级页表。Linux 内核通过大页映射(如 2MB 透明大页)来减少页表层级,从而降低 TLB 缺失率。
2.5 Linux 内核中的页表管理
Linux 内核在 x86_64 上实现了对 4 级页表的完整支持,并提供了统一的页表操作接口。内核通过 pgd、p4d、pud、pmd、pte 等类型来抽象不同级别的页表项,使得代码可以跨架构复用。
// Linux 内核页表层级类型(x86_64 4级页表)
typedef struct { pgdval_t pgd; } pgd_t; // PML4 表项
typedef struct { p4dval_t p4d; } p4d_t; // 第4级(x86_64 上未使用)
typedef struct { pudval_t pud; } pud_t; // PDPT 表项
typedef struct { pmdval_t pmd; } pmd_t; // PD 表项
typedef struct { pteval_t pte; } pte_t; // PT 表项
// 页表遍历宏示例
#define pgd_offset(mm, addr) ((mm)->pgd + pgd_index(addr))
#define p4d_offset(pgd, addr) p4d_offset(pgd, addr)
#define pud_offset(p4d, addr) pud_offset(p4d, addr)
#define pmd_offset(pud, addr) pmd_offset(pud, addr)
#define pte_offset_kernel(pmd, addr) pte_offset_kernel(pmd, addr)
内核开发者注意:在遍历用户空间页表时,必须使用
mmap_sem(或 mmap_lock)进行保护,防止页表在遍历过程中被并发修改。此外,直接修改页表项后,需要调用 flush_tlb_page 或 flush_tlb_mm 来刷新 TLB,否则 CPU 可能使用过期的缓存映射。
2.6 本章小结
- 页框与页是分页机制的基本单位,页框属于物理内存,页属于虚拟地址空间。
- 页表是地址转换的核心数据结构,记录虚拟页到物理页框的映射关系,并包含权限和控制位。
- 多级页表(x86_64 4级页表)通过分级索引压缩页表体积,支持大地址空间和按需分配,是 Linux 内核内存管理的基石。
- 理解页表结构是后续学习 TLB 刷新优化、大页映射、KSM(内核同页合并)等高级主题的前提。