# 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 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_虚拟存储器]]
