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(内存管理单元)自动完成地址转换。转换过程分为三个步骤:

  1. 拆分虚拟地址:将虚拟地址拆分为页号(VPN)和页内偏移(Offset)。
  2. 查页表:以 VPN 为索引,在页表中查找对应的 PTE,获取物理页框号(PFN)。
  3. 拼接物理地址:物理地址 = PFN × 页面大小 + Offset。
虚拟地址 → 物理地址转换流程 虚拟地址 VPN (20位) | Offset (12位) 页表 VPN → PTE 映射 物理地址 PFN (20位) | Offset (12位) 物理页框(Page Frame) 物理内存中与页面大小相同的连续区域,由 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 页面。
x86-64 四级页表映射结构 虚拟地址:PML4(9位) | PDPT(9位) | PD(9位) | PT(9位) | Offset(12位) PML4 512 条目 PDPT 512 条目 PD 512 条目 PT 512 条目 物理页框(Page Frame) 物理内存中的 4KB 连续区域 映射关系:虚拟页面 → 多级页表遍历 → 物理页框 每级页表负责虚拟地址中的 9 位索引,共 36 位 + 12 位偏移 = 48 位虚拟地址

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 进程的页表项原始数据。