2.3 存储管理
操作系统存储管理全解析:从分区到段页式
一、存储器层次结构
计算机存储系统按照速度从快到慢、容量从小到大、成本从高到低,构成如下层次结构:
寄存器 → 高速缓存(Cache) → 主存(内存) → 外存(磁盘等)
CPU 可以直接访问寄存器和高速缓存,通过地址总线访问主存,而外存中的数据必须先调入主存才能被 CPU 使用。存储管理的核心对象就是主存。
📌 生活类比:把存储层次想象成你书桌上的工作空间——
- 寄存器 = 你手中正拿着的笔(最快、最小)
- Cache = 桌面上摊开的常用资料(触手可及)
- 主存 = 身后的书架(需要转身去拿)
- 外存 = 隔壁仓库(需要走过去搬运,但能存放海量物品)
二、地址重定位
程序在编译后生成的是逻辑地址(又称相对地址、虚地址),而数据实际存放在主存中的物理地址(又称绝对地址)。地址重定位就是将逻辑地址转换为物理地址的过程。
根据转换时机的不同,分为两种方式:
- 静态重定位:在程序装入主存时一次性完成所有地址转换。程序一旦装入,其在内存中的位置就不能再移动。
- 动态重定位:在程序执行过程中,由硬件地址变换机构(如基址寄存器)实时完成地址转换。程序在内存中可以移动,支持共享和保护,是现代操作系统的基础技术。
📌 生活类比:
- 逻辑地址就像"第3排第5个座位"——这是你票面上的相对编号。
- 物理地址就像"教学楼B栋3楼12号教室第5把椅子"——这是实际位置。
- 静态重定位:入场前工作人员就把票面编号全部对应到实际座位,入场后对号入座,不能换座。
- 动态重定位:入场时只告诉你"你属于A区",到了A区门口再看实时显示屏,告诉你今天A区的具体位置在哪。如果A区临时调整了位置,显示屏会更新,你跟着走就行。
三、分区存储管理
分区存储管理是一种连续分配方式,即为每个进程分配一段连续的内存空间,将进程整体装入后再执行。
3.1 三种分区方式
3.1.1 固定分区(静态分区)
操作系统在初始化时就将主存划分为若干个大小固定的分区。作业按大小装配到合适的分区中。
- 优点:管理简单,无需复杂的分配算法。
- 缺点:分区大小固定,与作业实际需求往往不一致,产生内部碎片(分区中未被利用的空间)。
📌 举例:假设内存被固定划分为 4 个分区:10K、20K、30K、40K。现在有一个 12K 的作业需要装入,只能放进 20K 的分区,剩下的 8K 就浪费了——这就是内部碎片。
3.1.2 可变分区(动态分区)
主存空间的分区不在初始化时确定,而是在作业请求装入时,根据作业的实际大小动态划分一个恰好够用的分区。
- 优点:不存在内部碎片,内存利用率比固定分区高。
- 缺点:随着作业的装入和退出,主存中会产生大量不连续的小空闲块,产生外部碎片。
📌 举例:内存共 100K,依次装入 20K、30K、25K 三个作业后,又退出了中间 30K 的作业,此时内存布局如下:
[已用20K][空闲30K][已用25K][空闲25K]空闲总量有 55K,但分散成两块。如果此时来了一个 40K 的作业,虽然总空闲够用,但没有一块连续空间能装下它——这就是外部碎片。
3.1.3 可重定位分区(动态重定位分区)
在可变分区的基础上,增加了紧凑(Compaction)功能。当外部碎片导致无法满足新作业的内存请求时,通过移动所有已分配的分区,使其向内存一端集中,从而将分散的小空闲块合并为一个大空闲块。
由于分区移动后物理地址发生了变化,必须依赖动态重定位技术来更新地址映射,因此称为"可重定位分区"。紧凑操作开销较大(需要暂停进程、移动数据、更新地址),通常只在找不到足够大的空闲分区、但空闲分区总和能满足需求时才触发。
📌 举例:继续上面的场景,内存布局为:
[已用20K][空闲30K][已用25K][空闲25K]现在来了一个 40K 的作业装不下。触发紧凑操作,把所有已分配区域向左靠拢:
[已用20K][已用25K][空闲55K]两块碎片合并为 55K 的连续空间,40K 的作业就能顺利装入了。代价是移动数据期间需要暂停所有进程。
3.2 可变分区的分配算法
当系统需要为一个作业分配内存时,需要在空闲分区表中查找合适的空闲块。常见的分配算法如下(假设需要分配 9K 空间):
📌 统一场景:假设当前内存中有以下空闲块(按地址排列):
空闲块A:12K(地址 0~11K) 空闲块B:6K (地址 20~25K) 空闲块C:20K(地址 40~59K) 空闲块D:10K(地址 70~79K)现在需要分配 9K 空间,各算法的处理结果:
| 算法 | 策略 | 选择结果 | 特点 |
|---|---|---|---|
| 首次适应法(First Fit) | 按地址从低到高,找第一个 ≥ 9K 的空闲块 | 选中空闲块A(12K),切出 9K,剩余 3K | 简单高效,优先利用低地址空间 |
| 最佳适应法(Best Fit) | 按大小从小到大排序,找最接近的空闲块 | 选中空闲块D(10K),切出 9K,剩余 1K | 找到与需求最接近的空闲块,但容易留下难以利用的微小碎片(1K) |
| 最差适应法(Worst Fit) | 按大小从大到小排序,从最大空闲块中切割 | 选中空闲块C(20K),切出 9K,剩余 11K | 分割后剩余空间仍较大(11K),减少微小碎片的产生 |
| 循环首次适应法(Next Fit) | 与首次适应法类似,但从上次分配结束的位置继续查找 | 取决于上次分配到了哪里,不会每次都从A开始 | 使内存利用更均衡,避免低地址部分被过度切割 |
四、分页存储管理
4.1 引入背景
分区存储管理(无论是固定分区还是可变分区)都属于连续分配方式,要求每个进程占用一段连续的内存空间。这带来两个严重问题:
- 碎片问题:固定分区产生内部碎片,可变分区产生外部碎片,即使紧凑技术可以缓解外部碎片,但开销巨大。
- 进程大小受限:当进程所需内存大于系统可用连续空间时,无法将整个进程装入内存运行。
为了解决这些问题,引入了分页存储管理——一种离散分配方式。它将进程的逻辑地址空间和物理内存空间都划分为固定大小的块,进程的各个页可以分散存放在不连续的物理块中,从根本上消除了外部碎片。
📌 生活类比:把一本 200 页的书(进程)放进书架(内存)。分区管理要求你必须找到一段能放下整本书的连续空位,找不到就放不了。分页管理则相当于把书拆成一页一页的散页,每页可以夹在书架上任何位置,只要有一页的空位就行——大大降低了"放不下"的概率。
4.2 基本原理
- 将进程的逻辑地址空间划分为大小相等的页(Page),从 0 开始编号。
- 将物理内存划分为同样大小的物理块(页框,Frame),从 0 开始编号。
- 通过页表建立逻辑页号到物理块号的映射关系。
📌 举例:假设页面大小为 1K,一个 4K 的进程被分为 4 页(页号 0、1、2、3)。物理内存有 8 个物理块(块号 0~7)。页表可能如下:
页号 物理块号 0 5 1 2 2 7 3 1 可以看到,进程的 4 个页分散存放在物理块 5、2、7、1 中,并不连续,但通过页表可以正确找到每一页的位置。
4.3 地址结构与转换
逻辑地址由两部分组成(页号在高位,页内偏移在低位):
逻辑地址 = 页号(Page Number) + 页内偏移(Page Offset)
物理地址 = 物理块号(Frame Number) + 页内偏移(Page Offset)
地址转换过程:
- 从逻辑地址中分离出页号和页内偏移。
- 通过页号查询页表,得到对应的物理块号。
- 将物理块号与页内偏移拼接,得到物理地址。
注意:页内偏移在逻辑地址和物理地址中是完全相同的,不需要转换。页号与物理块号之间并非按序对应,必须通过查页表来确定映射关系。
📌 具体计算举例:
假设页面大小为 1K(1024字节),逻辑地址为 3200,页表如下:
页号 物理块号 0 5 1 2 2 7 3 1 第一步:求页号和页内偏移
- 页号 = 3200 ÷ 1024 = 3(整除)
- 页内偏移 = 3200 mod 1024 = 128
第二步:查页表
- 页号 3 → 物理块号 1
第三步:计算物理地址
- 物理地址 = 1 × 1024 + 128 = 1152
所以逻辑地址 3200 对应的物理地址是 1152。
4.4 优缺点
| 优点 | 缺点 |
|---|---|
| 消除了外部碎片,内存利用率高 | 存在页内碎片(最后一页可能装不满) |
| 分配和管理简单 | 需要额外的硬件支持(页表寄存器、MMU) |
| 支持多道程序设计 | 增加系统开销(查页表的时间) |
| 在虚拟存储中可能产生抖动(Thrashing)现象 |
📌 页内碎片举例:页面大小 1K,一个 3.5K 的进程需要 4 页。前 3 页装满(3K),第 4 页只装了 0.5K,剩下 0.5K 浪费——这就是页内碎片。它无法避免,但通常很小(平均半页)。
4.5 快表(TLB)
由于每次访存都需要先查页表再访问数据,相当于两次访存,严重影响性能。为此引入了快表(Translation Lookaside Buffer, TLB)来加速地址转换。
快表是一块小容量的相联存储器(Content-Addressable Memory, CAM),集成在 MMU 中,按内容并行查找,速度极快。它存放的是当前访问最频繁的少数活动页面的页表项副本。
地址转换流程(带快表):
- CPU 发出逻辑地址,将页号送入快表并行查找。
- 快表命中:直接取出物理块号,拼接页内偏移得到物理地址,只需一次访存取数据。
- 快表未命中:访问主存中的页表(慢表)获取物理块号,同时将该页表项写入快表,再访问数据,需要两次访存。
📌 生活类比:快表就像你手机通讯录里的常用联系人。打电话时,如果对方在常用联系人里(快表命中),一点就拨出去了;如果不在(快表未命中),就得打开完整通讯录慢慢翻找(查慢表),找到后再拨号,而且下次还会把这个人加到常用联系人里。
由于程序的局部性原理,快表的命中率通常可达 90% 以上,大幅提升了地址转换效率。
五、页面置换算法
当采用请求分页的虚拟存储管理时,如果发生缺页且内存已满,需要选择一个页面换出到外存,腾出空间给新页面。常见的置换算法如下:
📌 统一场景:假设物理内存只有 3 个页框,页面访问序列为:1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5
5.1 最优算法(OPT)
- 原理:选择未来最长时间内不再被访问的页面予以淘汰。
- 特点:这是一种理论上的理想算法,需要预知未来的访问序列,实际中无法实现。它的作用是作为性能评价的基准,用来衡量其他算法的优劣。
📌 举例:当前内存中有页面 1、2、3,需要装入页面 4。查看未来访问序列
1, 2, 5, 1, 2, 3, 4, 5:
- 页面 1:未来第 1 个就会被访问
- 页面 2:未来第 2 个会被访问
- 页面 3:未来第 6 个才会被访问(最晚)
OPT 选择淘汰页面 3,因为它在未来最长时间内不会被用到。
5.2 先进先出算法(FIFO)
- 原理:淘汰最早进入内存的页面(先进先出)。
- 特点:实现简单,但效率较低。可能淘汰正在频繁使用的页面。
- Belady 异常:FIFO 可能出现一种反常现象——分配的物理块数增多,缺页率反而升高。这是 FIFO 特有的缺陷。
📌 举例(Belady 异常):访问序列
1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5
- 分配 3 个页框时,FIFO 缺页 9 次
- 分配 4 个页框时,FIFO 缺页 10 次
页框多了反而缺页更多!这就是 Belady 异常,FIFO 的"反直觉"缺陷。
5.3 最近最少使用算法(LRU)
- 原理:淘汰最近最久未被访问的页面。根据局部性原理,过去很少使用的页面,将来大概率也不会被使用。
- 特点:性能优于 FIFO,接近 OPT,是实践中广泛采用的算法。LRU 属于栈算法,不会产生 Belady 异常。但在极端工作集剧烈变化等场景下,仍有可能出现抖动,只是概率远低于 FIFO。
- 实现:需要硬件支持,可通过寄存器或栈来记录页面的访问时间。
📌 举例:当前内存中有页面 1、2、3,需要装入页面 4。查看过去的访问历史(从近到远):
3, 2, 1
- 页面 3:最近刚访问过
- 页面 2:倒数第 2 个访问的
- 页面 1:最久没访问
LRU 选择淘汰页面 1,因为它最久没被使用。
5.4 淘汰原则
在实际系统中,页面淘汰通常遵循以下优先级:
- 优先淘汰最近未被访问的页面。
- 在都未被访问的页面中,优先淘汰未被修改的页面(因为未修改的页面换出时无需写回外存,节省 I/O 开销)。
📌 生活类比:想象你的书桌(内存)只能放 3 本书。你不断需要新书:
- OPT(上帝视角):扔掉未来最久不会翻的书——但你无法预知未来。
- FIFO:扔掉最早放上桌的书——不管它现在是否还在用。
- LRU:扔掉最久没翻过的书——根据过去的习惯推测未来,最实用。
六、段式存储管理
6.1 基本原理
段式存储管理将进程的逻辑地址空间按逻辑意义划分为若干个段(Segment),如主程序段、子程序段、数据段、栈段等。每个段有独立的段名(用段号表示)和段内地址。
与分页不同,段的物理大小不固定,由段的逻辑内容长度决定。
📌 生活类比:分页就像把一本书按固定页数拆成散页(每页一样大),而分段就像按章节拆分——第一章可能 10 页,第二章可能 50 页,每章大小不同,但每章内容自成一体。
6.2 地址结构与转换
段式地址由段号和段内偏移组成:
物理地址 = 段基址 + 段内偏移
段表中的每个表项包含两个关键属性:
- 段基址(Base):该段在主存中的起始物理地址。
- 段长(Limit):该段的长度,用于越界保护——段内偏移不能超过段长,否则产生越界错误。
📌 具体计算举例:
假设段表如下:
段号 段基址 段长 0 1000 400 1 3000 200 2 5000 600 逻辑地址为 (段号=1, 段内偏移=150):
- 查段表:段号 1 → 段基址 3000,段长 200
- 越界检查:150 < 200 ✅ 合法
- 物理地址 = 3000 + 150 = 3150
若逻辑地址为 (段号=1, 段内偏移=250):
- 查段表:段号 1 → 段长 200
- 越界检查:250 > 200 ❌ 越界错误!
6.3 优缺点
| 优点 | 缺点 |
|---|---|
| 符合程序逻辑结构,便于编程 | 容易产生外部碎片(段大小不等) |
| 各段独立,修改互不影响 | 内存利用率相对较低 |
| 便于实现段的共享和保护 | 地址转换开销较大 |
| 支持动态链接 |
七、分页与分段的核心区别
| 对比维度 | 分页存储管理 | 分段存储管理 |
|---|---|---|
| 划分依据 | 按物理空间划分,对用户透明 | 按逻辑意义划分,用户可见 |
| 大小 | 每页大小固定 | 每段大小不等 |
| 主要目的 | 提高内存利用率,消除外部碎片 | 满足用户编程、共享、保护的需求 |
| 碎片类型 | 仅有页内碎片(内部碎片) | 容易产生外部碎片 |
| 地址空间 | 一维地址空间 | 二维地址空间(段号 + 段内偏移) |
📌 一句话总结:分页是系统视角——为了高效利用内存而把空间切碎;分段是用户视角——为了符合程序逻辑而把代码分类。
八、段页式存储管理
8.1 基本原理
段页式存储管理结合了分段和分页的优点:先分段,再对每个段进行分页。
- 用户程序首先按逻辑结构分为若干段。
- 每个段内部再按固定大小分为若干页。
- 物理内存被划分为固定大小的物理块。
地址结构为三维:(段号,段内页号,页内偏移)
📌 生活类比:想象一个图书馆——
- 段 = 不同类别的书架区(文学区、科学区、历史区……)
- 页 = 每个书架区内的固定编号格子
- 物理块 = 仓库里的实际储物格
你要找一本书,先确定它在哪个区(段号),再确定在第几个格子(页号),最后知道它在仓库哪个储物格里(物理块号)。
8.2 地址转换流程
- 根据段号查段表,得到该段的页表起始地址。
- 根据段内页号查该段的页表,得到物理块号。
- 将物理块号与页内偏移拼接,得到物理地址。
📌 具体计算举例:
假设段表如下:
段号 页表起始地址 页表长度 0 500 3 1 800 2 段号 0 的页表(存放在主存 500 起始的位置):
页号 物理块号 0 4 1 7 2 2 逻辑地址为 (段号=0, 页号=1, 页内偏移=200),页面大小 1K:
- 查段表:段号 0 → 页表起始地址 500
- 查页表:页号 1 → 物理块号 7
- 物理地址 = 7 × 1024 + 200 = 7384
8.3 优缺点
| 优点 | 缺点 |
|---|---|
| 兼具段式和页式的优点 | 管理复杂度最高 |
| 空间浪费小(无外部碎片) | 需要两次查表(段表 + 页表),硬件开销大 |
| 存储共享和保护容易 | 地址转换开销增加,影响执行速度 |
| 支持动态链接 | 占用的内存空间(页表等)增加 |
总结
存储管理技术从简单到复杂,经历了如下演进路径:
固定分区 → 可变分区 → 可重定位分区 → 分页 → 分段 → 段页式
(连续分配) (连续分配+紧凑) (离散分配)
每一次演进都是为了解决前一代方案的痛点:固定分区的内部碎片催生了可变分区,可变分区的外部碎片催生了可重定位分区和分页,而分页缺乏逻辑语义的不足则由分段来弥补,最终段页式将两者融合。理解这条演进线索,就掌握了操作系统存储管理的核心脉络。

浙公网安备 33010602011771号