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

12 KiB
Raw Blame History

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

详细步骤

  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

六、分段的优缺点

优点

优点 说明
符合程序逻辑 按代码、数据、栈等逻辑单元组织,便于理解和管理
便于共享 以段为单位共享,粒度合理(如共享整个代码段)
便于动态链接 可以按段为单位进行动态链接和装入
支持动态增长 堆、栈等段可以独立扩展,不影响其他段
保护自然 每段可设置独立的访问权限(代码段只读、数据段可读写等)

缺点

缺点 说明
外碎片 段长不等,内存分配/回收后产生不连续的空闲区
段长限制 每段最大长度受地址结构限制
内存紧缩开销 消除外碎片需要移动段(类似动态分区的紧凑操作)

七、段页式存储管理

基本思想

段页式 = 分段 + 分页,兼具两者的优点:

  • 先按逻辑结构分段(保留分段的优点:共享、保护、逻辑清晰)
  • 再将每段按固定大小分页(保留分页的优点:消除外碎片)
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

段页式地址变换步骤

  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位保护模式

九、小结

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. 对比分析: 在什么场景下分段优于分页?在什么场景下分页优于分段?


关联笔记


上一讲: 14_分页存储管理 下一讲: 16_虚拟存储器