《操作系统导论》知识总结 第17章 空闲空间管理

本章主要讨论关于内存空闲空间管理的一些问题。如果需要管理的内存空间被划分为固定大小的单元,空闲空间管理就很容易。在这种情况下,只需要维护这些大小固定的单元的列表,如果有内存分配请求,就返回列表中的第一项。但是,如果空闲空间由大小不同的单元构成,管理就变得比较困难。这种情况出现在用户级的内存分配库(如malloc()和free()),或者操作系统用分段的方式实现虚拟内存。在这两种情况下,出现了外部碎片的问题,即空闲空间被分割成不同大小的碎片,后续的请求可能失败,因为没有一块足够大的连续空闲空间,即使这时总的空闲空间超出了请求的大小。

所以,本章需要解决的问题是,要满足变长内存分配请求,应该使用什么策略管理空闲空间?

假设

详细的原文假设看书p120页,这里简单总结一下。

  • 我们假定基本的接口就像malloc()和free()提供的那样。
  • 进一步假设我们主要关心外部碎片问题。
  • 我们还假设,内存一旦被分配给客户,就不可以被重定位到其他位置。
  • 最后我们假设,分配程序所管理的是连续的一块字节区域。

底层机制

首先,探讨空间分割与合并的基本知识。其次,看看如何快速并相对轻松地追踪已分配的空间。最后,讨论如何利用空闲区域的内部空间维护一个简单的列表,来追踪空闲和已分配的空间。

分割与合并

空闲列表包含一组元素,记录了堆中的哪些空间还没有分配。假设有下面的30字节的堆:
在这里插入图片描述
这个堆对应的空闲列表会有两个元素,一个描述第一个10字节的空闲区域(字节0~9),一个描述另一个空闲区域(字节20~29):
在这里插入图片描述

通过上面的介绍可以看出,由于没有足够的连续可用空间,任何大于10字节的分配请求都会失败。而恰好10字节的需求可以由两个空闲块中的任何一个满足。如果申请小于10字节空间,分配程序会执行所谓的分割操作:它找到一块可以满足请求的空闲空间,将其分割,第一块返回给用户,第二块留在空闲列表中。假设遇到申请一个字节的请求,分配程序选择对第二块空闲空间进行分割,对malloc()的调用会返回20(1字节分配区域的地址),空闲列表会变成这样:
在这里插入图片描述
从上面可以看出,空闲列表基本没有变化,只是第二个空闲区域的起始位置由20变成21,长度由10变为9了。因此,如果请求的空间大小小于某块空闲块,分配程序通常会进行分割。

对于这个堆,如果应用程序调用free(10)归还堆中间的空间,会发生什么?如果只是简单地将这块空闲空间加入空闲列表,可能得到如下的结果:
在这里插入图片描述
现在问题来了,尽管整个堆现在完全空闲,但它似乎被分割成了3个10字节的区域。如果用户此时请求20字节的空间,简单遍历空闲列表会找不到这样的空闲块,因此返回失败。为了避免这个问题,分配程序会在释放一块内存时合并可用空间。在用户归还一块空闲内存时,分配程序仔细查看要归还的内存块的地址以及邻近的空闲空间块。如果新归还的空间与原有空闲块相邻,就将它们合并为一个较大的空闲块。通过合并,最后空闲列表应该像这样:
在这里插入图片描述
实际上,这就是堆的空闲列表最初的样子。通过合并,分配程序可以更好地确保大块的空闲空间能提供给应用程序。

追踪已分配空间的大小

在使用free(void *ptr)释放已申请的内存空间时,我们会发现该接口没有块大小的参数。因此它假定对于给定的指针,内存分配库可以很快确定要释放空间的大小,从而将它放回空闲列表。要完成这个任务,大多数分配程序都会在头块(存在于返回的内存块之前)中保存一些额外的信息。在下面这个例子中,我们调用int *ptr = malloc(20)申请20字节的空间,并将结果保存在ptr中:
在这里插入图片描述
该头块中至少包含所分配空间的大小,也可能包含一些额外的指针来加速空间释放,包含一个幻数来提供完整性检查,以及其他的一些信息。我们假定一个简单的头块包含了分配空间的大小和一个幻数:

typedef struct header_t {
    int size;
    int magic;
} header_t;

在这里插入图片描述
用户调用free(ptr)时,库会通过简单的指针运算得到头块的位置:

void free(void *ptr) { 
 header_t *hptr = (void *)ptr - sizeof(header_t); 
}

获得头块的指针后,库可以很容易地确定幻数是否符合预期的值,作为正常性检查(assert(hptr->magic == 1234567)),并简单计算要释放的空间大小(即头块的大小加区域长度)。注意,内存分配库实际释放的是头块大小加上分配给用户的空间的大小。因此,如果用户请求N字节的内存,库会寻找N加上头块大小的空闲块。

嵌入空闲列表

假设我们需要管理一个4096字节的内存块(即堆是4KB)。为了将它作为一个空闲空间列表来管理,首先要初始化这个列表。初始时列表中只有一个条目,记录了大小为4096的空间(减去头块的大小)。下面是该列表中一个节点描述:

typedef struct node_t {
    int size;
    struct node_t *next;
} node_t;

现在,有一些代码用来初始化堆,并将空闲列表的第一个元素放在该空间中。假设堆构建在某块空闲空间上,这块空间通过系统调用mmap()获得。这不是构建这种堆的唯一选择,但在这个例子中很合适。

// mmap() returns a pointer to a chunk of free space 
node_t *head = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                    MAP_ANON | MAP_PRIVATE, -1, 0);
head->size    = 4096 - sizeof(node_t); 
head->next    = NULL;

执行这段代码之后,列表的状态是它只有一个条目,记录大小为4088。head指针指向这块区域的起始地址,这里假设是16KB。堆看起来如下所示。
在这里插入图片描述
现在,假设有一个100字节的内存请求。为了满足这个请求,库首先要找到一个足够大小的块。因为只有一个4088字节的块,所以选中这个块。然后,这个块被分割为两块,一块足够满足请求(以及头块,如前所述),一块是剩余的空闲块。假设头块大小为8个字节(两个整数,分别记录大小和幻数),那么现在堆中的空间如图所示。
在这里插入图片描述
至此,对于100字节的请求,库从原有的一个空闲块中分配了108字节,返回指向它的一个指针(在上图中用ptr表示),并在其之前连续的8字节中记录头块信息,以供未来的free()函数使用。同时将列表中的空闲节点缩小为3980字节。现在再来看该堆,其中有3个已分配区域,每个100(加上头块是108)字节。
在这里插入图片描述
可以看出,堆的前324字节已经分配,因此该空间中有3个头块,以及3个100字节的用户使用空间。空闲列表只有一个由head指向的节点,但在3次分割后,大小只有3764字节。但如果用户程序通过free()归还一些内存,会发生什么?在这个例子中,应用程序调用free(16500),归还了中间的一块已分配空间。内存库会计算出这块要释放空间的大小,并将空闲块加回空闲列表。假设我们将它插入到空闲列表的头位置,那么整个空间就如下所示。
在这里插入图片描述
现在,空闲空间被分割成两段,空闲列表包括一个100字节的小空闲块和一个3764字节的大空闲块。假设剩余的两块已分配的空间也被释放。如果没有合并,那么空闲列表将非常破碎。虽然整个内存空间是空闲的,但却被分成了多个小段,因此形成了碎片化的内存空间。解决方案非常简单:遍历列表,然后合并相邻块。完成之后,堆又成了一个整体。
在这里插入图片描述

让堆增长

如果堆中的内存空间耗尽,应该怎么办?最简单的方式就是返回失败。在某些情况下这也是唯一的选择。大多数传统的分配程序会从很小的堆开始,当空间耗尽时,再向操作系统申请更大的空间。通常,这意味着它们进行了某种系统调用(例如,大多数UNIX系统中的sbrk)让堆增长。操作系统在执行sbrk系统调用时,会找到空闲的物理内存页,将它们映射到请求进程的地址空间中去,并返回新的堆的末尾地址。这时,就有了更大的堆,请求就可以成功满足。

基本策略

理想的分配程序可以同时保证快速和碎片最小化。遗憾的是,由于分配及释放的请求序列是任意的,任何特定的策略在某组不匹配的输入下都会变得非常差。所以我们不会描述“最好”的策略,而是介绍一些基本的选择,并讨论它们的优缺点。

最优匹配

最优匹配策略首先遍历整个空闲列表,找到和请求大小一样或更大的空闲块,然后返回这组候选者中最小的一块。只需要遍历一次空闲列表,就足以找到正确的块并返回。最优匹配选择最接近用户请求大小的块,从而尽量避免空间浪费。然而,简单的实现在遍历查找正确的空闲块时,要付出较高的性能代价。

最差匹配

最差匹配方法与最优匹配相反,它尝试找最大的空闲块,分割并满足用户需求后,将剩余的块(很大)加入空闲列表。最差匹配尝试在空闲列表中保留较大的块,而不是向最优匹配那样可能剩下很多难以利用的小块。但是,最差匹配同样需要遍历整个空闲列表。更糟糕的是,大多数研究表明它的表现非常差,会导致过量的碎片,同时还有很高的开销。

首次匹配

首次匹配策略就是找到第一个足够大的块,将请求的空间返回给用户。同样,剩余的空闲空间留给后续请求。首次匹配有速度优势,但有时会让空闲列表开头的部分有很多小块。因此,分配程序如何管理空闲列表的顺序就变得很重要。一种方式是基于地址排序,通过保持空闲块按内存地址有序,合并操作会很容易,从而减少了内存碎片。

下次匹配

不同于首次匹配每次都从列表的开始查找,下次匹配算法多维护一个指针,指向上一次查找结束的位置。其想法是将对空闲空间的查找操作扩散到整个列表中去,避免对列表开头频繁的分割。这种策略的性能与首次匹配很接近,同样避免了遍历查找。

例子

下面是上述策略的一些例子。

设想一个空闲列表包含 3 个元素,长度依次为 10、30、20(我们暂时忽略头块和其他细节)

在这里插入图片描述
假设有一个 15 字节的内存请求。最优匹配会遍历整个空闲列表,发现 20 字节是最优匹配,因为它是满足请求的最小空闲块。结果空闲列表变为:
在这里插入图片描述
本例中发生的情况,在最优匹配中常常发生,现在留下了一个小空闲块。最差匹配类似,但会选择最大的空闲块进行分割,在本例中是 30。结果空闲列表变为:
在这里插入图片描述
在这个例子中,首次匹配会和最差匹配一样,也发现满足请求的第一个空闲块。不同的是查找开销,最优匹配和最差匹配都需要遍历整个列表,而首次匹配只找到第一个满足需求的块即可,因此减少了查找开销。

一些改进策略

分离空闲列表

有种很有趣的方式叫作分离空闲列表。如果某个应用程序经常申请一种(或几种)大小的内存空间,那就用一个独立的列表,只管理这样大小的对象。其他大小的请求都交给更通用的内存分配程序。这种方法的好处显而易见。通过拿出一部分内存专门满足某种大小的请求,碎片就不再是问题了。而且,由于没有复杂的列表查找过程,这种特定大小的内存分配和释放都很快。

然而,这种方式为系统引入了新的复杂性。例如,应该拿出多少内存来专门为某种大小的请求服务,而将剩余的用来满足一般请求?Solaris系统内核中的厚块分配程序(slab allocator)优雅地处理了这个问题。在内核启动时,它为可能频繁请求的内核对象创建一些对象缓存,如锁和文件系统inode等。这些对象缓存每个分离了特定大小的空闲列表,因此能够很快地响应内存请求和释放。如果某个缓存中的空闲空间快耗尽时,它就向通用内存分配程序申请一些内存厚块(总量是页大小和对象大小的公倍数)。相反,如果给定厚块中对象的引用计数变为0,通用的内存分配程序可以从专门的分配程序中回收这些空间,这通常发生在虚拟内存系统需要更多的空间的时候。

厚块分配程序比大多数分离空闲列表做得更多,它将列表中的空闲对象保持在预初始化的状态。通过将空闲对象保持在初始化状态,厚块分配程序避免了频繁的初始化和销毁,从而显著降低了开销。

伙伴系统

因为合并对分配程序很关键,所以一些研究致力于让合并变得简单,一个例子就是二分伙伴分配程序。在这种系统中,空闲空间首先从概念上被看成大小为2N的大空间。当有一个内存分配请求时,空闲空间被递归地一分为二,直到刚好可以满足请求的大小(再一分为二就无法满足)。这时,请求的块被返回给用户。在下面的例子中,一个64KB大小的空闲空间被切分,以便提供7KB的块:
在这里插入图片描述
在这个例子中,最左边的8KB块被分配给用户(如上图中深灰色部分所示)。请注意,这种分配策略只允许分配2的整数次幂大小的空闲块,因此会有内部碎片的麻烦。伙伴系统的漂亮之处在于块被释放时。如果将这个8KB的块归还给空闲列表,分配程序会检查“伙伴”8KB是否空闲。如果是,就合二为一,变成16KB的块。然后会检查这个16KB块的伙伴是否空闲,如果是,就合并这两块。这个递归合并过程继续上溯,直到合并整个内存区域,或者某一个块的伙伴还没有被释放。伙伴系统运转良好的原因,在于很容易确定某个块的伙伴。每对互为伙伴的块只有一位不同,这一位就决定了它们在整个伙伴树中的层次。

参考

添加链接描述

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
操作系统导论》是一本关于操作系统基本概念和原理的教材。操作系统是计算机系统中最核心的软件之一,它管理和控制计算机硬件资源,并提供给用户和应用程序一个简单易用、高效稳定的接口。 《操作系统导论》主要内容包括操作系统的历史、发展背景,以及操作系统的基本概念、功能和特性等。通过对操作系统的研究,读者能够了解操作系统的架构和组成,掌握操作系统的重要原理和算法,并能够利用这些知识进行操作系统的设计和开发。 在《操作系统导论》中,读者将深入学习操作系统的各个模块,比如进程管理、内存管理、文件系统等。通过对这些模块的学习,读者能够理解操作系统的核心功能,并能够掌握操作系统的基本工作原理。此外,书中还介绍了操作系统的各种经典算法和策略,如调度算法、页面置换算法等,这些算法是实现操作系统功能的基础。 除了基本概念和原理外,《操作系统导论》还涵盖了一些研究热点和前沿技术,如分布式系统、虚拟化技术和云计算等。通过深入了解这些新领域的内容,读者可以了解到操作系统在不同领域的应用和发展趋势。 总之,《操作系统导论》是一本系统介绍操作系统的教材,内容丰富、深入浅出,适合计算机科学与技术相关专业的学生和从业人员阅读使用。通过学习这本书,读者能够全面了解操作系统的基本概念和原理,提高操作系统的设计和开发能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值