基本分段存储管理
分页解决了碎片问题,但它是纯粹从系统管理效率出发的方案——把程序按固定大小机械地切开,切口落在哪儿完全不管。
这带来一个副作用:一个函数可能被拦腰切成两半,分在两个不相邻的页框里。 系统不在乎,但用户在乎——如果想把某个子程序共享给别的进程,或者给数据区单独设一个只读保护,按页去操作就非常别扭。
分段的出发点因此完全不同:按用户的逻辑意图来划分。
机制
段的划分
程序被划分成若干个段,每段完成一组相对完整的逻辑功能:主程序段、子程序段、数据段、栈段……
三个特点直接由这个出发点导出:
- 每段有自己的名字(编程时用段名,编译后转为段号)
- 每段从 0 开始编址,段内地址连续
- 段长由该段的实际内容决定,因而是可变的
疑问点:分段的存储块是否为可变长
命题:“按程序中实际的段来分配主存,所以分配后的存储块是可变长的。“该表述是否正确?
正确。 这正是分段与分页最本质的差别。
道理很直白:段是按逻辑功能划分的,而逻辑功能的规模本来就不一样。 一个主程序段可能 8KB,一个数据段可能 200KB,硬要求它们等长毫无意义。
因此系统只能按每个段的实际长度去内存里找一块同样大小的连续空间分配给它。分出去的块自然是可变长的。
这句话还有一个重要推论:既然是按实际大小分配连续空间,那么分段的碎片形态就和动态分区分配完全一样——没有内部碎片,但有外部碎片。
段表与地址变换
由于段长可变、各段又离散地存放在内存中,系统需要一张段表来记录每个段的位置。
段表项比页表项多一个字段:
| 段号 | 段长 | 本段在内存的起始地址(基址) |
|---|
“段长”这一项是分页所没有的——页的长度是固定的,不必记录;而段长各不相同,必须逐段记录,而且它还要用于越界检查。
逻辑地址结构:
flowchart LR LA["逻辑地址<br/><b>段号 S</b> | 段内偏移 W"]:::la STR["段表寄存器<br/>(段表始址 + 段表长度)"]:::reg C1{"S ≥ 段表长度?"}:::chk ST["查段表<br/>得<b>段长 C</b> 和<b>基址 B</b>"]:::pt C2{"W ≥ 段长 C?"}:::chk ERR["越界中断"]:::err PA["物理地址 = <b>B + W</b>"]:::pa LA --> C1 STR --> C1 C1 -->|"是"| ERR C1 -->|"否"| ST ST --> C2 C2 -->|"是"| ERR C2 -->|"否"| PA classDef la fill:#dbeafe,stroke:#2563eb,color:#1e3a5f classDef pa fill:#dcfce7,stroke:#16a34a,color:#14532d classDef reg fill:#fef3c7,stroke:#d97706,color:#78350f classDef pt fill:#fef3c7,stroke:#d97706,color:#78350f classDef chk fill:#f1f5f9,stroke:#94a3b8,color:#334155 classDef err fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
与分页最大的操作差异有两处:
① 要做两次越界检查。 先查段号是否超出段表长度,再查段内偏移是否超出段长。分页只需查第一次——因为页内偏移的位数是固定的,天然不可能超出页面大小。
② 物理地址是”加”出来的,不是”拼”出来的。 分页是 页框号 ‖ 页内偏移(拼接),分段是 基址 + 段内偏移(真正的加法)。因为段长可变,段内偏移不可能通过位截断得到。
段的共享与保护
这是分段最大的优势,也是它被发明出来的理由。
因为一个段就是一个逻辑上完整的单位,共享和保护可以以段为粒度进行:
- 共享:多个进程的段表中,让某个表项指向同一个物理段即可实现共享。被共享的段必须是可重入代码(纯代码)——执行中不修改自身,否则会互相破坏。
- 保护:可以给每个段单独设置访问权限(只读、只执行、读写)。代码段设为”只执行”,常量段设为”只读”,天然吻合。
对比之下,分页要共享或保护就很别扭:一个逻辑单元可能横跨若干页,而某一页里又可能混着两个不同逻辑单元的内容,权限没法干净地设置。
边界
分页 vs 分段:六条对比
这是本章最高频的辨析,每一条都考过:
| 分页 | 分段 | |
|---|---|---|
| 目的 | 系统管理需要:提高内存利用率、消除外部碎片 | 用户需要:便于共享、保护、动态增长 |
| 长度 | 页大小固定,由系统决定 | 段长可变,由用户程序决定 |
| 地址空间 | 一维 | 二维 |
| 碎片 | 内部碎片 | 外部碎片 |
| 对用户 | 不可见(纯系统行为) | 可见(用户自己划分) |
| 共享与保护 | 不便 | 方便 |
一维地址空间 vs 二维地址空间
这一条最抽象,也最常被考,值得单独讲清。
分页的逻辑地址是一维的:程序员只需要给出一个地址(比如 2050),硬件会自动按位截断成”页号 + 页内偏移”。页的划分对程序员完全透明,他甚至不需要知道页面有多大。
分段的逻辑地址是二维的:程序员必须显式给出段名(段号)和段内地址两个信息。因为段长可变,系统无法从一个连续编号的整数里推断出”这属于第几段”。
一句话判据:能不能只给一个数就定位?能,就是一维(分页);必须给两个数,就是二维(分段)。
这条差异的根源仍然是”长度是否固定”——固定长度才能靠位截断实现自动拆分。
分段的碎片形态与动态分区相同
因为分段是”按实际长度分配连续空间”,它继承了动态分区分配的全部碎片特性:
- 无内部碎片(要多少给多少)
- 有外部碎片(内存中散落着放不下任何段的小空隙)
- 需要紧凑来回收外部碎片
分段并没有解决分页所解决的那个问题。 它换来的是逻辑上的便利,代价是碎片问题回来了。这个矛盾正是 段页式要调和的。
段表寄存器与页表寄存器
两者完全对应:都存放表的始址和表的长度,都在进程切换时随上下文保存恢复。
差别只在段表项多了一个段长字段,用于第二次越界检查。
对照速查
| 页表项 | 段表项 | |
|---|---|---|
| 内容 | 页框号 | 段长 + 基址 |
| 越界检查次数 | 1 次(查页表长度) | 2 次(段表长度 + 段长) |
| 物理地址算法 | 拼接:页框号 ‖ 偏移 | 加法:基址 + 偏移 |
| 判据 | 分页 | 分段 |
|---|---|---|
| 谁决定长度 | 系统 | 用户 |
| 用户可见吗 | 否 | 是 |
| 几维地址 | 一维 | 二维 |
| 碎片 | 内部 | 外部 |
考点
- 段长可变,故分配的存储块可变长(这句表述是正确的)
- 段表项含”段长”,页表项没有
- 分段要做两次越界检查;分页只需一次
- 物理地址:分页靠拼接,分段靠加法
- 分页一维、分段二维,根源是长度是否固定
- 分段无内部碎片、有外部碎片,与动态分区相同
- 分段便于共享与保护,被共享的段须是可重入代码
链接
- 🏠 返回总览:操作系统第 3 章:内存管理总览
- ⬅️ 上一节:3.1.3 基本分页存储管理
- ➡️ 下一节:3.1.5 段页式存储管理
- 📖 名词库:第 3 章名词库