两级页表、分段与段页式:三种非连续分配方案的深度对比


引言

上一篇的分页方案解决了外部碎片,但留下了两个问题:一是每进程 4MB 的单级页表内存开销太大,二是分页的"一刀切"粒度缺乏对程序逻辑结构的表达能力——一个函数的代码可能被切成好几页散落在内存各处,分页本身没有"这个区域是代码、那个区域是数据"的概念。

两级页表解决第一个问题,分段解决第二个问题,段页式则试图把两者融合。这篇文章就是关于这三种"进阶方案"的。

📌 核心要点

  • 两级页表通过"按需创建"外层页目录和内层页表,将实际内存占用从每进程约 4MB 压缩到约 12KB(只创建被使用的页对应的页表部分),同时保持完整的 4GB 地址空间映射能力。(408 统考大纲,2026)
  • 分段按程序的逻辑单元(代码段、数据段、栈段)切分内存,地址是二维的(段号+段内偏移),有利于共享与保护,但存在外部碎片。
  • 段页式 = 先分段再分页,地址变换需要三次访存(段表→页表→数据),是三种方案中地址转换开销最大的。
  • x86-64 基本废弃了分段机制,说明在工程实践中"纯分页+权限位"可以替代分段的保护功能。

两级页表——"按需创建"解决单级页表的巨量开销

单级页表的浪费在哪里?

以一个典型的 32 位系统为例:4KB 页、32 位逻辑地址空间,每个进程有 2^20(约 100 万)个页表项。每个页表项 4 字节 → 每进程页表 4MB。100 个进程呢?400MB——这还只是页表的开销。

但这里有一个巨大的浪费:一个进程实际使用的地址空间通常很小。一个典型的 32 位 Linux 进程,实际映射的地址空间通常在几 MB 到几十 MB 的量级——远小于 4GB 的虚拟地址空间上限。也就是说,100 万个页表项中,绝大多数对应的都是"未使用"区域,从来不会被访问到。

单级页表的问题不在于"页表项太多",而在于"页表必须连续、完整地存在"。即便进程只用了地址空间的前 8MB,后 4088MB 对应的页表项也必须在页表中占据位置——这些位置存着 P = 0,标识"不在内存",但它们确实占用了宝贵的物理内存。

两级页表的设计

两级页表的思路直截了当:不要让一整张 4MB 的页表始终驻留在内存中,而是把它拆成可以按需分配的小块。

具体设计如下:32 位逻辑地址重新划分:

|←— 10 位 —→|←— 10 位 —→|←—— 12 位 ——→|
   外层页号     内层页号      页内偏移量
  (Page Dir)   (Page Table)   (Offset)

外层页表(页目录,Page Directory):包含 2^10 = 1024 个页目录项,每个目录项指向一个内层页表(或为空)。页目录需要始终驻留在内存中——它只有 1024 项 × 4 字节 = 4KB。

内层页表(Page Table):每个内层页表有 2^10 = 1024 个页表项,对应 4MB 的地址范围。关键在于:内层页表只在进程确实需要访问那 4MB 区域时才被创建。如果进程只用了地址空间的底部和顶部,那么只有对应的两个内层页表存在——总共 (1 + 2) × 4KB = 12KB 的内存开销。

两级地址变换

  1. 外层页号 → 查页目录(第一次访存),获得内层页表基址。
  2. 内层页号 → 查内层页表(第二次访存),获得页框号。
  3. 页框号 × 页大小 + 偏移量 → 物理地址(第三次访存,取数据)。

两级页表的地址变换需要三次访存——比单级页表的基本分页多一次。但 TLB 的存在使这个问题基本可以忽略:98%+ 的命中率意味着绝大多数访问绕过了两级查表。

考题类型:给定两级页表和逻辑地址,求物理地址

这是 408 统考的高频题型。解题步骤:

  1. 从逻辑地址中提取外层页号、内层页号、偏移量。
  2. 查页目录,获得内层页表的物理基址。
  3. 以内层页号为索引,查内层页表,获得页框号。
  4. 物理地址 = 页框号 × 页大小 + 偏移量。

如果某一步遇到"页表项不存在"(页目录项为空),则访问越界或触发缺页中断。

💡 实践建议:解题时注意区分"外层页号"和"内层页号"——它们的位数可以不同(不一定都是 10 位),具体取决于题目给定的地址结构。动手算之前先画出地址拆分图。

多级页表的一般化

对于 64 位系统,两级已经不够。Linux 实际使用四级页表(PGD → PUD → PMD → PTE),但基本原理和两级页表完全一样——每一级都是"按需创建"的目录结构。考研只需要掌握两级,但理解了这个思想后去看 Linux 内核的 mm_structpgd_t 就不会觉得突兀。


基本分段——按逻辑单元切分内存

分段的出发点

分页是从系统视角看问题:把物理内存切碎,方便管理。分段是从用户视角看问题:程序天然就是分段的——有代码、有数据、有栈。为什么不让内存分配也遵循这个逻辑结构?

分段(Segmentation)将进程的地址空间划分为若干个大小不等的段(Segment),每段是一个逻辑上完整的单元——比如主程序段、子程序段、数据段、栈段。每个段在物理内存中占据一片连续区域,但各段之间可以不连续。

分段设计中有个被低估的亮点:地址是二维的。在分页系统中,逻辑地址是一个一维的编号(你只给出一个地址值,系统从里面拆出页号)。在分段系统中,逻辑地址由两个显式部分组成:段号 S段内偏移量 W。这个"二维性"使分段天然支持按逻辑单元进行权限控制——给代码段设只读、给数据段设读写——比分页的"按页设权限"更贴近程序员的语义直觉。

段表与地址变换

与页表类似,分段也有 段表(Segment Table)。每个段表项包含:

  • 段基址(Base):该段在物理内存中的起始地址。
  • 段长(Limit):该段的长度(用于越界检查)。

地址变换流程:

  1. 从逻辑地址提取段号 S 和偏移量 W。
  2. 检查 S 是否越界(是否超过段表长度)。
  3. 查段表,获得段基址 B 和段长 L。
  4. 检查 W 是否超过 L(段内越界检查)。若 W ≥ L,触发越界中断。
  5. 物理地址 = B + W。

段表中的段长字段天然提供了越界保护——任何对段外地址的访问都会被硬件拦截。这在分页中需要通过单独的页表权限位来实现,没有分段这么"自然"。

分段的优势与劣势

优势

  • 逻辑清晰:每个段对应程序的自然结构,方便程序员理解和调试。
  • 便于共享:只需要在段表中让两个进程的某个段指向同一片物理内存。
  • 保护自然:段的长度字段天然定义了访问边界,代码段可以设为只读。

劣势

  • 外部碎片:各段大小不等,随着分配和释放会产生无法利用的零碎空间——和动态分区的问题一模一样。
  • 段大小不同,内存分配算法退化为动态分区分配(FF/BF/WF),碎片问题随之回归。

分页 vs 分段——本质区别

维度 分页 分段
划分依据 系统视角:定长切分,与程序逻辑无关 用户视角:按程序的逻辑单元划分
地址维度 一维(单一地址值) 二维(段号 + 段内偏移)
大小 等长(4KB / 2MB / 1GB) 不等长(取决于段的内容)
碎片类型 内部碎片(最后一页的页内浪费) 外部碎片(段间空隙)
共享与保护 按页精细控制 按段自然控制
程序员可见性 不可见(完全由 OS+硬件处理) 可见(程序可以指定段)
地址变换 一次查表(基本分页),两次访存 一次查表,两次访存

分页之所以在通用操作系统中胜出,核心原因是:外部碎片是不可接受的——你不能让系统因为"没有连续的足够大空间"而无法为进程分配内存。分页从根子上消解了这个问题——任意物理页框都可以分配给任意逻辑页。而分段保留了外部碎片问题,这个代价在实际系统中是无法接受的。


段页式——取二者之长的妥协方案

设计思想

段页式存储管理(Segmented Paging)的层级逻辑是:先按程序的逻辑结构分段,然后在每个段内部再分页。

这意味着:

  • 从程序员视角看:每个程序的地址空间仍是分段的,段号 + 偏移量这对"二维"地址概念保留。
  • 从物理内存管理视角看:每个段内部被分页管理,段之间不需要连续,段内部也不需要连续——彻底消灭了外部碎片。

三层地址结构与地址变换

段页式中的逻辑地址包含三部分:

|←—— 段号 S ——→|←—— 页号 P ——→|←—— 偏移量 W ——→|

地址变换流程:

  1. 段号 S → 查段表(第一次访存),获得该段的页表基址。
  2. 页号 P → 查页表(第二次访存),获得页框号 F。
  3. 物理地址 = F × 页大小 + 偏移量 W → 访问数据(第三次访存)。

三次访存——这是段页式方案最明显的代价。同样可以通过 TLB 来缓解——TLB 直接缓存 (段号, 页号) → 页框号的映射,命中时只需一次访存。

段页式中的各寄存器

  • 段表寄存器:存放当前进程的段表在内存中的起始地址和段表长度。
  • 段表项中有该段的页表基址和页表长度,用于后续的页内查找。
  • 每个段有自己独立的页表——所以段页式需要多张页表。

工程实例——Intel x86 的选择

Intel x86 架构的内存管理演化,就是一本活生生的"分页 vs 分段"史书。在 32 位保护模式下,x86 同时支持分段和分页:逻辑地址先经过分段单元转换为线性地址,线性地址再经过分页单元转换为物理地址。

但在 x86-64(64 位长模式)中,Intel 基本废弃了分段机制。代码段、数据段、栈段的段基址被强制设为 0,段长被设为最大值(段的边界检查实际上被绕过了)。地址转换变成"逻辑地址 = 线性地址",然后直接进入分页单元。

这个演化释放了一个明确的工程信号:分段的保护功能可以通过分页的页表权限位来等价实现,而分段引入的外部碎片和额外的硬件复杂度是不值得的。 分段的思想在某些安全机制中仍有影子(如 Intel MPK——Memory Protection Keys——允许按"域"控制页面访问权限),但作为通用的内存管理方案,分页已经全面胜出。


FAQ:常见问题速查

Q: 两级页表中,外层页表为什么不能也被部分地创建?

原则上可以——这就是三级页表的思想。外层页表本身也可以被进一步拆分。64 位系统中的四级页表就是这个逻辑递推的结果:每一层目录本身都是"按需创建"的,最顶层(PGD)才必须常驻。考研阶段只掌握两级,但理解了"多级拆分 → 按需驻留"这个核心思想,任意级数都可以类比。

Q: 段页式比纯分页的开销大那么多,为什么还要学它?

因为 OS 理论的内在逻辑:分段解决"逻辑表达",分页解决"碎片管理"——段页式是"把两件事都做到"的尝试,代表了非连续分配能达到的最高抽象层。即便现代 OS 已经不使用硬件分段,理解段页式可以帮你判断:在你的设计中,哪些保护应该靠地址空间结构,哪些应该靠权限位。

Q: 三种方案分别需要几次访存?

基本分页(单级页表):2 次。两级页表:3 次。基本分段:2 次。段页式:3 次。所有数字都是"在没有 TLB 的情况下"的理论值;TLB 命中时都只需要 1 次。


总结

两级页表用"多级目录+按需创建"解决了单级页表的内存浪费问题。分段从用户视角出发,按逻辑单元划分内存,提供自然的共享与保护能力。段页式试图融合二者精华,但三次访存的代价暴露了"万能方案"的典型困境。

在工业实践中,分页最终成为胜出者。但分段的思想并没有消失——它存在于内存保护、安全隔离等更专门的领域。理解了分页、分段和段页式的演进逻辑,你对操作系统内存管理设计的决策图谱就有了完整的认知。

下一篇,我们将进入操作系统内存管理最核心的概念之一:虚拟内存。

[INTERNAL-LINK: 虚拟内存——从局部性原理到请求分页 → 第三章第 4 篇]


📚 延伸阅读


Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐