Skip to content

连续分配(首次/最佳/最坏适应)

2026 大纲 三(一)2 连续分配管理方式

最朴素的分法:一个进程占一整块,连着放

上一节说内存必须在空间上同时分给多个进程。最朴素的分法就是连着放—— 给每个进程划一整块连续的内存,它从这一头用到那一头。

这个做法有个不小的好处:地址变换极其简单。整块连续意味着逻辑地址和物理地址之间 只差一个固定的偏移,一个重定位寄存器做一次加法就够了,硬件成本几乎为零。

代价则要过一阵子才显出来。进程来来去去,装了又卸、卸了又装,内存就被切得七零八落。 剩下的空闲空间加起来可能还很多,但它们互不相邻,谁也装不下一个稍大的进程

所以这一节的三种方式其实是同一个矛盾被反复缓解的三个阶段, 而它们缓解到最后仍然缓解不掉的那个东西——外碎片——正是下一节"干脆别连着放"的全部动机。 读的时候盯住两条线:碎片是内的还是外的,以及它是怎么产生的

交互可视化

加载可视化中...

一、三种连续分配方式:一条被需求推着走的链

三种方式不是三条并列的路,而是同一个矛盾被反复缓解的三个阶段:单道程序只需把内存分成系统区 + 用户区(单一连续分配,用户区剩下的部分谁也用不了 ⇒ 内碎片);要跑多道就把用户区事先划成若干固定大小的分区(固定分区分配,进程装进去填不满 ⇒ 仍是内碎片,且分区数固定 ⇒ 多道程序度有上限);要让分区贴合进程实际大小就不预划分、来一个切一块(动态分区分配,切得刚好 ⇒ 无内碎片,但反复切分与归还会在已分配区之间留下空隙 ⇒ 外碎片)。

方式分区何时划定分区大小内碎片外碎片多道程序度上限
单一连续分配——整个用户区1
固定分区分配系统启动前预先划定固定(等大或不等大)分区个数
动态分区分配进程装入时按需切分随进程而变受空闲空间限制

固定分区还需要一张分区说明表,每个表项记录分区的起始地址、大小、状态(已分配/未分配),分配时按进程大小检索该表。

不等大小分区是对固定分区的改良:把用户区划成"多个小分区 + 适量中等分区 + 少量大分区",按程序大小对号入座,能明显压低内碎片。该用等大还是不等大,看作业大小是否集中——同时控制多台同型设备这类场合作业大小一致,等大分区反而更简单实用。

二、动态分区的数据结构

结构组织形式每项/每块记什么特点
空闲分区表一张顺序表,一个空闲分区占一个表目分区号、分区大小、分区始址结构简单,表长固定,分区数变化大时不方便
空闲分区链双向链表,链信息直接放在分区自己的头尾头部:状态位、分区大小、前向指针;尾部:后向指针,并重复一遍状态位和分区大小不占额外内存,分区数不受限

分区被分配出去后,头尾的前后向指针就失去意义,只有状态位仍要维护。

三、分配:四种策略

四种策略的差别只有一处:面对多个够大的空闲分区时选哪一个。为了让"选"退化成"取第一个满足的",每种策略都把空闲分区按对应顺序排好。

策略选谁空闲分区排列找的时候怎么走
首次适应(First Fit)第一个够大的地址递增每次都从链首开始扫
邻近适应 / 循环首次适应(Next Fit)第一个够大的地址递增(同上)上次分配结束的位置继续往下扫,到链尾则绕回链首
最佳适应(Best Fit)最小的够大分区大小递增从链首开始扫,第一个够大的必然最小
最坏适应(Worst Fit)最大的分区大小递减直接取表头
策略优点缺点代价的来源
首次适应综合性能最好;高地址的大空闲区被完整保留低地址不断被切,积累大量小碎片;每次从头扫,查找开销随碎片增多而上升总从低地址开始
邻近适应空闲分区分布更均匀,查找开销小大的空闲分区也会被均匀地拆掉,最终缺少大分区指针不回头,雨露均沾
最佳适应大分区被保留下来每次切下的余量都是最小的 → 产生大量极小的不可用碎片每次都追求"最贴合"
最坏适应切完剩下的块仍然较大,不易产生小碎片大分区被迅速消耗,后来的大进程无处安放每次都动最大的那块
同一序列下四种策略的碎片演化:每一步扫到哪、切了谁、最终内存是什么形状(想看清"总空闲量相等但可用性不等"是怎么发生的时展开)

某系统采用动态分区分配,当前空闲分区按地址递增依次为 A = 20KB、B = 10KB、C = 120KB、D = 60KB(四块互不相邻,之间隔着已分配区)。依次到达三个进程 P1 = 15KB、P2 = 30KB、P3 = 50KB。邻近适应的起始查找指针初始指向 A。

首次适应(按地址递增扫描):

进程扫描过程命中分配后空闲分区
P1 = 15A(20)✓AA=5 / B=10 / C=120 / D=60
P2 = 30A(5)✗ → B(10)✗ → C(120)✓CA=5 / B=10 / C=90 / D=60
P3 = 50A(5)✗ → B(10)✗ → C(90)✓CA=5 / B=10 / C=40 / D=60

每次都必须从链首重新扫,而不是接着上一次的位置——这正是首次适应与邻近适应唯一的区别,也是它把碎片压在低地址的原因。

邻近适应(按地址递增,但从上次结束处续扫):

进程扫描起点扫描过程命中分配后空闲分区
P1 = 15AA(20)✓AA=5 / B=10 / C=120 / D=60
P2 = 30BB(10)✗ → C(120)✓CA=5 / B=10 / C=90 / D=60
P3 = 50DD(60)✓DA=5 / B=10 / C=90 / D=10

指针停在上一次命中的分区之后。P3 从 D 起扫,D 够大就直接命中,根本没有回头看 C——这就是"分布均匀"的机制,也是大分区被逐个拆开的原因。

最佳适应(按大小递增,取第一个够大的):

进程够大的候选命中(最小的那个)分配后空闲分区
P1 = 15A(20)、C(120)、D(60)A(20)A=5 / B=10 / C=120 / D=60
P2 = 30C(120)、D(60)D(60)A=5 / B=10 / C=120 / D=30
P3 = 50C(120)(D 只剩 30,不够)C(120)A=5 / B=10 / C=70 / D=30

候选集必须在当前状态上重算——P2 分完后 D 只剩 30KB,已经掉出 P3 的候选集了。手算时最容易错的一步,就是拿初始表去给第二个、第三个进程找分区。

最坏适应(按大小递减,直接取最大的):

进程当前最大分区命中分配后空闲分区
P1 = 15C(120)CA=20 / B=10 / C=105 / D=60
P2 = 30C(105)CA=20 / B=10 / C=75 / D=60
P3 = 50C(75)CA=20 / B=10 / C=25 / D=60

最坏适应连"够不够大"都不用挨个比——最大的都不够,别的更不够。所以它的查找开销恒定,代价是三次全咬在同一块大分区上。

碎片演化对比:四种策略分掉的总量相同(95KB),剩余总空闲量都是 115KB,但空闲空间的形状完全不同

策略最终空闲分区最大可用块新产生的极小碎片(≤10KB)若接着来一个 P4 = 65KB
首次适应5 / 10 / 40 / 60605分配失败
邻近适应5 / 10 / 90 / 10905、10成功(用 90 那块)
最佳适应5 / 10 / 70 / 30705成功(用 70 那块)
最坏适应20 / 10 / 25 / 6060分配失败

三条能从这张表里读出来的结论:总空闲量相等不等于可用性相等(都剩 115KB,能不能装下 65KB 却分成两派);最坏适应确实没造出小碎片(切下的余量分别是 105、75、25),代价是把唯一的大分区从 120KB 啃到 25KB;这只是一条特定序列上的快照,不能拿来给算法排名。

四、回收:四种邻接情况

分配只是"切一刀",回收却要判断归还区与前后空闲区是否相邻,并决定怎么合并。只登记不合并,两块相邻的空闲区会被永远当成两块小区看待,内存越回收越碎。

情况前邻后邻表项数变化首址取谁
不变F1 的首址(不变)
不变改为回收区首址
−1(取消 F2)F1 的首址
+1(新建)回收区首址

按"首址变不变、表项数变不变"这两个维度记,四种情况互不重叠。

走一遍四种邻接情况:连续回收四个进程,每次判前后邻接并写出空闲分区表(想看清合并时机与表项数怎么变时展开)

某系统用户区为 0~600KB,采用动态分区分配。当前占用情况为:P1 占 [0, 100)、P2 占 [100, 160)、空闲 [160, 220)、P3 占 [220, 320)、P4 占 [320, 380)、空闲 [380, 460)、P5 占 [460, 560)、P6 占 [560, 600)。初始空闲分区表:[160, 220) 60KB / [380, 460) 80KB,共 2 项。现依次回收 P1、P2、P4、P5。

① 回收 P1,回收区 [0, 100)

  • 前面:回收区首址为 0,前面没有任何分区 → 不邻接
  • 后面:回收区结束于 100,表中最靠前的空闲区起址是 160 ≠ 100 → 不邻接(中间隔着 P2)

情况四:新建表项 [0, 100),按地址插到表首。表项数 2 → 3

[0, 100) 100KB / [160, 220) 60KB / [380, 460) 80KB

判"是否邻接"只看地址是否严丝合缝。100 与 160 之间隔着尚未释放的 P2,所以只能单独立项——这也说明回收顺序会影响中间过程

② 回收 P2,回收区 [100, 160)

  • 前面:[0, 100) 结束于 100 = 回收区首址 → 邻接
  • 后面:[160, 220) 起址 160 = 100 + 60 → 邻接

情况三:三块合一为 [0, 220),用前一分区的表项和首址 0,大小 = 100 + 60 + 60 = 220KB,取消 [160, 220) 的表项。表项数 3 → 2

[0, 220) 220KB / [380, 460) 80KB

情况三是唯一让表项数减少的情况。刚才被迫单独立项的 [0, 100),在 P2 归还后立刻被并了回去——合并的时机取决于邻居什么时候释放,与回收算法无关

③ 回收 P4,回收区 [320, 380)

  • 前面:[0, 220) 结束于 220 ≠ 320 → 不邻接(中间是尚未释放的 P3)
  • 后面:[380, 460) 起址 380 = 320 + 60 → 邻接

情况二:合并为 [320, 460),大小 = 60 + 80 = 140KB,首址改为回收区首址 320。表项数不变,仍为 2。

[0, 220) 220KB / [320, 460) 140KB

这是四种情况里唯一要改首址的一种。若沿用原表项的首址 380,[320, 380) 这 60KB 就凭空消失了。

④ 回收 P5,回收区 [460, 560)

  • 前面:[320, 460) 结束于 460 = 回收区首址 → 邻接
  • 后面:回收区结束于 560,而 [560, 600) 是仍在占用的 P6 → 不邻接

情况一:不新建表项,只把前一分区的大小由 140KB 改为 140 + 100 = 240KB,首址仍是 320。表项数不变,仍为 2。

[0, 220) 220KB / [320, 560) 240KB

回收完毕后:内存里只剩 P3 占 [220, 320)、P6 占 [560, 600),空闲空间被合并成两大块(220KB 与 240KB)。若这四次回收全都只登记不合并,表里会留下 6 个零散表项,最大块只有 100KB——合并这一步,才是回收的实质

五、紧凑:能救外碎片,但有前提

外碎片已经产生之后还有一条补救路径:紧凑(Compaction,也叫拼接)——把内存中的进程全部往一端搬,使它们互相邻接,原来分散的小空闲区就拼成一整块。

它的硬前提是动态重定位:进程被搬走后物理位置变了,如果地址已在装入时写死(静态重定位),程序立刻就跑不了;只有把地址变换推迟到每次访存、由重定位寄存器完成,紧凑才只需要改一个寄存器的值。"动态可重定位分区分配"这个名字就是这么来的——紧凑和动态重定位是一件事的两面。代价则是搬家本身很贵:大量数据在内存里逐字节挪位,期间被搬的进程无法运行,为维持利用率往往还要频繁紧凑。

真正让紧凑变得不必要的,是下一篇的思路:既然搬来搬去是为了凑出连续空间,那干脆不再要求连续

六、伙伴算法:把合并的代价压下去

前面四种策略的回收都要扫描空闲结构、判断前后是否邻接,碎片越多扫得越久。 伙伴(Buddy)算法换了个思路:限制块的大小和位置,让"该跟谁合并"变成一次计算而不是一次查找。

做法是内存只按 2 的幂次切块——1 KB、2 KB、4 KB……每一级一条空闲链。 分配时找最小的够用层级,若该级没有空闲块就从上一级劈成两半,其中一半给出去、 另一半挂到本级链上。

关键在回收。一个 2k 的块只跟它的伙伴合并,而伙伴的定义要三条同时成立

  1. 大小相等(同为 2k);
  2. 物理相邻
  3. 来自同一个父块——即两者合并后恰好拼成一个对齐的 2k+1 块。

第三条是最容易被漏掉的。不是任意两个等大又相邻的块都能合:如果它们分属两个不同的父块, 合出来的东西不对齐,下一层就没法继续合。正因为有这条约束, 一个块的伙伴地址可以直接算出来(把块首地址的第 k 位取反),根本不用查找。

回收时检查伙伴:伙伴空闲就合并成 2k+1 的块,然后递归往上再查一层;伙伴还占着就停下。

代价是分配单位被强行凑成 2 的幂,内碎片重新出现了——申请 33 KB 会拿到 64 KB。 这是它和前四种策略最本质的取舍差别:前四种按需切分、没有内碎片但合并要查找; 伙伴算法按幂次切分、合并只需计算但重新引入了内碎片。

考点速记

  1. 三种连续分配是同一矛盾被反复缓解的三步:单一连续分配(内碎片)→ 固定分区分配(内碎片,多道程序度 = 分区数)→ 动态分区分配(无内碎片、有外碎片)。共同前提是一个进程必须占一整块连续内存
  2. 碎片判据:分区大小固定、进程填不满 ⇒ 内碎片(单一连续、固定分区);分区按需切分、剩下的边角料没人要 ⇒ 外碎片(动态分区)。
  3. 空闲分区链的尾部重复记状态位与大小,是为了回收时用"回收区首址 − 1"一步读到前一块的尾部——用极少空间换合并时的常数时间
  4. 四种分配策略只差"多个够大的分区选哪个",排序方式是选择规则的推论,不是额外要背的:最佳适应要最小的够大分区 ⇒ 按大小递增排;最坏适应要最大的 ⇒ 按大小递减排、直接取表头。
  5. 首次适应综合性能最好,但这是大量随机请求下的统计结论——单条请求序列上谁剩的大块更多,结论可能正好相反。
  6. 衡量碎片严重程度的指标是最大连续空闲块,不是空闲总量。 总量相同、形状不同时,能否装下新进程的结论会相反。
  7. 回收分四种邻接情况,用"首址变不变、表项数变不变"两个维度区分:只前邻 → 改前一项大小,表项数不变;只后邻 → 改首址为回收区首址并加大小,表项数不变;前后皆邻 → 用前一项首址、大小取三者之和、删掉后一项(−1);皆不邻 → 新建表项(+1)。⚠️"紧邻"指地址严丝合缝,差一个字节都不算。
  8. 紧凑能把外碎片拼成大块,前提是动态重定位、代价是大量数据搬移;它动的是进程位置,所以只对外碎片有意义
  9. 伙伴算法只合并互为伙伴的块——大小相等、物理相邻、且同属一个父块,三条缺一不可。代价是块按 2 的幂切分,内碎片重新出现

这一节在真题里被考过的形式

考法非常集中:四道题里三道是同一种手算题——给一串分配请求,问某种策略下的分配结果或碎片状况。 剩下一道问回收合并的规则。

  • 给空闲分区序列和一串请求,问最佳适应算法的分配结果(2010-28、2017-25)。⚠️ 手算时最容易错的一步是拿初始表去给第二、第三个请求找分区——候选集必须在当前状态上重算,前一个请求切完之后有的分区已经掉出候选集了。2017-25 还叠了一次分区合并。
  • 问动态分区 + 最佳适应下的外部碎片情况(2019-32)。判据回到速记第六条:看最大连续空闲块,不是把空闲量加起来。最佳适应每次切下的余量都最小,因而积累的是大量极小的、谁也用不了的碎片——总量看着不少,最大块却很小。
  • 问回收分区时仅合并大小相等的空闲分区的算法是哪个(2024-27)。答伙伴算法。⚠️ 另三个选项(最佳适应、最坏适应、首次适应)都是分配策略,它们的回收一律"前后相邻就合并",跟大小无关。这道题实际考的是"分配策略回收合并规则是两回事"。

复习优先级必须拿满,且要能手算。 四种策略的手算是本节唯一的技术动作, 练到"每一步都在当前状态上重算候选集"成为习惯即可。回收的四种情况按第七条那两个维度记, 不要死背四段话。伙伴算法目前只考过定义,把"三条伙伴条件 + 重新有内碎片"记住就够。

易错:手算时拿初始空闲分区表给后续请求找分区。每一步都要在当前状态上重算候选集

易错:拿空闲总量衡量碎片严重程度。指标是最大连续空闲块

易错:把伙伴算法当成一种分配策略去和最佳适应比。题目问的是回收合并规则——另三个的合并规则都是"相邻就合"。

易错:认为任意两个等大且相邻的空闲块都是伙伴。还必须同属一个父块(合并后恰好对齐成 2k+1)。

易错:认为伙伴算法没有内碎片。它按 2 的幂切块,申请 33 KB 会拿到 64 KB。

易错:回收时"只后邻"的情况忘了改首址。合并后的空闲区从回收区开头算起,不改就会把回收区那一段漏在表外。

易错:认为首次适应在任何一条请求序列上都最优。那是大量随机请求下的统计结论

易错:认为紧凑能消除内碎片。紧凑动的是进程位置,只对外碎片有意义

教材出处
  • 汤小丹《计算机操作系统》4.3.1 单一连续分配、4.3.2 固定分区分配(分区大小相等与不等、分区使用表),p126–p127
  • 同上 4.3.3 动态分区分配(空闲分区表与空闲分区链、尾部重复设置状态位和分区大小;分区分配操作),p128
  • 同上 4.3.3「回收内存」四种邻接情况与内存回收流程,p129–p130
  • 同上 4.3.4 基于顺序搜索的动态分区分配算法(首次适应、循环首次适应、最佳适应、最坏适应及各自代价),p130–p131
  • 同上 4.3.6 动态可重定位分区分配(紧凑必须配合动态重定位),p133–p134
  • 同上 4.4.2 对换空间的管理(对换区的分配与回收同样分四种情况),p137

相关知识

内存管理基本概念基本分页(地址变换)程序的链接与装入

真题练习