两级页表、分段与段页式:三种非连续分配方案的深度对比
两级页表、分段与段页式:三种非连续分配方案的深度对比
引言
上一篇的分页方案解决了外部碎片,但留下了两个问题:一是每进程 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 的内存开销。
两级地址变换
- 外层页号 → 查页目录(第一次访存),获得内层页表基址。
- 内层页号 → 查内层页表(第二次访存),获得页框号。
- 页框号 × 页大小 + 偏移量 → 物理地址(第三次访存,取数据)。
两级页表的地址变换需要三次访存——比单级页表的基本分页多一次。但 TLB 的存在使这个问题基本可以忽略:98%+ 的命中率意味着绝大多数访问绕过了两级查表。
考题类型:给定两级页表和逻辑地址,求物理地址
这是 408 统考的高频题型。解题步骤:
- 从逻辑地址中提取外层页号、内层页号、偏移量。
- 查页目录,获得内层页表的物理基址。
- 以内层页号为索引,查内层页表,获得页框号。
- 物理地址 = 页框号 × 页大小 + 偏移量。
如果某一步遇到"页表项不存在"(页目录项为空),则访问越界或触发缺页中断。
💡 实践建议:解题时注意区分"外层页号"和"内层页号"——它们的位数可以不同(不一定都是 10 位),具体取决于题目给定的地址结构。动手算之前先画出地址拆分图。
多级页表的一般化
对于 64 位系统,两级已经不够。Linux 实际使用四级页表(PGD → PUD → PMD → PTE),但基本原理和两级页表完全一样——每一级都是"按需创建"的目录结构。考研只需要掌握两级,但理解了这个思想后去看 Linux 内核的 mm_struct 和 pgd_t 就不会觉得突兀。
基本分段——按逻辑单元切分内存
分段的出发点
分页是从系统视角看问题:把物理内存切碎,方便管理。分段是从用户视角看问题:程序天然就是分段的——有代码、有数据、有栈。为什么不让内存分配也遵循这个逻辑结构?
分段(Segmentation)将进程的地址空间划分为若干个大小不等的段(Segment),每段是一个逻辑上完整的单元——比如主程序段、子程序段、数据段、栈段。每个段在物理内存中占据一片连续区域,但各段之间可以不连续。
分段设计中有个被低估的亮点:地址是二维的。在分页系统中,逻辑地址是一个一维的编号(你只给出一个地址值,系统从里面拆出页号)。在分段系统中,逻辑地址由两个显式部分组成:段号 S 和段内偏移量 W。这个"二维性"使分段天然支持按逻辑单元进行权限控制——给代码段设只读、给数据段设读写——比分页的"按页设权限"更贴近程序员的语义直觉。
段表与地址变换
与页表类似,分段也有 段表(Segment Table)。每个段表项包含:
- 段基址(Base):该段在物理内存中的起始地址。
- 段长(Limit):该段的长度(用于越界检查)。
地址变换流程:
- 从逻辑地址提取段号 S 和偏移量 W。
- 检查 S 是否越界(是否超过段表长度)。
- 查段表,获得段基址 B 和段长 L。
- 检查 W 是否超过 L(段内越界检查)。若 W ≥ L,触发越界中断。
- 物理地址 = B + W。
段表中的段长字段天然提供了越界保护——任何对段外地址的访问都会被硬件拦截。这在分页中需要通过单独的页表权限位来实现,没有分段这么"自然"。
分段的优势与劣势
优势:
- 逻辑清晰:每个段对应程序的自然结构,方便程序员理解和调试。
- 便于共享:只需要在段表中让两个进程的某个段指向同一片物理内存。
- 保护自然:段的长度字段天然定义了访问边界,代码段可以设为只读。
劣势:
- 外部碎片:各段大小不等,随着分配和释放会产生无法利用的零碎空间——和动态分区的问题一模一样。
- 段大小不同,内存分配算法退化为动态分区分配(FF/BF/WF),碎片问题随之回归。
分页 vs 分段——本质区别
| 维度 | 分页 | 分段 |
|---|---|---|
| 划分依据 | 系统视角:定长切分,与程序逻辑无关 | 用户视角:按程序的逻辑单元划分 |
| 地址维度 | 一维(单一地址值) | 二维(段号 + 段内偏移) |
| 大小 | 等长(4KB / 2MB / 1GB) | 不等长(取决于段的内容) |
| 碎片类型 | 内部碎片(最后一页的页内浪费) | 外部碎片(段间空隙) |
| 共享与保护 | 按页精细控制 | 按段自然控制 |
| 程序员可见性 | 不可见(完全由 OS+硬件处理) | 可见(程序可以指定段) |
| 地址变换 | 一次查表(基本分页),两次访存 | 一次查表,两次访存 |
分页之所以在通用操作系统中胜出,核心原因是:外部碎片是不可接受的——你不能让系统因为"没有连续的足够大空间"而无法为进程分配内存。分页从根子上消解了这个问题——任意物理页框都可以分配给任意逻辑页。而分段保留了外部碎片问题,这个代价在实际系统中是无法接受的。
段页式——取二者之长的妥协方案
设计思想
段页式存储管理(Segmented Paging)的层级逻辑是:先按程序的逻辑结构分段,然后在每个段内部再分页。
这意味着:
- 从程序员视角看:每个程序的地址空间仍是分段的,段号 + 偏移量这对"二维"地址概念保留。
- 从物理内存管理视角看:每个段内部被分页管理,段之间不需要连续,段内部也不需要连续——彻底消灭了外部碎片。
三层地址结构与地址变换
段页式中的逻辑地址包含三部分:
|←—— 段号 S ——→|←—— 页号 P ——→|←—— 偏移量 W ——→|
地址变换流程:
- 段号 S → 查段表(第一次访存),获得该段的页表基址。
- 页号 P → 查页表(第二次访存),获得页框号 F。
- 物理地址 = 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 篇]
📚 延伸阅读
- Operating System Concepts, 10th Edition, Chapter 9.5~9.7: Page Table Structure, Segmentation
- Intel 64 and IA-32 Architectures Software Developer’s Manual, Vol 3A, Chapter 3: Protected-Mode Memory Management, Chapter 4: Paging
- AMD64 Architecture Programmer’s Manual, Vol 2: System Programming, Chapter 5: Page Translation
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)