Skip to content

分页、分段与页表

基于通用操作系统概念 · 核于 2026-08

速查

  • 分页(Paging):虚拟地址空间切成固定大小页(Page,4KB),物理内存切成同大小帧(Frame)页表记录"页号→帧号"映射。优点:无外部碎片(任意空闲帧能放任意页);缺点:内部碎片(最后一页填不满,平均浪费页大小一半)。
  • 页表项(PTE):物理帧号 + 权限/状态位——Present(在内存?)、R/W(读写)、U/S(用户/内核)、Dirty(写过?换出要写回磁盘)、Accessed(访问过?供 LRU/Clock)。
  • 单级页表占内存:32 位/4KB 页 → 单进程 4MB 页表;64 位更恐怖。无法承受。
  • 多级页表:分级(x86-64 是 4 级:PGD→PUD→PMD→PTE),只为实际用到的虚拟地址建底层表,大量空洞只占上层一个"不存在"标记——省内存。代价:翻译多查几次(4 次),靠 TLB 缓存。
  • TLB(Translation Lookaside Buffer):CPU 内页表项缓存,命中 1 个时钟周期,miss 才查内存页表。命中率 >99% 是 VM 性能关键;**大页(2MB/1GB)**减少 TLB 压力,PCID/ASID 让进程切换不必 flush TLB。
  • 反置页表(Inverted Page Table):按物理帧号索引(而非虚拟页号),全系统只有一张,省内存——用于 PowerPC/IA-64。代价:查"虚拟→物理"需哈希,更复杂。
  • 分段(Segmentation):按逻辑段(代码/数据/栈)切分,段大小可变。优点:符合程序结构、易共享;缺点:外部碎片(空闲块零碎不连续,难分配大块)。
  • 段页式(Paged Segmentation):先分段、段内再分页——结合两者(逻辑清晰 + 无外部碎片)。x86 历史支持,现代 64 位 OS 用 flat 模式只用分页
  • 逻辑地址 vs 物理地址:逻辑地址=程序产生的地址(段:偏移 或纯虚拟地址);物理地址=送到内存总线的地址。两者经地址翻译(MMU + 页表)转换。
  • 地址转换计算模板:①页大小→页内偏移位数(4KB=2^12→12 位);②虚拟地址拆 VPN(高位)+ offset(低位 12 位);③查页表 VPN→PFN;④物理地址 = PFN 拼接 offset。
  • 共享页:多个进程的页表项映射同一物理帧——用于共享库(libc 只装入一份物理帧,各进程共享)、写时复制(COW)。
  • 进阶顺序:本文讲地址翻译机制 → 下一步 页面置换算法与抖动 讲内存满淘汰谁的算法。

一、分页:固定大小的页与帧

分页把虚拟地址空间和物理内存都切成固定大小的块:

   虚拟地址空间              物理内存(RAM)
   ┌──────────┐              ┌──────────┐
   │ 页 0     │──────────────│ 帧 5     │
   │ 页 1     │──────┐       │ 帧 6     │
   │ 页 2     │      └──────│ 帧 7     │
   │ 页 3     │──────────────│ 帧 2     │   ← 页可以不连续存放
   │ 页 4     │──┐           │ 帧 8     │     (帧 5/7/2/8 非连续)
   │ ...      │  └───────────│ 帧 9     │
   └──────────┘   (未装入)   └──────────┘
  • 页(Page):虚拟地址空间的固定大小单位,通常 4KB(也有支持 2MB/1GB 大页)。
  • 帧(Frame):物理内存的固定大小单位,与页大小相同。任意空闲帧都能放任意页——这是分页无外部碎片的原因(所有帧等大,没有"块大小不匹配"问题)。
  • 页表:记录"虚拟页号 → 物理帧号"的映射表。每个进程一张,进程切换时切换页表(CR3 寄存器指向当前页表基址)。
  • 内部碎片:分页的唯一碎片。假设页 4KB,一个 5KB 的结构占 2 页(8KB),浪费 3KB(平均每个分配浪费页大小的一半)。无法消除(除非页足够小,但页小了页表变大)。

二、页表项 PTE:映射与权限位

页表由大量**页表项(PTE)**组成,每项记录一个虚拟页的信息:

   PTE 格式(简化):
   ┌──────────────┬─────┬─────┬─────┬─────────┬────────┬─────┐
   │ 物理帧号 PFN │ P/R │ U/S │ R/W │Accessed │ Dirty  │ ... │
   └──────────────┴─────┴─────┴─────┴─────────┴────────┴─────┘
   Present 位:1=页在内存,0=不在(缺页)
  • Present 位:最关键。1 表示页在物理内存,0 表示不在(可能换出到 swap、或未分配、或 COW 触发)。访问 Present=0 的页触发缺页中断
  • R/W 位:读写权限。只读页被写时(如 COW 共享页)触发缺页,内核复制一份。
  • U/S 位:用户/内核权限。内核页 U/S=0,用户态访问触发保护异常(这是隔离内核内存的机制)。
  • Dirty 位:该页自装入后是否被写过。换出时若 Dirty=1 要写回磁盘(内容变了),Dirty=0 可直接丢弃(磁盘已有最新副本,如代码段)。
  • Accessed 位:该页最近是否被访问过。页面置换算法(Clock/二次机会)依据它判断页是否活跃——硬件在访问时自动置位,OS 周期性清零来采样。

三、多级页表:省内存的分级结构

单级页表的问题是占内存太大

   假设 32 位地址、4KB 页:
   - 虚拟地址空间 = 2^32 = 4GB
   - 页数 = 2^32 / 2^12 = 2^20 = 100 万页
   - 每个页表项 4 字节 → 单进程页表 = 4MB
   - 100 个进程 = 400MB 全是页表(物理内存才几 GB)

   64 位地址更恐怖:2^48 → 单进程页表 PB 级,根本存不下

多级页表的省内存之道:把页表分级,只为实际用到的虚拟地址范围建底层表:

   4 级页表(x86-64,4KB 页):
   虚拟地址 48 位 = [9位 PGD 索引][9位 PUD][9位 PMD][9位 PTE][12位 offset]

   PGD(顶级) → PUD → PMD → PTE(底层) → 物理帧

   - 大量未使用的虚拟地址(如 0x0 到 0x400000 之间的空洞)
     只需 PGD 一个"不存在"项,下面三级都不建 → 省海量空间
   - 只有实际用到的地址(堆/栈/代码/数据段)才建完整四级表
  • 省内存的本质:单级页表必须为整个虚拟地址空间预留表项(即使 99% 不用);多级页表只为用到的部分建表,未用的部分上层一个标记即可。
  • 代价:翻译层级多:4 级页表意味着一次翻译要 4 次内存访问(查 PGD→PUD→PMD→PTE),比单级的 1 次慢得多。必须靠 TLB 缓存——99%+ 的访问走 TLB,只有 miss 才走多级页表。
  • 5 级页表:随着物理内存增大(TB 级),x86-64 引入 5 级页表(LA57),虚拟地址扩展到 57 位。

四、TLB:页表项的硬件缓存

TLB(Translation Lookaside Buffer)是 CPU 内部缓存最近用到的页表项的高速缓存:

   CPU 访存(虚拟地址)


   ┌─────────────────────────────────┐
   │  TLB 查询(1 个时钟周期)        │
   │  命中 → 直接拿物理帧号 ✓         │   99%+ 的访问走这里
   │  miss → 走页表(多级,慢)       │
   └──────────────┬──────────────────┘

   物理地址 → 访 DRAM
  • TLB 容量小但关键:通常 64-2048 项,每项映射一个 4KB 页(覆盖 256KB-8MB)。对局部性好的程序,命中率 > 99%。
  • TLB miss 的代价:4 级页表要 4 次内存访问(每次几十 ns),单次 miss 可能耗时数百 ns——比访存本身慢 5 倍以上。
  • 大页(Huge Pages):用 2MB 或 1GB 大页,一项 TLB 项覆盖范围扩大 512 倍/262144 倍,显著减少 TLB miss。数据库(PostgreSQL/Oracle)、JVM、Hypervisor 常开启大页以降低 TLB 压力。
  • 进程切换与 TLB:传统上进程切换要 flush TLB(否则 A 进程用 B 的旧映射会出错),但 flush 后新进程要重新预热 TLB(前期大量 miss)。现代 CPU 用 PCID(Process Context ID,x86)/ASID(ARM) 给 TLB 项打进程标签,进程切换时不必全 flush,新进程的 TLB 项可能还在——显著提升上下文切换性能。

五、反置页表(进阶)

传统页表按虚拟页号索引,每个进程一张。反置页表(Inverted Page Table, IPT)反过来——按物理帧号索引,全系统只有一张

  • 思路:表的大小 = 物理帧数(与进程数无关),无论多少进程都只有一张表,省内存。
  • 查找:要从"虚拟地址"找"物理地址",需要反向查——用 (进程 ID, 虚拟页号) 做哈希,在 IPT 里找对应的物理帧。通常配合哈希表加速。
  • 应用:PowerPC、IA-64(Itanium)使用。x86/Linux 仍用传统多级页表(多级已足够省内存)。

六、分段:按逻辑段切分

分段(Segmentation)是比分页更早的地址空间组织方式,按程序的逻辑段切分:

   程序的逻辑结构:
   ┌──────────┐ 代码段(只读、可共享)
   │  main()  │
   ├──────────┤ 数据段(读写)
   │  全局变量 │
   ├──────────┤ 堆段(向上增长)
   │   ↑      │
   │  ...     │
   │   ↓      │ 栈段(向下增长)
   └──────────┘
  • 段表(段寄存器):每个段有 段基址(base)+ 段限(limit)+ 权限。虚拟地址 = 段号 + 段内偏移;物理地址 = 段基址 + 段内偏移。
  • 优点:符合程序逻辑结构(代码段/数据段天然分离)、易于按段共享(共享代码段)、易于按段保护(代码段只读、数据段读写)。
  • 缺点:外部碎片。段大小可变,频繁分配回收后,物理内存出现大量零碎的空闲块——可能总空闲够,但没有一个连续大块能装新段。需要紧凑(compaction,移动段合并空闲块,代价极大)。
  • 为什么被淘汰:外部碎片是长期运行的系统的灾难(无法根治)。x86 历史上支持段页式(先分段、段内再分页),但现代 64 位 OS 用 flat 模式(段基址=0、段限=最大),相当于关闭分段、只用分页。

七、段页式:结合两者

段页式(Paged Segmentation)试图结合分段与分页的优点:

  • 机制:先按逻辑分段,每个段内再分页。地址翻译:虚拟地址 = 段号 + 段内页号 + 页内偏移 → 查段表得段基址 → 段内查页表得物理帧。
  • 优点:保留了段的逻辑清晰(易共享/保护)+ 分页的无外部碎片(段内按页分配,无空洞)。
  • 缺点:地址翻译两层(段表 + 页表),更复杂。
  • 现状:x86 硬件支持段页式,但实际部署中 OS 多用 flat 模式(关闭分段),所以段页式主要是教科书概念,工程上少用

八、地址转换计算:解题模板

题型:给定页大小、页表(或部分映射)、虚拟地址,求物理地址。

解题步骤

   已知:页大小 = 4KB = 2^12 字节
         页表(部分):
            VPN 0 → PFN 5
            VPN 1 → PFN 9
            VPN 2 → PFN 3
         求虚拟地址 0x1543 的物理地址。

   步骤:
   1. 页内偏移位数 = log2(页大小) = log2(4096) = 12 位
      → offset = 虚拟地址 & (0xFFF) = 0x1543 & 0xFFF = 0x543
   2. VPN = 虚拟地址 >> 12 = 0x1543 >> 12 = 0x1(即页号 1)
   3. 查页表:VPN 1 → PFN 9
   4. 物理地址 = (PFN << 12) | offset = (9 << 12) | 0x543
               = 0x9000 | 0x543 = 0x9543

关键点

  • offset 位数 = log2(页大小):4KB→12 位,8KB→13 位,64KB→16 位。
  • offset 不变:虚拟地址的 offset 直接搬到物理地址(页内偏移不变)。
  • VPN 是高位:虚拟地址去掉 offset 位剩下的就是 VPN。
  • PFN 来自页表:查 VPN 对应的 PFN,物理地址 = (PFN << offset位数) | offset。
  • 特殊情况:VPN 查不到(Present=0)→ 缺页中断;VPN 越界 → 段错误(SIGSEGV)。

九、共享页:节省内存的利器

多个进程可以共享同一个物理帧——只要它们的页表项都映射到该帧:

  • 共享库(shared library):libc.so 被几百个进程链接,但物理内存里只装入一份代码段帧,各进程的页表都映射到这些帧。代码段只读,共享安全;数据段每个进程有独立副本(写时复制或独立分配)。
  • 写时复制(COW)fork() 后父子进程共享物理页(页表都映射同一帧,标记只读)。任一方写时触发缺页(写只读页),内核才复制一份新帧——延迟复制,绝大多数 fork 后立即 exec(丢弃共享页),避免无谓复制。
  • 共享内存 IPCshmget/mmap(MAP_SHARED) 让多个进程的页表映射同一物理帧,直接读写通信(最快的 IPC)。

下一步

讲清了地址翻译机制(页表/多级/TLB/分段)后,下一步解决"物理帧满了淘汰谁"——页面置换算法与抖动 讲 FIFO/LRU/最优/Clock 的抉择、Belady 异常、抖动与工作集理论。