《操作系统导论》第二部分 内存虚拟化 P4 空闲空间管理

本文探讨了用户级内存分配库中的空闲空间管理方法,重点介绍了分割与合并、追踪已分配空间大小、建立空闲列表及堆增长等底层机制。此外,还详细分析了几种基本策略,如最优匹配、最差匹配、首次匹配和下次匹配。
摘要由CSDN通过智能技术生成

C5 空闲空间管理

管理空闲空间可以很容易,如果需要管理的空间被划分为固定大小的单元,只需要维护这些大小固定的单元的列表,如果有请求就返回列表中的第一项

但要管理的空闲空间由不同大小的单元构成时,管理就变得困难,这种情况出现在用户malloc()或calloc()时,或者操作系统用分段实现虚拟内存,这两种情况下,就会出现外部碎片,空闲空间被分割成不同大小的小块,成为碎片,后续的请求可能失败,因为没有一块足够大的连续空闲空间满足需求,即使这时总的空闲空间超出请求空间大小

示例:全部可用空闲空间是20kb,但被分割成两个10kb的碎片,导致一个15kb的分配请求失败:
在这里插入图片描述

本章中主要讨论用户级内存分配库中的分配程序,即由malloc,calloc,free操作的堆中的空闲空间管理,从而提升堆的利用率,减少堆段加载到物理内存中产生的外部碎片

5.1 空闲空间的几个假设

假设1:基本接口就像malloc()和free()提供的那样,malloc需要一个请求空间大小的size参数,用于申请对应的空间,free需要一个空间的指针,用于释放相应的内存块

假设2:堆上管理空闲空间的数据结构不一定真的是列表,只要是某种可以追踪空闲空间的数据结构都可以,这里假设是空闲列表

假设3:内存一旦被分给各客户,就不可以被重定向到其它位置,直到相应的free()将它归还

假设4:分配程序管理的是连续的一块字节区域,假设这块区域在整个生命周期内大小固定

5.2 空闲空间的底层机制

(1) 分割与合并

堆空闲列表包含一组元素,记录了堆中哪些空间还没有分配,假设有下面30字节的堆:
在这里插入图片描述
这个堆对应的空闲列表会有两个元素,一个描述第一个10字节的空闲区域,第二个描述另一个空闲区域:
在这里插入图片描述
对于上面的情况,任何大于10字节的空间请求都会失败,恰好10字节的空间请求可以由两个空闲块中的任何一个满足,但如果申请空间小于10字节,会发生什么?

假设只申请1字节的空间,分配程序会执行分割,它找到一块可以满足需求的空闲空间,将其分割,第一块返回给用户,第二块留在空闲列表中,对于上述的例子,空闲列表可能会变成:
在这里插入图片描述
由上面可以看出,空闲列表基本没有变化,只是第二个空闲块起始位置由20变成21,长度由10变成9,如果请求空间大小小于某块空闲块,分配程序通常会进行分割

许多分配程序还有一种机制,叫合并,如果对于上述的例子,free掉中间10各字节,归还堆中间的空间会发生什么,如果只是简单地将这块空闲空间加入到空闲列表:
在这里插入图片描述
尽管现在整个堆都是空闲的,但貌似被分成了3个10字节的空闲块,如果申请20字节的空间,简单的遍历这个空闲列表,由于找不到20字节的空闲空间,可能会返回失败,为了避免这个问题,分配程序会在释放一块内存时合并可用的空间,在归还一块空间时,仔细查看要归还的内存块的地址和邻近的空闲空间块,可以通过合并得到更大的空闲空间:
在这里插入图片描述
实际上,这是堆的空闲列表最初的样子,在所有分配之前,通过合并,分配程序可以更好的确保大块的空闲空间能提供给应用程序

(2) 追踪已分配空间的大小

free()接口没有块大小的参数,对于给定的指针,内存分配库要很快的确定释放空间的大小,从而将它放回空闲列表

要完成这个任务,大多数分配程序都会在头块中保留一点额外的信息,它在内存中,通常在返回的内存前
在这里插入图片描述
该头块中至少包含所分配空间的大小,也可能包含一些额外的指针来加速空间释放

在这里插入图片描述

用户调用free(ptr)时,库会通过简单的指针运算先得到头块的位置,获得头块的指针后,根据头块中的内容计算要释放的空间的大小,实际要释放的是头块大小加上分配给用户的空间大小,因此如果用户请求N字节的内存,库不是寻找大小为N的空闲块,而是寻找N加上头块大小的空闲块

(3) 建立一个空闲列表

如何在堆内部建立一个空闲列表呢?

假设堆为4kb,即4096字节,为了将整个堆作为一个空闲列表,首先要初始化它,开始,列表只有一个条目,记录了大小为4088(假设头块大小为8字节)的空间
在这里插入图片描述
现在假设有一个100字节的内存请求,为了满足这个请求,库先要找到一个足够大的块,因为只有一个4088字节的块,所以选中这个块,然后,这个块被分割成两块,一块满足请求需求,一块是剩余的空闲块:
在这里插入图片描述
现在在申请两次100字节的空间,并准备释放第二次申请的100字节空间(包括它的头块):
在这里插入图片描述
释放第二个100字节空间(包括其头块)后:
在这里插入图片描述
现在假设剩余的两块已分配的空间也被释放,没有合并的情况下,空闲列表将非常破碎,没有合并,虽然整个内存空间是空闲的,但被分成了小段,因此形成了碎片化的内存空间,解决方法很简单,遍历列表,合并相邻块,完成之后,又形成了一个完整的堆

(4) 让堆增长

如果堆中内存空间耗尽,最简单的方法就是返回失败

大多数传统的分配程序会从很小的堆开始,当空间耗尽时,向操作系统申请更大的空间,通常这意味着进行了某种系统调用

5.3 基本策略

有了空闲空间的底层机制,来看看管理空闲空间的一些基本策略,即保证快速和碎片最小化

(1) 最优匹配

最优匹配:首先遍历整个空闲列表,找到和请求大小一样或更大的空闲块,然后返回这组候选者中最小的一块,即选择最接近用户请求大小的块,从而避免空间浪费

只需要遍历一次空闲列表,就可以找到正确的块并返回,但遍历查找需要付出较高的性能代价

(2) 最差匹配

最差匹配:它尝试寻找最大的空闲块,分割并满足用户需求后,将剩余的块加入空闲列表,最差匹配尝试在空闲列表中保留较大的块

最差匹配仍然要遍历整个空闲列表,但会导致过量的碎片,还有很高的开销

(3) 首次匹配

首次匹配:找到第一个足够大的块,将请求的空间返回给用户,同样将剩余的空闲空间留给后续请求

首次匹配具有速度优势,但有时会让空闲列表开头有很多小块

(4) 下次匹配

下次匹配:不同于首次匹配每次从列表的开始查找,下次匹配算法多维护一个指针,指向上一次查找结束的位置

其想法是将堆空闲空间的查找操作扩散到整个列表中,避免对列表开头频繁的分割,避免了遍历

(5) 示例

假设一个空闲列表包含3个元素,长度依次为10,30,20(暂时忽略头块)
在这里插入图片描述
假设有一个15字节的内存请求,最优匹配会遍历整个空闲列表,发现20字节空闲块是最优匹配,因为它是满足请求的最小空闲块,结果空闲列表变成:
在这里插入图片描述
最差匹配对于15字节的请求,会选择最大的空闲块30进行分割:
在这里插入图片描述
首次匹配的结果和最差匹配一样,找到30的空闲块进行分割,但首次匹配不同于最优匹配和最差匹配,它避免了查找开销

5.4 其它方式

(1) 分散空闲列表

分散空闲列表:如果某个应用程序经常申请一种或几种大小的内存空间,那就用一个独立的列表管理这样大小的对象,其它大小的请求都交给更通用的内存分配程序

分散空闲列表的好处是通过拿出一部分内存专门满足某种大小的请求,碎片就不再是问题了,由于没有复杂的列表查找过程,这种特定大小的内存分配和释放都很快

(2) 伙伴系统

因为合并对分配程序很重要,所以人们设计了一些方法,让合并变得简单,一个好的例子就是二分伙伴分配程序

在这种系统中,空闲空间首先从概念上被看作2^N的大空间,当有一个内存分配请求时,空闲空间被递归地一分为二,直到更好可以满足请求的大小(再一分为二就无法满足需求),这时,请求的块被返回给用户,一个64kb的空闲空间就被切分,以便提供7kb的块:
在这里插入图片描述
这种分配策略只允许分配2的整数次幂大小的空闲块,因此会有内部碎片,伙伴系统的优秀之处在于块被释放时,如果将这个8kb的块归还给空闲列表,分配程序会检查“伙伴”8kb是否空闲,如果是就合二为一,变成16kb的块,再依次向上追溯,直到合并了整个内存区域,或者某一个块的伙伴还么有被释放

(3) 其它想法

上述的众多策略都有一个重要问题:缺乏可扩展性,即查找列表可能很慢,因此,更先进的分配程序采用更复杂的数据结构来优化这个开销,通过牺牲简单性来换取性能,包括平衡二叉树,伸展树,偏序树等

5.6 小结

在构建一个空闲空间管理系统时需要做许多折中,对分配程序提供的确切工作负载了解的越多,就越能调整它以更好处理这种工作负载,在现代计算机系统中,构建一个用于各种工作负载,快速,空间高效,可扩展的分配程序仍是一个持续的挑战

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值