连续分配管理方式
连续分配指为一个进程分配一块地址连续的内存空间。三种方案按灵活性递增:单一连续分配 → 固定分区分配 → 动态分区分配。
这一节的两个重点是:碎片是怎么产生的,以及空闲分区该怎么组织才能找得快。
机制
三种连续分配方案
① 单一连续分配
内存分为系统区和用户区,用户区只放一道用户程序,独占整个用户区。
只适用于单用户单任务的操作系统(如早期的 DOS)。优点是无外部碎片、实现极简;缺点是只能一道程序,且用户区没用完的部分全是浪费。
② 固定分区分配
把用户区预先划分成若干个固定大小的分区,每个分区装一道作业。分区大小可以相等(缺乏灵活性,大作业装不下、小作业浪费)或不等(按常见作业大小划分,灵活些)。
系统需要一张分区说明表,记录每个分区的起始地址、大小、状态(已分配/未分配)。
它是最早的、也是最简单的支持多道程序的方案。 因为分区位置固定不变,可以采用静态重定位,不需要地址变换机构。
缺点是产生内部碎片——作业往往比分区小,剩下的那一块留在分区内谁也用不了。
③ 动态分区分配(可变分区分配)
不预先划分分区,而是在作业装入时,按其实际需要临时划出一块大小刚好的分区。
系统需要用空闲分区表或空闲分区链来记录当前还有哪些空闲块。
两种结构分别长什么样?
- 空闲分区表:每个空闲分区占一个表项,通常记录分区号、起始地址和分区大小。例如:
2 号空闲区|起址 1000|大小 500。- 空闲分区链:把每个空闲分区本身当作一个链表结点,在分区内保存分区大小以及前驱、后继指针,把所有空闲分区串起来。
分配或回收内存后,都要相应地修改、删除、插入或合并表项(链结点)。两者记录的信息本质相同,区别只是一个用表集中保存,一个用链把空闲区连接起来。
优点是没有内部碎片(分多少给多少)。但用久了会出现新问题:进程不断装入和退出,内存里会散落许多小而分散的空闲块,它们单独都太小放不下新作业,加起来却不小——这就是外部碎片。
解决外部碎片的手段是紧凑(拼接):把内存中的作业统统往一端挪,把零散的空闲区合并成一大块。
注意紧凑要移动已经装入的作业,因此必须采用动态重定位——移动之后只需修改重定位寄存器即可。这正好解释了为什么可变分区不能用静态重定位。
内部碎片与外部碎片
这一对是本章贯穿始终的概念,务必分清:
- 内部碎片:已经分配给某进程、但该进程用不上的那部分空间。它在分区内部。
- 外部碎片:尚未分配给任何进程、但太小以致无法利用的空闲区。它在各分区之间。
判据一句话:这块空间是不是已经归属某个进程了? 归属了却用不着,是内部;没归属却用不上,是外部。
各方案的碎片情况:
| 方案 | 内部碎片 | 外部碎片 |
|---|---|---|
| 单一连续分配 | 有 | 无 |
| 固定分区分配 | 有 | 无 |
| 动态分区分配 | 无 | 有 |
| 分页 | 有(最后一页) | 无 |
| 分段 | 无 | 有 |
| 段页式 | 有(每段最后一页) | 无 |
规律很清晰:按”固定大小的块”分配就有内部碎片,按”实际需要的大小”分配就有外部碎片。 分页按固定的页框分配,所以像固定分区;分段按段的实际长度分配,所以像动态分区。
基于顺序搜索的动态分区分配算法
系统把所有空闲分区串成一条链,分配时从链上找一块合适的。四种找法:
首次适应(First Fit)——空闲分区按地址递增排序,每次从链头开始找,找到第一个够大的就用。
综合性能最好。原因是它倾向于优先使用低地址部分的空间,从而在高地址端保留下大块的连续空闲区。缺点是低地址端会不断被切割出小碎片,而每次查找又都要从头扫过这些碎片。
邻近适应 / 循环首次适应(Next Fit)——与首次适应相同,但每次从上次找到的位置继续往下找,而不是回到链头。
设计动机是避免每次都扫描低地址端那堆小碎片。但它带来一个副作用:空闲区被均匀地消耗,高地址端的大块也会被切碎,反而使大作业更难被满足。综合性能通常不如首次适应。
最佳适应(Best Fit)——空闲分区按容量递增排序,每次找能满足要求的最小分区。
名字叫”最佳”,实际效果并不好:它总是切出尽可能小的剩余部分,于是产生大量难以利用的外部碎片。
最坏适应(Worst Fit)——空闲分区按容量递减排序,每次用最大的那块。
想法是切完之后剩下的部分还够大、还能用。但代价是大块空闲区被迅速消耗殆尽,之后大作业就没地方放了。
四者的排序结论:首次适应综合最好,其余三个各有明显缺陷。这个结论有点反直觉(名字最好听的”最佳适应”其实很差),是选择题的常客。
基于索引搜索的动态分区分配算法
疑问点:基于索引搜索的分配算法
动态分区分配中的”基于索引搜索的分配算法”较为抽象,其与顺序搜索的本质区别何在?
两者的本质区别只有一个:要不要遍历。
顺序搜索的问题在于只有一条链。系统里空闲分区一多(大型系统可能有成千上万个),每次分配都要从头扫过去,开销随分区数线性增长。
索引搜索的思路是:别把空闲分区堆在一条链上,按大小分类,为每一类建一个索引项。 分配时先查索引表定位到”该找哪一类”,直接取该类链上的第一块即可,完全不用遍历比较。
这就是”索引”二字的全部含义——用一张按大小分类的表,把”线性查找”换成”直接定位”。
三种具体实现:
① 快速适应算法
按容量大小把空闲分区分成若干类(例如 2KB、4KB、8KB、16KB……),每一类单独拉一条链,再用一张索引表记录每条链的表头指针。
分配时:根据请求大小找到能满足要求的最小那一类,直接取该链的第一块,整块给出去。
- 优点:查找极快(几乎是 O(1));而且因为整块分配、不切割,不产生外部碎片。
- 缺点:分区回收时要判断和合并伙伴,算法复杂;且由于整块给出,会产生内部碎片(请求 5KB 却给了 8KB)。
② 伙伴系统(见下一小节详述)
③ 哈希算法
以空闲分区的大小为关键字建立哈希表,表项指向对应的空闲分区链。分配时对请求大小做一次哈希运算,直接定位到链表头。
思想与快速适应一致,只是把”查索引表”换成了”算哈希”,进一步加快了定位。
伙伴系统
规则:所有分区的大小都是 2 的幂(
- 分配:请求大小为
时,找到满足 的最小 块。若该大小的空闲块不存在,就找更大的块对半劈开,劈出的两半互为伙伴;不断劈,直到得到 大小的块为止。 - 回收:释放一块时,检查它的伙伴是否也空闲。若是,两者合并成更大的块;合并后继续向上检查能否再合并,直到不能为止。
“伙伴”的严格定义是:大小相同、地址连续,且是由同一个大块一分为二得来的那一对。注意”大小相同且地址相邻”还不够——必须来自同一次划分,所以第 1 块和第 2 块是伙伴,但第 2 块和第 3 块不是。
flowchart TB A["<b>1024K</b> 空闲"]:::free A -->|"请求 100K,无合适块,对半劈"| B1["512K"]:::free A --> B2["512K"]:::free B1 -->|"仍太大,再劈"| C1["256K"]:::free B1 --> C2["256K"]:::free C1 -->|"仍太大,再劈"| D1["<b>128K</b> ← 分配给请求"]:::used C1 --> D2["128K 空闲<br/>(D1 的伙伴)"]:::free classDef free fill:#dbeafe,stroke:#2563eb,color:#1e3a5f classDef used fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
请求 100K 时,最终拿到一个 128K 的块,其中 28K 是内部碎片——这是伙伴系统必然的代价。
伙伴系统与”离散分配”的关系
疑问点:伙伴算法出现在"连续分配"一节,但 Linux 用它管理页框,两者是否矛盾
教材原文:“Linux 系统采用 3.1.2 节介绍的’伙伴算法’对内存中不同长度的连续空闲页框进行统计和记录……”
疑问在于:连续分配与离散分配似乎被混在了一起。
不矛盾。这个困惑源于把两个不同层面的”连续”当成了同一件事。
先把两个视角拆开:
视角一:进程看到的地址空间——这里谈”连续 / 离散”。
分页管理的意义在于:进程的逻辑地址空间可以被拆散,映射到内存中任意位置的、互不相邻的物理页框上。 进程自己感觉地址是连续的,实际存放却是离散的。这就是”离散分配”。
视角二:物理页框本身的分配——这里的”连续”是另一回事。
即使采用了分页,操作系统内核仍然经常需要一批”物理上连续的页框”,典型场景有三类:
- DMA 传输:外设直接读写内存时不经过 MMU,看到的是物理地址,因此缓冲区必须物理连续
- 内核数据结构:内核自身的很多结构要求物理连续,以便直接用物理地址访问
- 大页:一次映射一大片,需要底层物理页框连续
伙伴算法服务的正是视角二。 它的职责是:管理系统中所有的空闲物理页框,并且能够高效地分出一批连续的页框(
所以两句话就能理清:
“离散分配”说的是——进程的逻辑页可以散落到任意物理页框。 “伙伴算法”说的是——物理页框这个资源池本身,要怎么切分和合并才能保证随时拿得出连续的一批。
它们不在一个层面上,因此不冲突。 教材把伙伴算法放在 3.1.2,是因为它在算法形态上属于”连续分配”这一族(它分出去的是一整块连续空间);而 Linux 用它管理页框,用的也正是这个能力。
补一句:分页解决的是”进程不必占用连续内存”,但它并没有消灭”某些时候就是需要连续物理内存”这个需求。 现代内核里两套机制是并存的——伙伴系统管连续页框块的分配,页表管逻辑到物理的离散映射。
边界
首次适应为什么反而最好
这是最反直觉的一条。四种算法里,名字最谦虚的”首次适应”综合性能最优,名字最好听的”最佳适应”很差。
原因在于它们对”大块空闲区”的保护程度不同:
- 首次适应总是从低地址开始用,高地址端的大块得以保留,将来大作业还有地方放。
- 最佳适应每次都精确切割,剩下的零头越来越小,很快积累出大量无法利用的外部碎片。
- 最坏适应专挑大块下手,大块被最快消耗光。
- 邻近适应让消耗均匀分布,结果是所有大块都被切碎。
判据:好的分配算法应当尽量少地破坏大块连续空间。
紧凑的前提是动态重定位
紧凑要移动内存中的作业。如果作业用的是静态重定位,地址已经写死在代码里,一移动就全错了。
因此动态分区分配必须配合动态重定位。 这也是”固定分区可以用静态重定位、可变分区不能”的根本原因。
内部碎片 vs 外部碎片:谁能被紧凑消除
紧凑只能消除外部碎片,对内部碎片无能为力。
因为内部碎片已经被算作某个进程的合法领地,操作系统无权也无法把它抠出来——它就在进程的分区里面。而外部碎片属于系统的空闲空间,把作业挪一挪就能拼起来。
快速适应 / 伙伴系统的碎片类型变了
顺序搜索的四种算法(FF/NF/BF/WF)都是按需切割,因此只有外部碎片。
而快速适应和伙伴系统是按预设的档位整块分配(8KB、
这是索引搜索类算法为了”查得快”付出的代价。
对照速查
| 方案 | 分区划分 | 内部碎片 | 外部碎片 | 可否静态重定位 |
|---|---|---|---|---|
| 单一连续 | 用户区只放一道 | 有 | 无 | 是 |
| 固定分区 | 预先划定,大小固定 | 有 | 无 | 是 |
| 动态分区 | 装入时按需划出 | 无 | 有 | 否(要紧凑) |
| 顺序搜索算法 | 排序方式 | 每次取 | 主要问题 |
|---|---|---|---|
| 首次适应 FF | 地址递增 | 第一个够大的 | 综合最好;低址碎片多 |
| 邻近适应 NF | 地址递增(循环) | 从上次位置继续 | 大块被均匀切碎 |
| 最佳适应 BF | 容量递增 | 最小的够用块 | 产生大量小碎片 |
| 最坏适应 WF | 容量递减 | 最大的块 | 大块很快耗尽 |
| 索引搜索算法 | 组织方式 | 特点 |
|---|---|---|
| 快速适应 | 按容量分类,每类一条链 + 索引表 | 查找极快,整块分配→有内部碎片 |
| 伙伴系统 | 块大小均为 | 便于分出连续页框块,有内部碎片 |
| 哈希算法 | 以容量为键建哈希表 | 定位最快 |
考点
- 内部碎片 vs 外部碎片的判据(是否已归属某进程)及各方案的碎片类型
- 首次适应综合性能最好,最佳适应产生大量小碎片
- 紧凑只能消除外部碎片,且必须配合动态重定位
- 索引搜索的本质是”按大小分类建表,免去遍历”
- 伙伴系统的伙伴定义:大小相同、地址连续、同源于一次划分
- 伙伴算法管的是物理页框的连续性,与”进程离散分配”不在一个层面
链接
- 🏠 返回总览:操作系统第 3 章:内存管理总览
- ⬅️ 上一节:3.1.1 内存管理的基本原理和要求
- ➡️ 下一节:3.1.3 基本分页存储管理
- 📖 名词库:第 3 章名词库