Appearance
分段与段页式管理
概念
分段存储管理指按程序的逻辑单元(主程序段、子程序段、数据段、栈段……)把地址空间划分成若干"长度不等"的段,每段占据一块连续的内存。
一句话说清它是什么:分页是"系统按物理需要把程序切块",分段是"按程序自己的逻辑结构切块"——分页对用户透明(用户看不见页),分段对用户可见(用户知道有"代码段""数据段")。
text
分页: 系统视角 —— 程序被"机械地"切成等长的页
┌────┬────┬────┬────┬────┐ 页大小全相等, 与逻辑结构无关
│ 页0│ 页1│ 页2│ 页3│ 页4│
└────┴────┴────┴────┴────┘
分段: 用户视角 —— 程序按"逻辑意义"切成不等长的段
┌──────────┬────┬──────────────┬───────┐
│ 主程序段 │数据│ 子程序段 │ 栈段 │ 长度各不相同, 一"段"是一个完整逻辑单元
└──────────┴────┴──────────────┴───────┘⚠️ 这是全章的总纲:"页是物理单位、段是逻辑单位"——后面所有对比(是否等长、是否二维、是否共享保护、是否有外部碎片)都是从这一句推出来的。
原理
一、分段的两层地址结构(★ 与分页最大的区别)
分页的一维地址空间 vs 分段的二维地址空间:
text
分页: 一维地址空间 —— 地址 = "第几页 + 页内偏移", 页号是连续编号的
逻辑地址 ┌──────┬────────┐
│ 页号 │ 页内偏移 │ ← 页号只是一个"序号"
└──────┴────────┘
分段: 二维地址空间 —— 地址 = "段名(段号) + 段内偏移", 段号之间没有顺序含义
逻辑地址 ┌──────┬────────┐
│ 段号 │ 段内偏移 │ ← 段号是"名字", 段的长度各不相同
└──────┴────────┘"二维"的含义:分页只需一个线性地址就能定位;分段必须给出"哪一个段 + 段内多深"两个独立的量。 这就是"分页是一维、分段是二维"这句教材原话的来历。
二、段表与段地址变换
段表(segment table):每段一项,项里至少两个字段——段长 + 基址。
text
段表 (由段表寄存器指向: 段表始址 + 段表长度)
┌──────┬────────┬──────────┐
│ 段号 │ 段长 │ 基址 │
├──────┼────────┼──────────┤
│ 0 │ 1200 │ 8000 │
│ 1 │ 600 │ 4000 │
│ 2 │ 2000 │ 12000 │
└──────┴────────┴──────────┘地址变换流程(★ 两次越界检查是分段特有的):
text
逻辑地址 (段号 S, 段内偏移 W)
│
①检查 S < 段表长度 ? ── 否 ──▶ 越界中断
│ 是
②查段表第 S 项, 取出 (段长 L, 基址 B)
│
③检查 W < 段长 L ? ── 否 ──▶ 越界中断 ★ 分页没有这一步!
│ 是
④物理地址 = 基址 B + 段内偏移 W⚠️ 为什么分段要"第二次检查"而分页不用:分页的页是等长的,只要页号不越界,偏移必然在页内(偏移位数天然限制了它 < 页大小);分段的段长各不相同,必须拿段长做一次显式比较——这是"段长检查"与"页内偏移天然合法"的分野。
三、分页与分段的全面对照(本章最核心的一张表)
| 对比项 | 分页 | 分段 |
|---|---|---|
| 划分依据 | 物理单位(系统定,与逻辑无关) | 逻辑单位(按程序结构,用户可见) |
| 大小 | 固定且相等 | 不固定、各段不等 |
| 地址空间维度 | 一维(页号 + 偏移) | 二维(段号 + 偏移) |
| 对用户是否可见 | 不可见(透明) | 可见 |
| 划分者 | 系统(硬件/OS) | 编译程序 / 程序员 |
| 有无内部碎片 | 有(最后一页装不满) | 无(按需切,段内装满) |
| 有无外部碎片 | 无 | 有(段长不一,留下零头) |
| 表项内容 | 页框号(+ 有效位等) | 段长 + 基址 |
| 越界检查次数 | 1 次(页号) | 2 次(段号 + 段长) |
| 共享与保护 | 不便(页是物理切块,一个逻辑单元跨多页) | 方便(一整段就是一个逻辑单元,好共享、好设权限) |
| 主要用途 | 提高内存利用率 | 满足用户逻辑需求、便于共享与保护 |
| 访存次数(无快表) | 2 次 | 2 次 |
⚠️ 记忆钩:分页"看得见的只有碎片,看不见页";分段"看得见段,却多了外部碎片"——两者的优缺点恰好互补,这就是"段页式"出现的理由。
四、段页式管理(取两者之长)
先分段,段内再分页:用户按逻辑把程序分段,系统把每段再等分成页。
地址结构三段划分:
text
┌────────┬────────┬──────────────┐
│ 段号 │ 页号 │ 页内偏移 │
└────┬───┴────┬───┴──────────────┘
│ │
▼ ▼
段表 页表 最终页框
(每段一张页表;
段表项里存"该段页表长度 + 该段页表始址")地址变换流程(★ 需要三次访存):
text
①段号 → 查段表 (第 1 次访存) → 得到该段的"页表始址"与"页表长度"
②检查 页号 < 页表长度 ?
③页号 → 查页表 (第 2 次访存) → 得到页框号
④物理地址 = 页框号 × 页大小 + 页内偏移
⑤取数据 (第 3 次访存)⚠️ 段页式的三条口径:
- 每个进程一张段表,每个段一张页表——段表项里放的是"页表的长度和始址"。
- 代价是访存 3 次(若有快表则可降到 2 次以内)——这是"取两者之长"必须付出的账。
- 内部碎片(页内)与外部碎片(段间)都没了? 不完全——页内碎片还在(段的最后一页可能装不满),但外部碎片消失了(因为段被分页离散存放)。这是段页式最漂亮的一点。
五、三种管理方式的总账
| 方式 | 内部碎片 | 外部碎片 | 访存次数(无快表) | 对用户 |
|---|---|---|---|---|
| 连续分配(20 章) | 固定分区有 | 动态分区有 | 1 次 | 可见(程序占一段) |
| 分页(21 章) | 有(页内) | 无 | 2 次 | 不可见 |
| 分段(本章) | 无 | 有 | 2 次 | 可见 |
| 段页式(本章) | 有(页内) | 无 | 3 次 | 可见 |
示例
例 1:段地址变换手算
段表:段 0 基址
8000、段长1200;段 1 基址4000、段长600;段 2 基址12000、段长2000。 求逻辑地址(0, 0)、(0, 1199)、(1, 100)、(2, 1999)、(1, 600)的物理地址。
逐个走"两步检查 + 一次加法":
| 逻辑地址 | ①段号 < 3 ? | ②偏移 < 段长 ? | 物理地址 = 基址 + 偏移 |
|---|---|---|---|
| (0, 0) | ✔ | ||
| (0, 1199) | ✔ | ||
| (1, 100) | ✔ | ||
| (2, 1999) | ✔ | ||
| (1, 600) | ✔ | 非法:段内偏移越界 |
⚠️ 边界口径:偏移的取值范围是
—— (0,1199)合法、(1,600)非法(段长 600,最大合法偏移是 599)。"≤ 段长就放行"是本题最常见的错法。
例 2:C 实现——段表查表与越界判定
#include <stdio.h>
#define NSEG 3
struct seg_entry {
long base; /* 基址 */
long limit; /* 段长 */
};
static struct seg_entry segtab[NSEG] = {
{ 8000, 1200},
{ 4000, 600},
{12000, 2000}
};
/* 返回物理地址; -1 = 段号越界, -2 = 段内偏移越界 */
static long seg_addr(int segno, long offset) {
if (segno < 0 || segno >= NSEG) return -1; /* ① 检查段号 */
if (offset < 0 || offset >= segtab[segno].limit) return -2; /* ② 检查段长 */
return segtab[segno].base + offset; /* ③ 基址 + 偏移 */
}
int main(void) {
struct { int s; long o; } q[5] = {
{0, 0}, {0, 1199}, {1, 100}, {2, 1999}, {1, 600}
};
int i;
printf("段表:\n");
for (i = 0; i < NSEG; i++)
printf(" 段号 %d: 基址 %5ld 段长 %4ld\n",
i, segtab[i].base, segtab[i].limit);
printf("\n逻辑地址 -> 物理地址\n");
for (i = 0; i < 5; i++) {
long pa = seg_addr(q[i].s, q[i].o);
printf(" (%d, %4ld) -> ", q[i].s, q[i].o);
if (pa == -1) printf("非法: 段号越界\n");
else if (pa == -2) printf("非法: 段内偏移越界\n");
else printf("物理地址 %ld\n", pa);
}
return 0;
}
c 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
预期输出:
段表:
段号 0: 基址 8000 段长 1200
段号 1: 基址 4000 段长 600
段号 2: 基址 12000 段长 2000
逻辑地址 -> 物理地址
(0, 0) -> 物理地址 8000
(0, 1199) -> 物理地址 9199
(1, 100) -> 物理地址 4100
(2, 1999) -> 物理地址 13999
(1, 600) -> 非法: 段内偏移越界⚠️ 三点说明:
- 两个越界分支必须分开报——"段号越界"说明"程序访问了不存在的段"(通常是野指针);"段内偏移越界"说明"访问超出了本段范围"(通常是数组越界)。 这两类错误在调试里含义完全不同。
offset >= limit而不是offset > limit——边界差一的错,就是例 1 里(1,600)那一条。- 本机无 C 编译器:此段代码逐行人工审查,并用等价的 Python 实现(下例同一组数据)实跑核对过输出——两边结果完全一致。
例 3:Python——三种管理方式的对照分析
import math
PAGE = 4096
TAU = 1 << 12
print('=== ① 分页与分段: 同一个逻辑地址的两种解释 ===')
va = 0x12345678
print(' 逻辑地址 = 0x%08X' % va)
print(' 分页(一维): 页号 = 0x%X, 页内偏移 = 0x%X'
% (va >> 12, va & 0xFFF))
print(' 段页式(三维): 段号 = 0x%X, 页号 = 0x%X, 页内偏移 = 0x%X'
% (va >> 28, (va >> 12) & 0xFFF, va & 0xFFF))
print()
print('=== ② 段页式地址结构(32 位: 4 + 12 + 12) ===')
for name, bits in [('段号', 4), ('页号', 12), ('页内偏移', 12)]:
if name == '段号':
print(' %s %d 位 -> 最多 %d 个段' % (name, bits, 2 ** bits))
elif name == '页号':
print(' %s %d 位 -> 每段最多 %d 页' % (name, bits, 2 ** bits))
else:
print(' %s %d 位 -> 页大小 %d B = %d KB' % (name, bits, 2 ** bits, 2 ** bits // 1024))
print(' 每段最大 = %d 页 x %d B = %d B = %d MB'
% (2 ** 12, PAGE, 2 ** 12 * PAGE, 2 ** 12 * PAGE // 1024 // 1024))
print()
print('=== ③ 访存次数对照 ===')
for name, cnt, detail in [
('分页 / 分段(无快表)', 2, '查页表/段表 1 次 + 取数据 1 次'),
('分页 / 分段 + 快表命中', 1, '查快表(不访存) + 取数据 1 次'),
('两级页表(无快表)', 3, '查一级页表 + 查二级页表 + 取数据'),
('段页式(无快表)', 3, '查段表 + 查页表 + 取数据')]:
print(' ' + name + ': ' + str(cnt) + ' 次 (' + detail + ')')
print()
print('=== ④ 碎片与透明性总账 ===')
rows = [
('连续分配(动态分区)', '无', '有', 1, '可见'),
('分页', '有(页内)', '无', 2, '不可见'),
('分段', '无', '有', 2, '可见'),
('段页式', '有(页内)', '无', 3, '可见'),
]
for name, inner, outer, acc, vis in rows:
print(' ' + name + ': 内部碎片=' + inner + ', 外部碎片=' + outer
+ ', 访存=' + str(acc) + ' 次, 对用户' + vis)
print()
print('=== ⑤ 段地址变换(与例 2 同一组数据) ===')
seg = [(8000, 1200), (4000, 600), (12000, 2000)]
for s, o in [(0, 0), (0, 1199), (1, 100), (2, 1999), (1, 600)]:
base, limit = seg[s]
if o >= limit:
print(' (%d, %4d) -> 非法: 段内偏移越界 (合法范围 0..%d)' % (s, o, limit - 1))
else:
print(' (%d, %4d) -> 物理地址 %d' % (s, o, base + o))
python 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
输出对照(真实运行结果):
=== ① 分页与分段: 同一个逻辑地址的两种解释 ===
逻辑地址 = 0x12345678
分页(一维): 页号 = 0x12345, 页内偏移 = 0x678
段页式(三维): 段号 = 0x1, 页号 = 0x345, 页内偏移 = 0x678
=== ② 段页式地址结构(32 位: 4 + 12 + 12) ===
段号 4 位 -> 最多 16 个段
页号 12 位 -> 每段最多 4096 页
页内偏移 12 位 -> 页大小 4096 B = 4 KB
每段最大 = 4096 页 x 4096 B = 16777216 B = 16 MB
=== ③ 访存次数对照 ===
分页 / 分段(无快表): 2 次 (查页表/段表 1 次 + 取数据 1 次)
分页 / 分段 + 快表命中: 1 次 (查快表(不访存) + 取数据 1 次)
两级页表(无快表): 3 次 (查一级页表 + 查二级页表 + 取数据)
段页式(无快表): 3 次 (查段表 + 查页表 + 取数据)
=== ④ 碎片与透明性总账 ===
连续分配(动态分区): 内部碎片=无, 外部碎片=有, 访存=1 次, 对用户可见
分页: 内部碎片=有(页内), 外部碎片=无, 访存=2 次, 对用户不可见
分段: 内部碎片=无, 外部碎片=有, 访存=2 次, 对用户可见
段页式: 内部碎片=有(页内), 外部碎片=无, 访存=3 次, 对用户可见
=== ⑤ 段地址变换(与例 2 同一组数据) ===
(0, 0) -> 物理地址 8000
(0, 1199) -> 物理地址 9199
(1, 100) -> 物理地址 4100
(2, 1999) -> 物理地址 13999
(1, 600) -> 非法: 段内偏移越界 (合法范围 0..599)四条结论:
- 同一个 32 位数值
0x12345678:分页只切成"页号 + 偏移"两段;段页式切成"段号 + 页号 + 偏移"三段——这就是"一维 vs 三维"的直观样子(分段与段页式的段号都是"逻辑名字",位置在最高位只是约定)。 - 第 ③ 组把访存次数排成了一条清晰的账:1 次(快表命中)< 2 次(单级无快表)< 3 次(两级 / 段页式)——考试里凡是问"访问一个数据要几次访存",答案必在这个序列里。
- 第 ④ 组是"碎片总账":分页与段页式都"无外部碎片、有页内碎片";连续分配与分段都"有外部碎片"——段页式把外部碎片消掉的代价就是第 ⑤ 行那"3 次访存"。
- 第 ⑤ 组与例 1、例 2 三方结果一致——
(1,600)在 Python 里也判"越界,合法范围 0..599",再次确认边界是" "。
考点
考点
1. 必背结论
- 分段按逻辑单位划分、大小不等、对用户可见;分页按物理单位划分、大小相等、对用户透明。
- 分页是一维地址空间;分段是二维(段号 + 段内偏移)——这句是概念题的标答。
- 段表项至少两字段:段长 + 基址(分页项里没有"长度",因为页等长)。
- 两次越界检查:段号 < 段表长度、段内偏移 < 段长;分页只有前一种。
- 物理地址 = 段基址 + 段内偏移;合法偏移范围
(W ≥ L即越界)。 - 分段:无内部碎片、有外部碎片;分页:有内部碎片、无外部碎片。
- 段页式 = 先分段、段内分页;地址 = 段号 + 页号 + 页内偏移;每段一张页表,段表项存"该段页表长度 + 始址"。
- 访存次数:单级分页/分段 2 次;段页式 3 次;快表命中各减 1 次。
- 段页式的碎片:有页内碎片(段最后一页),无外部碎片。
- 分段便于共享与保护(一整段是一个逻辑单元,可直接设"只读/可执行");分页不便共享(一个逻辑单元会跨页,权限没法整段设)。
2. 高频陷阱
- 越界判据写成
W > L:错。应为W ≥ L(例 1 的(1,600)段长 600,就是越界)。"差一"是本篇最高频的失分点。 - 说"分段没有碎片":错。分段没有内部碎片,但绝对有外部碎片(段长不等,回收后留下零头)。
- 说"分页有外部碎片":错。分页无外部碎片,只有最后一页的页内碎片。
- 把"段页式"说成"先分页再分段":错。顺序是"先分段,段内再分页"——段号在地址高位就是顺序的体现。
- 把"段表"与"页表"的结构搞混:段表项 = 段长 + 基址;页表项 = 页框号 + 标志位(没有长度)。
- 说"段页式的访存次数与分页一样":错。段页式要 3 次(查段表 + 查页表 + 取数据),比单级分页多一次。
- 把"分页的大小由用户定":错。页大小由系统/硬件决定;段长由程序逻辑决定。
- 混淆"段表寄存器"与"段表长度":段表寄存器存"段表始址 + 段表长度"两个量,后者用于"段号越界检查"。
- 认为"分段一定比分页好"或反之:错。分段利于共享保护但产生外部碎片;分页消灭外部碎片但不利于共享。段页式才试图兼取。
- 对"段号越界"与"段内偏移越界"不作区分:答"越界"两个字可能拿不到满分——要分别指出"段号非法"和"偏移超出段长",因为对应两类不同性质的错误。
3. 解题模板("分段/段页式题")
① 抄下段表(段号, 段长, 基址), 抄下待变换的逻辑地址清单
② 每个地址走三步:
检查段号 < 段表长度? 不满足 -> "段号越界"
取该段 (段长 L, 基址 B); 检查偏移 W < L? 不满足 -> "段内偏移越界"
物理地址 = B + W
③ 若问"分页 vs 分段": 答四组对比(划分依据/大小/维度/碎片), 再补"共享保护"
④ 若问"段页式": 先写地址三段划分, 再写"3 次访存", 最后点明"无外部碎片、有页内碎片"
⑤ 若问"访问次数/EAT": 数清"查了几级表 + 取数据", 命中快表则减 14. 与相邻章节的接口
os/20-contiguous.md(连续分配):动态分区的外部碎片与本篇分段的外部碎片是同一个问题(段内要求连续)——解法也一样:紧凑(需动态重定位)。os/21-paging.md(分页):本篇的骨架是"与分页对照";段页式就是"把 21 章的页表挂到本篇章表的每一项下面"。os/23-virtual.md(虚拟内存):请求分页/请求分段是在本篇机制上再加"有效位 + 缺页处理";本章的"越界中断"与那章的"缺页中断"是两类不同事件(前者非法、后者可修复)。arch/13-virtual.md(虚拟存储器):段页式的"3 次访存"与那里"两级页表 3 次访存"的数字一致,但成因不同(一个是段表多一级,一个是页表多一级)——别把两处的"3"当成同一件事。os/32-io.md(I/O 管理):设备共享、缓冲区共享都属"共享"话题;分段"便于共享"是因为它把逻辑单元完整地留在了一段里。
小结
- 分段 = 按逻辑单元划分 + 长度不等 + 对用户可见;分页 = 按物理单位划分 + 等长 + 对用户透明。
- 一维 vs 二维:分页只需"页号 + 偏移";分段必须"段号 + 偏移",段号是"名字"不是"序号"。
- 段表项 = 段长 + 基址;地址变换 = 段号越界检查 → 段长越界检查 → 基址 + 偏移——比"分页"多出"段长检查"这一步。
- 合法偏移范围
——(1,600)在段长 600 时越界(例 1 / 例 2 / 例 3 三方一致)。 - 碎片分工:分段无内部碎片、有外部碎片;分页有内部碎片、无外部碎片——两者正好互补。
- 段页式 = 先分段、段内分页;地址 = 段号 + 页号 + 页内偏移;每段一张页表;访存 3 次(比单级多一次)。
- 段页式消掉了外部碎片(段被离散存放),但保留了页内碎片,并把访存次数推到 3 次。
- 锚点数据:段表
(8000,1200)/(4000,600)/(12000,2000)→(0,0)=8000、(0,1199)=9199、(1,100)=4100、(2,1999)=13999、(1,600)非法。 - 访存次数阶梯:快表命中 1 次 < 单级 2 次 < 两级/段页式 3 次。
下一篇:虚拟内存:请求分页、页面置换算法
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。