分段
1. 背景:简单内存管理的局限性
1.1 传统方式:基址/界限寄存器
- 机制:将进程的整个地址空间加载到物理内存的一个连续区域。使用一对基址(Base)和界限(Bounds/Limit)寄存器进行地址转换和保护。
- 优点:实现简单,支持进程重定位。
- 缺点:
- 内存浪费:进程虚拟地址空间中未使用的部分(特别是堆和栈之间的大量“空闲”区域)仍然占用物理内存。
- 灵活性差:如果物理内存没有足够大的 连续 空间来容纳整个地址空间,即使总空闲内存足够,进程也无法运行。
- 不支持稀疏地址空间:对于需要很大虚拟地址范围(如 32 位/4GB)但实际只使用其中一小部分(几 MB)的程序,效率极低。
1.2 核心问题
- 如何在物理内存有限的情况下,高效支持 大且稀疏 的虚拟地址空间?
2. 分段 (Segmentation) 概念
2.1 核心思想
- 不再将整个地址空间视为一个整体,而是将其划分为多个逻辑上独立的 段 (Segment)。
- 典型的段包括:代码段 (Code)、堆段 (Heap)、栈段 (Stack)。
- 每个段是虚拟地址空间内的一个 连续 区域,但各段在物理内存中 不必连续 存放。
2.2 硬件支持
- MMU (内存管理单元) 中包含 多对 基址/界限寄存器,每一对 对应地址空间中的一个逻辑段。
- 每个段寄存器对至少记录:
- 基址 (Base):该段在物理内存中的起始地址。
- 界限/大小 (Limit/Size):该段的长度。
2.3 主要优势
- 节省物理内存:只有被实际使用的段才需要分配物理内存,虚拟地址空间中未使用的区域(如堆栈间隙)不再浪费物理内存。
- 支持稀疏地址空间:即使虚拟地址空间很大,只要各段的总大小能被容纳,进程就能运行。
- 灵活性:可以将不同段放置在物理内存的不同空闲区域。
3. 分段机制下的地址转换
3.1 识别段与段内偏移
- 虚拟地址结构:虚拟地址需要能被解析成
(段标识, 段内偏移)
。
- 段标识方法:
- 显式方法 (Explicit):使用虚拟地址的高位比特来指定段。例如,14 位虚拟地址,前 2 位可以指定 4 个段(00=代码, 01=堆, 10=未使用, 11=栈)。
- 隐式方法 (Implicit):根据地址产生的方式来判断段。例如,PC 产生的地址属于代码段,栈指针相关的地址属于栈段,其他属于堆段。
3.2 地址转换步骤 (以显式方法为例)
- 从虚拟地址中提取 段标识符 (Segment Identifier)。
- 根据段标识符选择对应的 段寄存器对 (获取
Base
和 Limit
)。
- 从虚拟地址中提取 段内偏移 (Offset)。
- 边界检查:检查
Offset
是否小于该段的 Limit
(0 <= Offset < Limit
)。
- 若越界,则触发 段错误 (Segmentation Fault / Violation) 异常,通常导致进程终止。
- 计算物理地址:
PhysicalAddress = Base[Segment] + Offset
。
- 访问计算出的物理地址。
3.3 栈段的特殊处理
- 问题:栈通常向下(向低地址方向)增长。
- 硬件支持:段描述符中增加一个 方向位 (Direction Bit),指示段是向上增长还是向下增长。
- 地址转换调整:
- 对于向下增长的段(如栈),偏移量的计算和边界检查需要调整。
- 示例:假设栈段虚拟地址范围 14KB-16KB,物理基址 28KB,大小 2KB,向下增长。访问虚拟地址 15KB (二进制
11 1100 0000 0000
):
- 段标识
11
指向栈段。
- 原始偏移是
3KB
(0x0C00)。
- 计算反向偏移:
NegativeOffset = Offset - MaxSegmentSize = 3KB - 4KB = -1KB
。
- 边界检查:
abs(NegativeOffset) <= Limit
(即 1KB <= 2KB
,合法)。
- 计算物理地址:
PhysicalAddress = Base + NegativeOffset = 28KB + (-1KB) = 27KB
。
4. 分段的进阶特性
4.1 支持共享 (Sharing)
- 动机:节省内存,允许多个进程共享相同的物理内存区域,特别是代码段。
- 机制:为每个段增加 保护位 (Protection Bits),如:
- 读 (Read)
- 写 (Write)
- 执行 (Execute)
- 实现:将代码段标记为“只读/可执行”。操作系统可以让多个进程的虚拟地址空间中的代码段映射到 同一块 物理内存。硬件在每次访问时检查权限,防止进程修改共享代码,保持隔离性。
4.2 分段粒度 (Granularity)
- 粗粒度 (Coarse-grained):地址空间划分为少数几个大段(如代码、堆、栈)。实现相对简单。
- 细粒度 (Fine-grained):地址空间划分为大量小段。
- 需要更复杂的硬件支持(如内存中的 段表 (Segment Table))。
- 允许更精细的内存管理和共享。
- 早期系统(如 Multics, Burroughs B5000)采用过,编译器可将代码和数据细分为多个段。
5. 操作系统对分段的支持与挑战
5.1 上下文切换 (Context Switching)
- 操作系统需要在切换进程时 保存 即将暂停进程的 所有段寄存器 的状态,并 恢复 即将运行进程的段寄存器状态。
5.2 物理内存管理
- 操作系统需要维护物理内存的空闲空间信息,并为新进程的段或现有段的增长找到合适的 空闲块。
5.3 外部碎片 (External Fragmentation)
- 定义:随着段的分配和释放,物理内存中会产生许多 不连续 的、大小不一 的小空闲块。
- 问题:即使总的空闲内存足够,也可能找不到一个足够大的 连续 空闲块来满足新的段分配请求。
- 解决方案:
- 内存紧缩 (Compaction):移动物理内存中的现有段,使它们连续存放,从而合并小的空闲块形成一个大的连续空闲区。代价高昂(需要停止进程、大量内存拷贝、更新段基址)。
- 空闲列表管理算法:使用特定算法(如
First-Fit
, Best-Fit
, Worst-Fit
, Buddy System
等)来管理空闲块,试图减少碎片的产生或影响。
- 没有完美算法:所有算法都只能 缓解 而不能完全 消除 外部碎片。存在多种算法本身就说明了没有绝对最优解。
6. 分段的总结与局限
6.1 优点 (Pros)
- 高效利用物理内存:避免为虚拟地址空间中的未使用部分分配物理内存。
- 支持稀疏地址空间。
- 逻辑分段:代码、数据、栈的分离符合程序的逻辑结构。
- 支持共享:通过保护机制可以方便地实现段(尤其是代码段)的共享。
- 快速地址转换:硬件直接执行,开销小。
6.2 缺点 (Cons)
- 外部碎片:最主要的问题,导致物理内存利用率下降,分配困难。
- 段内稀疏问题:如果一个逻辑段本身很大但内部使用稀疏(例如一个大堆),分段机制仍然要求为整个段(可能的最大尺寸)找到连续或足够大的物理空间(取决于具体实现),无法解决段 内部 的稀疏性问题。
- 段增长困难:如果一个段需要增长,可能没有相邻的空闲物理内存,需要移动段或寻找更大的空闲块。
6.3 结论
- 分段是对基址/界限方法的重要改进,提高了内存效率和灵活性,并引入了共享的可能性。
- 然而,外部碎片问题非常棘手,且其对稀疏性的支持粒度停留在段级别。
- 这些局限性促使了内存管理技术向 分页 (Paging) 或 段页式结合 (Segmentation with Paging) 的方向发展,以求更好地解决内存碎片和支持任意粒度的稀疏地址空间。