12 KiB
12 KiB
15. 段式存储管理
课程: 操作系统 - 存储器管理 核心内容: 分段引入原因、分段思想、地址结构、段表、地址变换、段页式存储管理
前置知识
一、为什么需要分段
14_分页存储管理虽然解决了碎片问题,但在以下场景中存在不足:
1. 信息共享不方便
分页按固定大小划分,不考虑程序的逻辑结构。如果要共享一段代码(如共享库函数),该代码可能跨越多个页面,其中某些页面还包含不需要共享的数据,导致共享粒度过粗。
分页视角(按固定大小切分,不考虑逻辑含义):
┌────────┐ ┌────────┐ ┌────────┐
│ 代码1 │ │ 代码2+ │ │ 数据 │ ← 一个逻辑模块跨了3个页
│ │ │ 常量 │ │ │ 共享时会把不相关的内容也共享了
└────────┘ └────────┘ └────────┘
2. 动态链接问题
动态链接需要在运行时将目标模块装入内存并链接。分页系统中,模块的装入和链接以页为单位,不够灵活。
3. 程序员视角需求
程序员编写程序时,自然地将代码划分为代码段、数据段、堆栈段等逻辑模块。分页完全打乱了这种逻辑结构。
核心需求: 地址空间的划分应该按照逻辑意义进行,而非固定大小。这就是分段的思想。
二、分段的基本思想
分段按照程序的逻辑结构将地址空间划分为若干个段:
- 每个段有独立的段名和段长
- 每个段在内存中连续存放
- 不同段之间不需要连续
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 指向段表在内存中的起始地址。
五、地址变换过程
变换流程
flowchart TD
A["逻辑地址 (S, d)"] --> B["用STBR找到段表基址"]
B --> C["定位段表项: 段表基址 + S × 段表项大小"]
C --> D{"d < Limit ?"}
D -->|"是"| E["物理地址 PA = Base + d"]
D -->|"否"| F["**越界中断**<br/>(Segmentation Fault)"]
E --> G["访问物理内存"]
style F fill:#ffcdd2,stroke:#c62828
style D fill:#fff3e0,stroke:#e65100
详细步骤
- 提取段号和偏移: 从逻辑地址中分离出段号 S 和段内地址 d
- 查段表: 用 STBR + S 定位到第 S 个段表项
- 越界检查: 比较 d 与 Limit
- 若 d >= Limit,触发越界中断(Segmentation Fault)
- 若 d < Limit,继续
- 计算物理地址: PA = Base + d
- 访问内存: 用物理地址访问实际内存
地址变换计算示例
例题: 某分段系统,段表如下。求以下逻辑地址对应的物理地址:
- (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 |
六、分段的优缺点
优点
| 优点 | 说明 |
|---|---|
| 符合程序逻辑 | 按代码、数据、栈等逻辑单元组织,便于理解和管理 |
| 便于共享 | 以段为单位共享,粒度合理(如共享整个代码段) |
| 便于动态链接 | 可以按段为单位进行动态链接和装入 |
| 支持动态增长 | 堆、栈等段可以独立扩展,不影响其他段 |
| 保护自然 | 每段可设置独立的访问权限(代码段只读、数据段可读写等) |
缺点
| 缺点 | 说明 |
|---|---|
| 外碎片 | 段长不等,内存分配/回收后产生不连续的空闲区 |
| 段长限制 | 每段最大长度受地址结构限制 |
| 内存紧缩开销 | 消除外碎片需要移动段(类似动态分区的紧凑操作) |
七、段页式存储管理
基本思想
段页式 = 分段 + 分页,兼具两者的优点:
- 先按逻辑结构分段(保留分段的优点:共享、保护、逻辑清晰)
- 再将每段按固定大小分页(保留分页的优点:消除外碎片)
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 │
└──────────────┴──────────────┴──────────────┘
段页式地址变换
flowchart TD
A["逻辑地址 (S, P, d)"] --> B["1. 用STBR找到段表"]
B --> C["2. 用段号S查段表<br/>得到该段的页表基址和段长"]
C --> D{"P < 该段页数?"}
D -->|"是"| E["3. 用页号P查页表<br/>得到物理页框号 PPN"]
D -->|"否"| F["越界中断"]
E --> G["4. 物理地址 = PPN × 页大小 + d"]
G --> H["访问物理内存"]
style F fill:#ffcdd2,stroke:#c62828
段页式地址变换步骤
- 查段表: 用 STBR + S 找到第 S 个段表项,得到该段的页表基址
- 查页表: 用页表基址 + P 找到第 P 个页表项,得到 PPN
- 拼接地址: 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 |
解题过程:
- S=1, P=2, d=0x100
- 查段表:段1的页表基址 = 0xA000,段长 = 6 页
- P=2 < 6,合法
- 查段1的页表:P=2 对应 PPN=8
- PA = 8 × 4096 + 0x100 = 0x8000 + 0x100 = 0x8100
八、三种存储管理方式对比
| 对比项 | 纯分页 | 纯分段 | 段页式 |
|---|---|---|---|
| 划分依据 | 固定大小 | 逻辑结构 | 先逻辑后固定大小 |
| 地址维度 | 一维 | 二维 | 三维 |
| 碎片 | 内碎片 | 外碎片 | 内碎片 |
| 共享 | 以页为粒度 | 以段为粒度 | 以段为粒度 |
| 内存访问次数 | 2次(页表+数据) | 2次(段表+数据) | 3次(段表+页表+数据) |
| 代表系统 | Linux | 早期Multics | Intel x86(32位保护模式) |
九、小结
mindmap
root((段式存储管理))
分段引入
信息共享不便
动态链接需求
逻辑结构需求
分段思想
按逻辑分段
代码段/数据段/栈段
各段独立地址空间
地址结构
二维: 段号+偏移
不同于分页的一维
段表
段号/段长/基址
越界检查
STBR寄存器
地址变换
查段表→越界检查→基址+偏移
PA = Base + d
段页式
先分段再分页
三维地址: 段号+页号+偏移
兼具两者优点
思考题
-
概念理解: 分段和分页的根本区别是什么?为什么说分段的地址空间是二维的?
-
地址变换: 某分段系统有 4 个段,段表如下。求物理地址或判断是否越界:
- (0, 0x800) → ?
- (2, 0x5000) → ?
- (3, 0x2000) → ?
段号 段长 基址 0 0x1000 0x5000 1 0x2000 0x8000 2 0x4000 0xA000 3 0x3000 0x2000 -
段页式: 为什么段页式需要 3 次内存访问?TLB 如何缓解这个问题?
-
对比分析: 在什么场景下分段优于分页?在什么场景下分页优于分段?