Files
obsidian/操作系统/15_段式存储管理/15_段式存储管理.md

399 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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["**越界中断**<br/>(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查段表<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
```
### 段页式地址变换步骤
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 x8632位保护模式 |
---
## 九、小结
```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_虚拟存储器]]