操作系统:连续分配存储管理方式

连续分配

为了能将用户程序装入内存,必须为它分配一定大小的内存空间。连续分配方式是最早出现的一种存储器分配方式,分配的策略为一个用户程序分配一个连续的内存空间。程序中代码或数据的逻辑地址相邻,内存空间分配时物理地址的相邻。连续分配方式可分为四类:单一连续分配、固定分区分配、动态分区分配以及动态可重定位分区分配算法四种方式。
此处会涉及到内存碎片的概念,内存的内部碎片是指分配给进程的内存空间有部分没有用上,外部碎片指的是内存中某些空间分区由于太小导致难以使用。

单一连续分配

在单道程序环境下经常使用单一连续分配,这种方式把内存分为系统区和用户区两部分。其中系统区仅提供给 OS 使用,通常使用内存的低址部分。在用户区内存中,仅装有一道用户程序,整个内存的用户空间由该程序独占。
采用单一连续分配可以不采取存储器保护措施,因为在单用户环境下机器由一用户独占,不可能存在其他用户干扰的问题,同时不设置保护措施也可以节省硬件。即使出现破坏行为,也只有可能是用户程序自己破坏操作系统,OS 通过系统的再启动而重新装入内存。
单一连续分配的实现简单,不会出现外部碎片,可以采用覆盖方式扩充内存且可以不需要内存保护措施。但是只能用于单用户单任务的 OS 中,会出现内部碎片,存储的的利用率比较低。

固定分区分配

分区式存储管理

多道程序系统需要在内存中装入多道程序,这样需要保证程序之间又不会发生相互干扰。此时可以采用分区式存储管理方式,将整个用户空间划分为若干个固定大小的区域,在每个分区中只装入一道作业。如果在内存中有四个用户分区,便允许四个程序并发运行。当有一空闲分区时,便可以再从外存的后备作业队列中选择一个适当大小的作业,装入该分区。当该作业结束时,又可再从后备作业队列中找出另一作业调入该分区。

划分分区的方法

将内存的用户空间划分为若干的分区,可以令分区大小相等或者不等。如果所有划分的的内存分区大小相等将缺乏灵活性,程序太小时会造成内存空间的浪费,当程序太大时一个分区又不足以装入该程序,致使该程序无法运行。不过在计算机同时控制多个相同对象时,这些对象所需的内存空间大小往往相同,这种划分方式比较方便和实用。
所有划分的的内存分区大小不等,可以增加存储器分配的灵活性。通常可把内存区划分成含有多个较小的分区、适量的中等分区及少量的大分区,这样便可根据程序的大小,为之分配适当的分区。

内存分配

进行内存分配时通常将分区按其大小进行排队,并建立一张分区使用表,其中各表项包括每个分区的起始地址、大小及状态(是否已分配)。当有一用户程序要装入时,由内存分配程序依据用户程序的大小检索该表,找出一个能满足要求的、尚未分配的分区分配给该程序,然后将该表项中的状态置为“已分配”。若未找到大小足够的分区,则拒绝为该用户程序分配内存。
例如一张分区使用表如下:

分区号 大小 起始地址 状态
1 12 20 已分配
2 32 32 已分配
3 64 64 未分配
4 128 128 已分配

实际存储空间的分配情况可能是:

动态分区分配

动态分区分配又称为可变分区分配,这种策略不会预先划分内存分区,而是根据进程的实际需要动态地为之分配内存空间,这种方式的优点是可以尽可能使分区大小正好适合内存所需。在实现动态分区分配时,涉及到分区分配中所用的数据结构、分区分配算法和分区的分配与回收操作这样三方面的问题。

数据结构

为了实现动态分区分配,系统中必须配置相应的数据结构来描述空闲分区和已分配分区的情况。常用的数据结构有以下 2 种形式,第一种是空闲分区表,它在系统每个空闲分区占一个表目,表目中包括分区号、分区大小和分区始址等数据项。

分区号 大小 起始地址 状态
1 12 20 空闲
2 32 32 空闲
3 64 64 空闲
4 128 128 空闲

第二种是空闲分区链,每个分区的起始部分设置一些用于控制分区分配的信息,通过前、后向链接指针将所有的空闲分区链接成一个双向链。

动态分区分配算法

为把一个新作业装入内存,须按照一定的分配算法,从空闲分区表或空闲分区链中选出一分区分配给该作业。

分配内存

系统应利用某种分配算法,从空闲分区链(表)中找到所需大小的分区。设请求的分区大小为 u.size,表中每个空闲分区的大小可表示为 m.size。若 m.size - u.size ≤ size(size 是事先规定的不再切割的剩余分区的大小),说明多余部分太小可不再切割,将整个分区分配给请求者。如果多余部分超过 size,便从该分区中按请求的大小划分出一块内存空间分配出去,余下的部分仍留在空闲分区链(表)中,然后将分配区的首址返回给调用者。
例如当前的内存分配状态如图所示,内存分配表如图所示。

分区号 大小 起始地址 状态
1 20 5 空闲
2 5 35 空闲
3 15 55 空闲


现在需要插入大小为 5M 的进程三,通过某种算法后插入分区 1,修改后的内存分配表和内存状态如下。

分区号 大小 起始地址 状态
1 15 10 空闲
2 5 35 空闲
3 15 55 空闲


如果通过某种算法后插入分区 2,修改后的内存分配表可以把分区 2 删除。

分区号 大小 起始地址 状态
1 20 5 空闲
3 15 55 空闲

回收内存

当进程运行完毕释放内存时,系统根据回收区的首址,从空闲区链(表)中找到相应的插入点,此时可能出现以下四种情况之一。

情况一

第一种情况是回收区与插入点的前一个空闲分区 F1。此时应将回收区与插入点的前一分区合并,不必为回收分区分配新表项,而只需修改其前一分区 F1 的大小。例如内存分配表和内存状态如下,此时要回收进程 1。

分区号 大小 起始地址 状态
1 20 5 空闲
3 15 55 空闲


将回收的空间和分区 1 和并,然后修改分区大小即可。

分区号 大小 起始地址 状态
1 30 5 空闲
3 15 55 空闲

情况二

第二种情况是回收分区与插入点的后一空闲分区 F2 相邻接,此时也可将两分区合并形成新的空闲分区,但用回收区的首址作为新空闲区的首址,大小为两者之和。例如内存分配表和内存状态如下,此时要回收进程 2。

分区号 大小 起始地址 状态
1 20 5 空闲
3 15 55 空闲


将回收的空间和分区 3 和并,除了修改分区大小还要修改起始地址。

分区号 大小 起始地址 状态
1 20 5 空闲
3 30 40 空闲

情况三

第三种情况是回收区同时与插入点的前、后两个分区邻接,此时将三个分区合并,使用 F1 的表项和 F1 的首址并取消F2的表项,大小为三者之和。例如内存分配表和内存状态如下,此时要回收进程 1。

分区号 大小 起始地址 状态
1 15 10 空闲
2 5 35 空闲
3 15 55 空闲


将回收的空间和分区 1、2 和并,然后修改分区大小即可。

分区号 大小 起始地址 状态
1 30 10 空闲
3 15 55 空闲

情况四

第四种情况是回收区既不与 F1 邻接又不与 F2 邻接,这时应为回收区单独建立一个新表项,填写回收区的首址和大小,并根据其首址插入到空闲链中的适当位置。例如内存分配表和内存状态如下,此时要回收进程 3。

分区号 大小 起始地址 状态
1 20 5 空闲
3 15 55 空闲


这时就需要向内存分配表加入一个新条目,并且写上大小和起始地址。

分区号 大小 起始地址 状态
1 20 5 空闲
3 15 55 空闲
2 5 35 空闲

基于顺序搜索的分区分配

为了实现动态分区分配,通常是将系统中的空闲分区链接成一个链。基于顺序搜索的分配方式是依次搜索空闲分区链上的空闲分区,去寻找一个其大小能满足要求的分区,主要有如下四种算法。

首次适应算法

首次适应(first fit,FF)算法要求空闲分区链以地址递增的次序链接,在分配内存时从链首开始顺序查找,直至找到一个大小能满足要求的空闲分区为止。然后再按照作业的大小,从该分区中划出一块内存空间,分配给请求者,余下的空闲分区仍留在空闲链中。若从链首直至链尾都不能找到一个能满足要求的分区,则表明系统中已没有足够大的内存分配给该进程,内存分配失败。

该算法倾向于优先利用内存中低址部分的空闲分区,保留了高址部分的大空闲区。缺点是低址部分不断被划分,会留下许多难以利用的外部碎片。而每次查找又都是从低址部分开始的,会导致搜索的时间开销较大。

循环首次适应算法

为避免低址部分留下许多很小的空闲分区,以及减少查找可用空闲分区的开销,可以使用循环首次适应算法。循环首次适应(next fit,NF)算法在为进程分配内存空间时,是从上次找到的空闲分区的下一个空闲分区开始查找,直至找到一个能满足要求的空闲分区。实现该算法一般会使用循环链表,如果最后一个(链尾)空闲分区的大小仍不能满足要求,则应返回到第一个空闲分区。

算法能使内存中的空闲分区分布得更均匀,减少了查找空闲分区时的开销。但这样会导致高地址的大分区可能被划分为小分区来使用,使缺乏大的空闲分区给较大的进程。

最佳适应算法

最佳适应(best fit,BF)算法在每次为作业分配内存时,总是把能满足要求、又是最小的空闲分区分配给作业。该算法要求将所有的空闲分区按其容量以从小到大的顺序形成一空闲分区链,这样第一次找到的能满足要求的空闲区必然是最佳的。

由于每次分配后所切割下来的剩余部分总是最小的,所以在存储器中会留下许多难以利用的碎片。

最坏适应算法

最坏适应(worst fit,WF)算法在扫描整个空闲分区表或链表时,总是挑选一个最大的空闲区,从中分割一部分存储空间给作业使用。该算法要求将所有的空闲分区,按其容量以从大到小的顺序形成空闲分区链。

这个算法会使存储器中缺乏大的空闲分区,它的优点是可使剩下的空闲区不至于太小,产生碎片的可能性最小,对中、小作业有利。同时最坏适应分配算法查找效率很高,查找时只要看第一个分区能否满足作业要求即可。

基于索引搜索的分区分配

基于顺序搜索的动态分区分配比较适用于不太大的系统,当系统很大时系统中的内存分区可能会很多,空闲分区链就可能很长,这时采用顺序搜索分区方法可能会很慢。为了提高搜索空闲分区的速度,在大、中型系统中往往会采用基于索引搜索的动态分区分配算法。

快速适应算法

快速适应(quick fit)算法又称为分类搜索法,是将空闲分区根据其容量大小进行分类,对于每一类具有相同容量的所有空闲分区,单独设立一个空闲分区链表。同时在内存中设立一张管理索引表,其中的每一个索引表项对应了一种空闲分区类型,并记录了该类型空闲分区链表表头的指针。该算法在搜索时先根据进程的长度,从索引表中去寻找到能容纳它的最小空闲区链表,然后从链表中取下第一块进行分配即可。

该算法在进行空闲分区分配时不会对任何分区产生分割,所以能保留大的分区,也不会产生内存碎片。优点是查找效率高,主要缺点在于分区归还主存时的算法复杂,系统开销较大。此外该算法在分配空闲分区时是以进程为单位的,一个分区只属于一个进程,因此会存在内部碎片。

伙伴系统

伙伴系统(buddy system)算法规定无论已分配分区或空闲分区,其大小均为 2 的 k 次幂(k 为整数,1 ≤ k ≤ m)。将这些空闲分区按分区的大小进行分类,对于具有相同大小的所有空闲分区,单独设立一个空闲分区双向链表。

当需要为进程分配一个长度为 n 的存储空间时,首先计算一个 i (2^i-1 < n ≤ 2^i),然后在空闲分区大小为 2^i 的空闲分区链表中查找。若找到即把该空闲分区分配给进程。如果找不到表明长度为 2^i 的空闲分区已经耗尽,则在分区大小为 2^i+1 的空闲分区链表中寻找。若存在 2^i+1 的一个空闲分区,则把该空闲分区分为相等的两个分区,其中的一个分区用于分配,而把另一个加入分区大小为 2^i 的空闲分区链表中。如果还是找不到就继续搜索,以此类推。
在伙伴系统中,对于一个大小为 2^k,地址为 x 的内存块,其伙伴块的地址则用 buddyk(x)表示,其通式为:

在回收空闲分区时,需要对空闲分区进行合并,所以其时间性能比快速适应算法差.但由于它采用了索引搜索算法,比顺序搜索算法好。由于对空闲分区进行合并,减少了小的空闲分区,提高了空闲分区的可使用率,故优于快速适应算法,比顺序搜索法略差。

Hash 算法

哈希算法就是利用哈希快速查找的优点,以及空闲分区在可利用空闲区表中的分布规律建立哈希函数。构造一张以空闲分区大小为关键字的哈希表,该表的每一个表项记录了一个对应的空闲分区链表表头指针。当进行空闲分区分配时,根据所需空闲分区大小通过哈希函数计算,从中得到相应的空闲分区链表,实现最佳分配策略。

动态可重定位分区分配

紧凑

连续分配方式的一个重要特点是一个系统或用户程序必须被装入一片连续的内存空间中,当一台计算机按照此方法分配了较多的内存空间后,将会分割出许多小的分区。这会导致内存空间缺乏大分区,即使这些分散的许多小分区的容量总和大于要装入的程序,但由于这些分区不相邻接,也无法把该程序装入内存。例如如图所示的 3 个空闲分区的大小总合是 10 M,但是由于这些空间并不是邻接的,所以会导致无法插入一个 10 M 大小的程序。

若想把大作业装入,可将内存中的所有作业进行移动,使它们全都相邻接,这样可把原来分散的多个空闲小分区拼接成一个大分区。这种通过移动内存中作业的位置,把原来多个分散的小分区拼接成一个大分区的方法称为“拼接”或“紧凑”。

虽然“紧凑”能获得大的空闲空间,但是经过紧凑后的用户程序在内存中的位置发生了变化,就需要对程序和数据的地址加以修改(变换)。系统每“紧凑”一次,就要对移动了的程序或数据的地址进行修改,这个功能不仅是实现复杂,而且还大大地影响到系统的效率。

动态重定位

为使地址的转换不会影响到指令的执行速度,必须有硬件地址变换机构的支持。需要在系统中增设一个重定位寄存器,用它来存放程序(数据)在内存中的起始地址。程序在执行时,真正访问的内存地址是相对地址与重定位寄存器中的地址相加而形成的。地址变换过程是在程序执行期间的,故称为动态重定位。当系统对内存进行了“紧凑”时,不需对程序做任何修改,只要用该程序在内存的新起始地址去置换原来的起始地址即可。

参考资料

《计算机操作系统(第四版)》,汤小丹 梁红兵 哲凤屏 汤子瀛 编著,西安电子科技大学出版社

posted @ 2021-08-08 16:07  乌漆WhiteMoon  阅读(2178)  评论(0编辑  收藏  举报