2. 分页机制原理:页框、页面、页表与页表项
分页(Paging)是现代操作系统实现虚拟内存的核心机制。它将虚拟地址空间和物理内存分别划分为固定大小的块,通过页表建立映射关系,从而实现非连续物理分配、隔离保护和按需加载。
2.1 核心概念定义
| 术语 | 英文 | 定义 | 大小 |
|---|---|---|---|
| 页面(Page) | Page | 虚拟地址空间划分的固定大小块,是虚拟内存的基本单位 | 通常 4 KB(x86)或 64 KB(ARM 大页) |
| 页框(Page Frame) | Page Frame | 物理内存划分的固定大小块,与页面大小严格相等 | 与页面大小一致 |
| 页表(Page Table) | Page Table | 存储虚拟页面到物理页框映射关系的数据结构,每个进程独立拥有 | 取决于地址空间大小 |
| 页表项(PTE) | Page Table Entry | 页表中的一行,记录一个虚拟页面对应的物理页框号及权限属性 | 通常 4 字节或 8 字节 |
关键原则:页面大小 = 页框大小。虚拟页面与物理页框之间通过页表项建立多对一映射(共享内存)或一对一映射(私有内存),但不存在一对多映射。
2.2 虚拟地址到物理地址的转换流程
CPU 访问内存时,MMU(内存管理单元)自动完成地址转换。转换过程分为三个步骤:
- 拆分虚拟地址:将虚拟地址拆分为页号(VPN)和页内偏移(Offset)。
- 查页表:以 VPN 为索引,在页表中查找对应的 PTE,获取物理页框号(PFN)。
- 拼接物理地址:物理地址 = PFN × 页面大小 + Offset。
2.3 页表项(PTE)结构详解
每个 PTE 不仅包含物理页框号,还携带一组权限控制位。以 x86-64 架构为例,一个典型的 8 字节 PTE 结构如下:
| 位域 | 名称 | 功能描述 |
|---|---|---|
| Bit 0 | P(Present) | 页面是否在物理内存中。0 表示缺页,访问将触发 Page Fault |
| Bit 1 | R/W(Read/Write) | 0 表示只读,1 表示可读写 |
| Bit 2 | U/S(User/Supervisor) | 0 表示仅内核态可访问,1 表示用户态也可访问 |
| Bit 3 | PWT(Page Write-Through) | 缓存写策略:0 写回,1 写直达 |
| Bit 4 | PCD(Page Cache Disable) | 1 表示禁止该页的缓存 |
| Bit 5 | A(Accessed) | 硬件自动置位,表示该页被访问过(用于页面置换算法) |
| Bit 6 | D(Dirty) | 硬件自动置位,表示该页被写入过(用于回写磁盘) |
| Bit 7 | PS(Page Size) | 大页标志:0 为 4KB 页,1 为 2MB/1GB 大页 |
| Bit 8–11 | G(Global)等 | 全局页、软件可用位等 |
| Bit 12–51 | PFN(物理页框号) | 物理地址的高 40 位,指向物理页框的起始地址 |
| Bit 52–62 | 保留/软件可用 | 操作系统可自定义用途(如 NUMA 节点 ID) |
| Bit 63 | NX(No-Execute) | 1 表示禁止在该页执行代码(DEP 保护) |
实践要点:PTE 的权限位是操作系统实现写时复制(Copy-on-Write)、按需调页(Demand Paging)和内存映射文件(mmap)的基础。例如,fork() 后子进程的 PTE 被标记为只读,写操作触发缺页后复制物理页。
2.4 多级页表与映射关系
现代 64 位系统使用多级页表来避免为整个地址空间预分配页表。以 x86-64 的 4 级页表为例:
- PML4(Page Map Level 4):顶级页表,512 个条目,覆盖整个 48 位虚拟地址空间。
- PDPT(Page Directory Pointer Table):二级页表,512 个条目,每个覆盖 1 GB。
- PD(Page Directory):三级页表,512 个条目,每个覆盖 2 MB。
- PT(Page Table):四级页表,512 个条目,每个映射 4 KB 页面。
2.5 映射关系的关键特性
- 非连续映射:连续的虚拟页面可以映射到不连续的物理页框,消除外部碎片。
- 共享映射:多个虚拟页面可映射到同一个物理页框(如共享库、共享内存)。
- 按需映射:PTE 的 Present 位为 0 时,物理页框尚未分配,访问时触发缺页中断。
- 权限隔离:U/S 位和 R/W 位实现用户态/内核态隔离,以及代码段/数据段保护。
常见误区:页表本身也存储在物理内存中,由 MMU 硬件直接遍历。操作系统负责维护页表内容,但遍历过程由硬件完成,不经过软件。修改页表后必须刷新 TLB(Translation Lookaside Buffer),否则 CPU 可能使用过期的缓存映射。
2.6 页框与页面的生命周期
| 阶段 | 页面状态 | 页框状态 | PTE Present 位 |
|---|---|---|---|
| 进程创建 | 已分配虚拟地址 | 未分配 | 0(缺页) |
| 首次访问 | 按需调入 | 分配并填充 | 1 |
| 正常运行 | 驻留内存 | 被占用 | 1 |
| 页面置换 | 换出到交换区 | 释放 | 0(换出标志) |
| 进程退出 | 释放虚拟地址 | 释放 | 无效 |
内核视角:物理页框是稀缺资源,操作系统通过伙伴系统(Buddy System)管理页框分配,通过页面置换算法(如 LRU、Clock)决定哪些页框被回收。PTE 的 Accessed 位和 Dirty 位为这些算法提供硬件级支持。
2.7 代码示例:解析 PTE 权限位
以下 C 代码演示如何解析一个 x86-64 页表项的权限位:
#include <stdint.h>
#include <stdio.h>
// x86-64 页表项结构(简化)
typedef struct {
uint64_t present : 1; // Bit 0
uint64_t rw : 1; // Bit 1
uint64_t user : 1; // Bit 2
uint64_t pwt : 1; // Bit 3
uint64_t pcd : 1; // Bit 4
uint64_t accessed : 1; // Bit 5
uint64_t dirty : 1; // Bit 6
uint64_t ps : 1; // Bit 7
uint64_t global : 1; // Bit 8
uint64_t ignored : 3; // Bit 9-11
uint64_t pfn : 40; // Bit 12-51
uint64_t reserved : 11; // Bit 52-62
uint64_t nx : 1; // Bit 63
} __attribute__((packed)) pte_t;
void dump_pte(pte_t *pte) {
printf("PTE 解析结果:\n");
printf(" Present (P) : %s\n", pte->present ? "是 (在内存)" : "否 (缺页)");
printf(" 可写 (R/W) : %s\n", pte->rw ? "可读写" : "只读");
printf(" 用户态 (U/S) : %s\n", pte->user ? "用户态可访问" : "仅内核态");
printf(" 已访问 (A) : %s\n", pte->accessed ? "是" : "否");
printf(" 已脏 (D) : %s\n", pte->dirty ? "是 (已写入)" : "否");
printf(" 大页 (PS) : %s\n", pte->ps ? "是 (2MB/1GB)" : "否 (4KB)");
printf(" 不可执行 (NX) : %s\n", pte->nx ? "禁止执行" : "允许执行");
printf(" 物理页框号 : 0x%010lx\n", pte->pfn);
}
int main() {
// 模拟一个有效的 PTE:PFN=0x12345,可读写,用户态,已访问,未脏
pte_t pte = {
.present = 1,
.rw = 1,
.user = 1,
.accessed = 1,
.dirty = 0,
.ps = 0,
.nx = 0,
.pfn = 0x12345
};
dump_pte(&pte);
return 0;
}
实验建议:在 Linux 系统中,可以通过
/proc/self/pagemap 接口读取进程自身的 PTE 信息,观察物理页框号和权限位。使用 sudo cat /proc/1/pagemap | xxd 可查看 init 进程的页表项原始数据。