# 15. 段式存储管理 > **课程**: 操作系统 - 存储器管理 > **核心内容**: 分段引入原因、分段思想、地址结构、段表、地址变换、段页式存储管理 --- ## 前置知识 - [[13_存储管理基础]] — 存储器层次结构、逻辑地址与物理地址 - [[14_分页存储管理]] — 分页思想、页表、地址变换 --- ## 一、为什么需要分段 [[14_分页存储管理|分页]]虽然解决了碎片问题,但在以下场景中存在不足: ### 1. 信息共享不方便 分页按固定大小划分,不考虑程序的逻辑结构。如果要共享一段代码(如共享库函数),该代码可能跨越多个页面,其中某些页面还包含不需要共享的数据,导致共享粒度过粗。 ``` 分页视角(按固定大小切分,不考虑逻辑含义): ┌────────┐ ┌────────┐ ┌────────┐ │ 代码1 │ │ 代码2+ │ │ 数据 │ ← 一个逻辑模块跨了3个页 │ │ │ 常量 │ │ │ 共享时会把不相关的内容也共享了 └────────┘ └────────┘ └────────┘ ``` ### 2. 动态链接问题 动态链接需要在运行时将目标模块装入内存并链接。分页系统中,模块的装入和链接以页为单位,不够灵活。 ### 3. 程序员视角需求 程序员编写程序时,自然地将代码划分为**代码段、数据段、堆栈段**等逻辑模块。分页完全打乱了这种逻辑结构。 > **核心需求**: 地址空间的划分应该按照**逻辑意义**进行,而非固定大小。这就是分段的思想。 --- ## 二、分段的基本思想 分段按照程序的**逻辑结构**将地址空间划分为若干个段: - 每个段有独立的**段名**和**段长** - 每个段在内存中**连续存放** - 不同段之间**不需要连续** ```mermaid flowchart TB subgraph VA["进程虚拟地址空间"] direction TB S0["主程序段 (段0)"] S1["子程序段 (段1)"] S2["数据段 (段2)"] S3["栈段 (段3)"] end subgraph PM["物理内存"] direction TB M0["区域A"] M1["区域B"] M2["区域C"] M3["区域D"] M4["空闲"] M5["区域E"] end S0 -->|"段表映射"| M5 S1 -->|"段表映射"| M0 S2 -->|"段表映射"| M2 S3 -->|"段表映射"| M3 style VA fill:#e3f2fd,stroke:#1976d2 style PM fill:#fff8e1,stroke:#f9a825 ``` ### 分段 vs 分页 | 对比项 | 分页 | 分段 | |--------|------|------| | **划分依据** | 固定大小(物理需要) | 逻辑意义(程序员视角) | | **段/页大小** | 所有页等大 | 各段长度不同 | | **地址空间** | 一维(VA直接计算VPN和偏移) | **二维**(段号 + 段内偏移) | | **碎片类型** | 内碎片(页内浪费) | 外碎片(段间空闲区) | | **共享** | 以页为粒度,较粗 | 以段为粒度,符合逻辑 | | **动态增长** | 不方便 | 方便(如堆、栈段可动态扩展) | --- ## 三、地址结构 分段系统的逻辑地址是**二维**的,由两部分组成: ``` 逻辑地址 = (段号 S, 段内地址 d) ``` ``` 逻辑地址表示: ┌──────────────┬────────────────────┐ │ 段号 S │ 段内地址 d │ │ (高位部分) │ (低位部分) │ └──────────────┴────────────────────┘ 注意: 不同于分页的VPN|VPO可以统一计算, 分段中 d 的取值范围取决于段长,各段不同。 ``` **地址表示示例**: - 分页地址 `(0x1A8F)` → 可直接算出 VPN=6, VPO=0x28F - 分段地址 `(2, 0x100)` → 段号=2,偏移=0x100(需要查段表才知道该段的基址和长度) --- ## 四、段表 ### 段表结构 每个进程拥有一张**段表**,段表以**段号**为索引,每项包含: | 字段 | 含义 | |------|------| | **段号** | 段的编号(隐含在索引中) | | **段长 (Limit)** | 该段的长度(字节数),用于越界检查 | | **段基址 (Base)** | 该段在物理内存中的起始地址 | ``` 段表示例: ┌──────┬────────────┬────────────────┐ │ 段号 │ 段长(Limit) │ 基址(Base) │ ├──────┼────────────┼────────────────┤ │ 0 │ 0x2000 │ 0x4000 │ ← 主程序: 在内存0x4000处, 长8KB │ 1 │ 0x1000 │ 0x8000 │ ← 子程序: 在内存0x8000处, 长4KB │ 2 │ 0x3000 │ 0xB000 │ ← 数据段: 在内存0xB000处, 长12KB │ 3 │ 0x1800 │ 0x2000 │ ← 栈段: 在内存0x2000处, 长6KB └──────┴────────────┴────────────────┘ ``` 段表基址寄存器 **STBR** 指向段表在内存中的起始地址。 --- ## 五、地址变换过程 ### 变换流程 ```mermaid flowchart TD A["逻辑地址 (S, d)"] --> B["用STBR找到段表基址"] B --> C["定位段表项: 段表基址 + S × 段表项大小"] C --> D{"d < Limit ?"} D -->|"是"| E["物理地址 PA = Base + d"] D -->|"否"| F["**越界中断**
(Segmentation Fault)"] E --> G["访问物理内存"] style F fill:#ffcdd2,stroke:#c62828 style D fill:#fff3e0,stroke:#e65100 ``` ### 详细步骤 1. **提取段号和偏移**: 从逻辑地址中分离出段号 S 和段内地址 d 2. **查段表**: 用 STBR + S 定位到第 S 个段表项 3. **越界检查**: 比较 d 与 Limit - 若 d >= Limit,触发**越界中断**(Segmentation Fault) - 若 d < Limit,继续 4. **计算物理地址**: PA = Base + d 5. **访问内存**: 用物理地址访问实际内存 ### 地址变换计算示例 > **例题**: 某分段系统,段表如下。求以下逻辑地址对应的物理地址: > - (0, 0x1500) > - (1, 0x2000) > - (2, 0x0800) **解题过程**: | 逻辑地址 | S | d | Limit | 比较 | Base | PA = Base + d | 结果 | |---------|---|---|-------|------|------|--------------|------| | (0, 0x1500) | 0 | 0x1500 | 0x2000 | 0x1500 < 0x2000 ✓ | 0x4000 | 0x4000 + 0x1500 | **0x5500** | | (1, 0x2000) | 1 | 0x2000 | 0x1000 | 0x2000 >= 0x1000 ✗ | — | — | **越界中断** | | (2, 0x0800) | 2 | 0x0800 | 0x3000 | 0x0800 < 0x3000 ✓ | 0xB000 | 0xB000 + 0x0800 | **0xB800** | --- ## 六、分段的优缺点 ### 优点 | 优点 | 说明 | |------|------| | **符合程序逻辑** | 按代码、数据、栈等逻辑单元组织,便于理解和管理 | | **便于共享** | 以段为单位共享,粒度合理(如共享整个代码段) | | **便于动态链接** | 可以按段为单位进行动态链接和装入 | | **支持动态增长** | 堆、栈等段可以独立扩展,不影响其他段 | | **保护自然** | 每段可设置独立的访问权限(代码段只读、数据段可读写等) | ### 缺点 | 缺点 | 说明 | |------|------| | **外碎片** | 段长不等,内存分配/回收后产生不连续的空闲区 | | **段长限制** | 每段最大长度受地址结构限制 | | **内存紧缩开销** | 消除外碎片需要移动段(类似动态分区的紧凑操作) | --- ## 七、段页式存储管理 ### 基本思想 **段页式** = 分段 + 分页,兼具两者的优点: - 先按**逻辑结构分段**(保留分段的优点:共享、保护、逻辑清晰) - 再将每段**按固定大小分页**(保留分页的优点:消除外碎片) ```mermaid flowchart TB subgraph VA["虚拟地址空间"] direction TB S0["段0 (主程序)"] S1["段1 (子程序)"] S2["段2 (数据)"] end subgraph S0P["段0 内部分页"] direction TB P0["页0"] P1["页1"] P2["页2"] end subgraph PM["物理内存"] direction TB F0["页框0"] F1["页框1"] F2["页框2"] F3["页框3"] end VA --> S0P P0 --> F2 P1 --> F0 P2 --> F3 style VA fill:#e3f2fd,stroke:#1976d2 style S0P fill:#f3e5f5,stroke:#7b1fa2 style PM fill:#fff8e1,stroke:#f9a825 ``` ### 段页式地址结构 逻辑地址由**三部分**组成: ``` 段页式逻辑地址: ┌──────────────┬──────────────┬──────────────┐ │ 段号 S │ 页号 P │ 页内偏移 d │ └──────────────┴──────────────┴──────────────┘ ``` ### 段页式地址变换 ```mermaid flowchart TD A["逻辑地址 (S, P, d)"] --> B["1. 用STBR找到段表"] B --> C["2. 用段号S查段表
得到该段的页表基址和段长"] C --> D{"P < 该段页数?"} D -->|"是"| E["3. 用页号P查页表
得到物理页框号 PPN"] D -->|"否"| F["越界中断"] E --> G["4. 物理地址 = PPN × 页大小 + d"] G --> H["访问物理内存"] style F fill:#ffcdd2,stroke:#c62828 ``` ### 段页式地址变换步骤 1. **查段表**: 用 STBR + S 找到第 S 个段表项,得到该段的**页表基址** 2. **查页表**: 用页表基址 + P 找到第 P 个页表项,得到 **PPN** 3. **拼接地址**: PA = PPN × 页大小 + d > **注意**: 段页式需要 **3 次内存访问**(段表 + 页表 + 数据),比纯分页多一次。TLB 的作用更加关键。 ### 段页式地址变换计算示例 > **例题**: 某段页式系统,页面大小 4KB。段表如下,求逻辑地址 (1, 2, 0x100) 的物理地址。 **段表**: | 段号 | 页表基址 | 段长(页数) | |------|---------|-----------| | 0 | 0x8000 | 4 | | 1 | 0xA000 | 6 | | 2 | 0xC000 | 3 | **段1的页表** (基址 0xA000): | 页号 | PPN | |------|-----| | 0 | 5 | | 1 | 2 | | 2 | 8 | | 3 | 1 | | 4 | 7 | | 5 | 3 | **解题过程**: 1. S=1, P=2, d=0x100 2. 查段表:段1的页表基址 = 0xA000,段长 = 6 页 3. P=2 < 6,合法 4. 查段1的页表:P=2 对应 PPN=**8** 5. PA = 8 × 4096 + 0x100 = 0x8000 + 0x100 = **0x8100** --- ## 八、三种存储管理方式对比 | 对比项 | 纯分页 | 纯分段 | 段页式 | |--------|--------|--------|--------| | **划分依据** | 固定大小 | 逻辑结构 | 先逻辑后固定大小 | | **地址维度** | 一维 | 二维 | 三维 | | **碎片** | 内碎片 | 外碎片 | 内碎片 | | **共享** | 以页为粒度 | 以段为粒度 | 以段为粒度 | | **内存访问次数** | 2次(页表+数据) | 2次(段表+数据) | 3次(段表+页表+数据) | | **代表系统** | Linux | 早期Multics | Intel x86(32位保护模式) | --- ## 九、小结 ```mermaid mindmap root((段式存储管理)) 分段引入 信息共享不便 动态链接需求 逻辑结构需求 分段思想 按逻辑分段 代码段/数据段/栈段 各段独立地址空间 地址结构 二维: 段号+偏移 不同于分页的一维 段表 段号/段长/基址 越界检查 STBR寄存器 地址变换 查段表→越界检查→基址+偏移 PA = Base + d 段页式 先分段再分页 三维地址: 段号+页号+偏移 兼具两者优点 ``` --- ## 思考题 1. **概念理解**: 分段和分页的根本区别是什么?为什么说分段的地址空间是二维的? 2. **地址变换**: 某分段系统有 4 个段,段表如下。求物理地址或判断是否越界: - (0, 0x800) → ? - (2, 0x5000) → ? - (3, 0x2000) → ? | 段号 | 段长 | 基址 | |------|------|------| | 0 | 0x1000 | 0x5000 | | 1 | 0x2000 | 0x8000 | | 2 | 0x4000 | 0xA000 | | 3 | 0x3000 | 0x2000 | 3. **段页式**: 为什么段页式需要 3 次内存访问?TLB 如何缓解这个问题? 4. **对比分析**: 在什么场景下分段优于分页?在什么场景下分页优于分段? --- ## 关联笔记 - [[13_存储管理基础]] — 存储器层次结构与地址空间基础 - [[14_分页存储管理]] — 分页思想、页表与地址变换 - [[16_虚拟存储器]] — 基于分段的虚拟存储器实现 --- **上一讲**: [[14_分页存储管理]] **下一讲**: [[16_虚拟存储器]]