深入理解操作系统(28)第十章:虚拟存储器(5)动态内存分配(堆/malloc,free,calloc,sbrk/吞吐率/交换空间/内外部碎片/隐式,显式空闲链表/放置策略/带边界标记的合并/分离存储

1. 堆

1.1 linux下,堆是一个请求二进制零的区域

一个动态存储器分配器维护着一个进程的虚拟存储器区域,称为堆(图10.35)。

在大多数的Unix系统中,堆是一个请求二进制零的区域

它紧接在未初始化的bss区域后开始,并向上生长(向更高的地址)。
对于每个进程,内核维护着一个变量brk(读做“break”),它指向堆的顶部。

图10.35
在这里插入图片描述

分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟存储器组块(chunk),要么是已分配的,要么是空闲的。已分配块(block)显式地保留为供应用使用。空闲块保持空闲,直到它显式地被应用所分配。一个己分配的块保持己分配状态,直到它被释放。

1.2 显示和隐式释放

这种释放要么是应用显式执行的,要么是存储器分配器自身险式执行的。
分配器有两种基本风格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放己分配的块。

2. malloc 和 free

2.1 malloc

C标准库提供了个称为序包的显式分配器。程序通过调用malloc函数来从堆中分配

#include<stdlib.h>
void *malloc(size_t size)
返回:若成功则为指针,若出为NULL

malloc函数返回一个指针,指回大小为至少size字节的存储器块,这个块会为可能包含在这个块内的所有数据对象类型做对齐。
在我们熟悉的Unix系统上,malloc返回一个字(双字,字一般是4字节或者8字节)边界对齐的块。

size_t 类型被定义为unsigned int(无符号整数)。

2.2 calloc realloc sbrk free

calloc:

那些想要已初始化的动态有储器的应用程序可以使用calloc,
它个基于malloc的瘦包装函数,它将分配的存储器初始化为零。

realloc:

想要改变一个以前已分配块的大小,可以使用函数realloc

sbrk:

动态存储器分配器(例如malloc)可以通过使用mmap和munmap函数显式地分配和释放堆存储器,还可以使用sbrk函数。

#include<unistd.h>
void *sbrk(int incr);
返回值:若成功则为老brk指针,若出错则-1

sbrk函数通过将内核的brk指针增加incr来扩展和收缩堆

free:

程序是通过调用free函数来释放己分配的堆块
#include<stdlib.h>
void free(void *ptr)

参考:
calloc , malloc 和 realloc
https://blog.csdn.net/lqy971966/article/details/117983301

3. 为什么要使用动态内存分配

程序使用动态存储器分配的最重要的

原因是它们经常直到程序实际运行时,才知道某些数据结构的大小。

4. 分配器的要求和目标

4.1 分配器要求

显式分配器必须在一些相当严格的约束条件下工作:

1. 处理任意请求序列
	一个应用可以有任意序列的分配请求和释放请求,只要满足约束条件:
	每个释放请求必须对应于一个当前已分配块,这个块产生于以前的分配请求。
	因此,分配器不可以假设分配和释放清求的顺序。
	例如,分配器不能假设所有的分配请求都有相匹配的释放请求,
	或者有相匹配的分配和空闲请求是嵌套的。

2. 立即响应请求
	分配器必须立即响应分配请求。因此,不允许分配器为了提高性能重新排列或者缓冲请求。

3. 只使用堆
	为了使分配器是可扩展的,分配器使用的任何非标量数据结构都必须保存在堆里。
	
4. 对齐块(对齐要求)
	分配器必须对齐块,使得它们可以保存任何类型的数据对象。
	在大多数系统中,这意味着分配器返回的块是8字节(双字)边界对齐的
	
5. 不修改已分配的块
	分配器只能操作或者改变空闲块
	特别是,一旦块被分配了,就不允许修改或者移动它了。
	因此,诸如压缩已分配块这样的技术是不允许使用的。

4.2 两个目标:最大化吞吐率和存储器使用率最大化

在这些限制条件下工作,分配器的编写者试图实现吞吐率最大化和存储器使用率最大化,
而这两个性能目标经常是相互冲突的。

4.2.1 目标1:最大化吞吐率

假定一个分配和释放请求的某种序列为:A0 A1 ……An
我们希望一个分配器的吞吐率最大化

吞吐率就是在每个单位时间里完成的请求数
	例如,如果一个分配器在1秒中内完成500个分配请求和500个释放请求,
	那么它的吞吐率就是每秒1000次操作。

一般而言,我们可以通过使满足分配和释放请求的平均时间最小化来使吞吐率最大化。正如我们会看到的。开发一个具有合理性能的分配器并不困难,

所谓合理性能是指一个分配请求的最糟运行时间与空闲块的数量成线性关,
而一个释放请求的运行时间是个常数。

4.2.2 目标2:最大化存储器利用率(虚拟存储器大小由交换空间决定)

天真的程序员经常不正确地假设虚拟有储器是一个无限的资源。

实际上,一个系统中被所有进程分配的虚拟存储器的全部数量是受磁盘上交换空间的数量限制的

好的程序员知道虚拟存储器是一个有限的空间,必须高效地使用。对于可能被要求分配和释放大块存储器的动态存储器分配器来说,尤其如此。

5. 碎片

造成堆利用率很低的主要原因是一种称为碎片(fragmentation)的现象,

当虽然有未使用的存储器但不能用来瞒足分配诮求时,就发生这种现象。

有两种形式的碎片:内部碎片(internal fragmentation丿和外部碎片(external fragmentation)。

5.1 内部碎片

内部碎片是在一个已分配块比有效载荷大时发生的。
如:要分配1M,实际系统分配2M

很多原因都可能造成这个问题。
例如,一个分配器的实现可能对己分配块强加一个最小的大小值,而这个大小要比某个请求的有效载荷大。
内部碎片的量化是简单明了的。它就是己分配块和它们的有效载荷之差的和。因此.在任意时刻,内部碎片的数量只取决于以前请求的模式和分配器的实现方式。

5.2 外部碎片

外部碎片是当空闲存储器合计起来足够满足一个分配请求,
但是没有一个单独的空闲块足够大可以来处理这个诮求时发生的。

外部碎片比内部碎片的量化要困难得多,因为它不仅取决于以前请求的模式和分配器的实现方式,还取决于将来请求的模式。例如,假设在K个请求之后,所有空闲块的大小都恰好是4个字。这个堆会有外部碎片吗?答案取决于将来请求的模式。如果将来所有的分配请求都要求比4个字小的块,那么就不会有外部碎片。另一方面,如果有一个或者多个请求要求比4个字大的块,那么这个堆就会有外部碎片。

因为外部碎片是难以量化和不可能预测的,所以分配器典型地采用启发式策略来试图维持少量的大空闲块,而不是维持大量的小空闲块。

5. 实现问题

可以想像出的最简单的分配器会把堆组织成一个大的字节数组,还有一个指针p初始指向这个数组的第一个字节。为了分配size字节,malloc将P的当前值保存在栈里,将P增加size,并将P的旧值返回到调用函数。free只是简单地返回到调用函数,而不做其他任軻事情。

这个简单的分配器是设计中的一种极端情况。因为每个malloc和free只执行很少量的指令,吞吐率会极好。然而,因为分配器从不重复使用任何块,存储器利用率将极差。一个实际的分配器要在吞吐率和利用率之间把握好平衡,就必须考虑以下几个问题:

1. 空闲块组织:我们如何记录空闲块?
2. 放置:我们如何选择一个合适的空闲块来放置一个新分配的块?
3. 分割:在我们将一个新分配的块放置到某个空闲块之后,
		 我们如何处理这个空闲块中的剩余部分?
4. 合并:我们如何处理一个刚刚被释放的块?

本节剩下的部分将详细讨论这些问题。
因为像放置、分割以及合并这样的基本技术贯穿在许多不同的空闲块组织中,所以我们将在一种叫做隐式空闲链表的简单空闲块组织结构中来介绍它们。

6. 隐式空闲链表

6.1 一个简单的堆块格式图

任何实际的分配器都需要一些数据结构,允许它来区别块边界,并区别已分配块和空闲块。
大多数分配器将这些信息嵌在块本身当中。一个简单的方法如图10.37所示。
图10.37
在这里插入图片描述

在这种情况中,一个块是由一个字的头部、有效载荷,以及可能的一些额外的填充组成的。
头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。如果我们强加一个双字的对齐约束条件,那么块大小就总是8的倍数,且块大小的最低3位总是零。因此,我们只需要存储块大小的29个高位,释放剩余的3位来编码其他信息。
在这种情况中,我们用其中的最低位(己分配位)来指明这个块是己分配的,还是空闲的。

例如假设我们有一个已分配的块,大小为24(0x18)字节。那么它的头部将是

0x00000018 | 0x1 = 0x00000019

类似地,一个块大小为40(0x28)字节的空闲块有如下的头部:

0x00000028 | 0x0 = 0x00000028

头部后面就是应用调用malloc时请求的有效载荷。有效载荷后面是一块不使用的填充块,其大小可以是任意的。

需要填充有很多原因:

比如,填充可能是分配器策略的一部分,用来对付外部碎片
或者也需要用它来满足对齐要求

6.2 隐式空闲链表

假设块的格式如图10.37所示,我们可以将堆组织为一个连续的已分配块和空闲块的序列,如图10.38所示。

图10.38
在这里插入图片描述

我们称这种结构为隐式空闲链表

是因为空闲块是通过头部中的大小字段含地连接着的。

分配器可以通过遍历堆中的所有块,从而间接地遍历整个空闲块的集合。注意,我们需要以某种特殊标记结束的块,在这个小例中,就是一个设置了已分配位而大小为零的终止头部。

优缺点:

隐式空闪链表的优点是简单
显著的缺点是任何操作的开销
	例如放置分配的块,要求空闭链表的搜索与堆中己分配块和空闲块的总数呈线性关系。

很重要的一点就是意识到系统对齐要求和分配器对块格式的选择对分配器上的最小块大小有强
制的要求。没有己分配块或者空闲块可以比这个最小值还小。
例如。如果我们假设一个双字的对齐要求,那么每个块的大小都必须是双字(8字节)的倍数。因此,图10.37中的块格式就导致最小的块大小为两个字:一个字作头,另一个字维持对齐要求,即使应用只请求一字节,分配器也仍然需要创建一个两字的块。

7. 放置分配的快

当一个应用请求一个K字节的块时,分配器搜索空闲链表,查找一个足够大、可以放置所请求块的空闲块。
分配器执行这种搜索的方式是由放置策略确定的

一些常见的策略是首次适配,下一次适配和最佳适配

7.1 放置策略:首次适配,下一次适配和最佳适配

1. 首次适配从头开始搜索空闲链表,选择第一个合适的空闲块。
2. 从上一次查询结束的地方开始
	下一次适配和首次适配很相似,只不过不是从链表的起始处开始每次搜索
3. 最佳适配检查每个空闲块,选择匹配所需请求人小的最小空闲块。

首次适配的一个优点是它趋向于将大的空闲块保留在链表的后面。
缺点是它趋向在靠近链表起始处留下小空闲块的“碎片”,这就增加了对较大块的搜索时间。

下一适配是由Donald作为首次适配的一种代替品最早提出的,源于这样一个想法:

如果我们上一次在某个空闲块里己经发现了一个匹配,那么很可能下一次我们也能在这个剩余块中发现匹配。

7.2 放置策略比较

下一次适配比首次适配运行起来明显要快一些。
然而,一些研究表明,下一次适配的存储器利用率要比首次适配低得多。
研究还表明最佳适配比首次适配和下一次适配的利用率都要高一些。
然而,在简单空闲链表组织结构中,比如隐式空闲链表中,使用最佳适配的缺点是它要求对堆进行彻底的搜索。在后面,我们将看到更加精细复杂的分离式空闲链表组织,它实现了最佳适配策略,而不需要进行彻底的堆搜索。

8. 分割空闲块

一旦分配器找到一个匹配的空闲块,它就必须做另一个策略决定,就是分配这个空闲块中多少空间。
一个选择是用整个空闲块,虽然这种方式简单而快捷,但是主要的缺点就是它会造成内部碎片。如果放置策略趋向于产生好的匹配,那么额外的内部碎片也是可以接受的。
然而,如果匹配不太好,那么分配器通常会选择将这个空闲块分割为两部分。第一部分变成分配块,而剩下的变成一个新的空闲块。
图10.39展示了分配器如何分割图中8个字的空闲块,来满足一个应用的对堆存储器3个字的请求.

图10.39
在这里插入图片描述

9. 获取额外的堆内存

如果分配器不能为请求块找到合适的空闲块,将发生什么呢?
一个选择是通过合并那些在存储器中物理上相邻的空闲块来创建一些更大的空闲块。
然而,如果这样还是不能生成一个足够大的块,或者如果空闲块己经最大程度地合并了。那么分配器就会向内核请求额外的堆存储器,要么是通过调用mmap,要么是通过调用sbrk函数。在任一种情况下,分配器都会将额外的(或增加的)存储器转化成一个大的空闲块,将这个块插到空闲链表中,然后将被请求的块放置在这个新的空闲块中。

10. 合并空闲块

当分配器释放一个己分配块时,可能有其他空闲块与这个新释放的空闲块相邻。这些邻接的空闲块可能引起一种现象,叫做假碎片,这里有许多可用的空闲块被切割成为小的、无法使用的空闲块。

比如,图10.40展示了释放图10.39中分配的块后得到的结果。

图10.40

结果是两个相邻的空闲块,每个的有效载荷都为3个字。因此,接下来一个对4字有效载荷的请求就会失败,即使两个空闲块的合大小足够大,可以满足这个请求。

10.1 立即合并 推迟合并

为了对付假碎片问题,任何实际的分配器都必须合并相邻的空闲块,这个过程称为合并。这就提出了一个重要的策略决定,那就是何时执行合并。分配器

1. 可以选择立即合并,也就是在每次一个块被释放时,就合并所有的相邻块。
	优点:立即合并很简单明了,可以在常数时间内执行完成
	缺点:但是对于某些请求可能会产生一种形式的抖动,就是反复合并,反复分割。
	
2. 或者它也可以选择推迟合并,也就是等到某个稍晚的时候再合并空闲块。
	应用:快速的分配器通常会选择某种形式的推迟合并

11. 带边界标记的合并

分配器是如何实现合并的?
让我们称我们想要释放的块为当前块。那么,合并(存储器中的)下一个空闲块很简单而且高效。当前块的头部指向下一个块的头部,可以检查这个指针以判断下一个块是否是空闲的。如果是,就将它的大小简单地加到当前块头部的大小上,这两个块在常数时间内被合并,

11.1 如何合并前面的块-脚部

但是我们该如何合并前面的块呢?给定一个带头部的空闲链表,惟一的选则将是搜索整个链表,记住前面块的位置,直到我们到达当前块。使用隐式空闲链表,这意味着每次调用free的时间都与堆的大小成线性关系。即使使用更复杂精细的空闲链表组织,搜索时间也不会是常数。

Knuth提出了一种聪明而通用的技术,叫做边界标记(boundary tag),允许在常数时间内进行对前面块的合并

这种思想,如图10.41所示,是在每个块的结尾处添加一个脚部(footer 边界标记)
图10.41
在这里插入图片描述

其中脚部就是头部的一个副本。

如果每个块包括这样一个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当前块结尾位置1一个字的距离。

11.2 四种合并情况

考虑当分配器释放当前块时所有可能存在的情况:

1. 前面的块和后面的块都是己分配的。
2. 前面的块是己分配的,后面的块是空闲的。
3.前面的块是空闲的,而后面的块是已分配的。
4.前面的和后面的块都是空闲的。

图10.42
在这里插入图片描述

说明:
在情况1中,两个邻接的块都是己分配的,因此不可能进行合并。所以当前块的状态仅仅是从已分配变成空闲。
在情况2中,当前块与后面的块合并,用当前块和后面块的大小的和来更新当前块的头部和后面块的脚部。
在情况3中,前面的块和当前块合并。用两个块大小的和来更新前面块的头部和当前块的脚部。
在情况4中,要合并所有的三个块形成一个单独的空闲块,用二个块大小的和来更新前面块的头部和后面块的脚部。
在每种情况中,合并都是在常数时间内完成的。

11.3 带边界标记的合并的优缺点,总结

优点:
边界标记的概念是简单优雅的,它对许多不同类型的分配器和空闲链表组织都是通用的。

缺点:
然而,它也存在一个潜在的缺陷。要求每个块都保持一个头部和一个脚部,在应用秫序操作许多个小块时,会产生显著的存储器开销。
例如,如果一个图形应用通过反复调用malloc和free来动态地创建和销毁图形节点,并且每个图形节点都只要求两个存储器字,那么头部和脚部将占用每个己分配块的一半的空间。

扩展:
幸运的是,有一种非常聪明的边界标记的优化方法,能够使得在已分配块中不再需要脚部。回想一下,当我们试图在存储器中合并当前块以及前面的块和后面的块时,只有在前面的块是空闲时。才会需要用到它的脚部。如果我们把前面块的己分空闲位存放在当前块中多出来的低付中,那么已分配的块就不需要脚部了,这样我们就可将这个多出来的空间用作有效载荷了,不过请注意,空闲块仍然需要脚部.

12. 综合:实现一个简单的分配器

构造一个分配器是一件富有挑战性的枉务。设计空间很大,有多种块格式、空闲链表格式,及放置、分割和合并策略可供选择。
另一个挑战就是你经常被迫在类型系统的安全和熟悉的限定之外编程,即使用容易出错的指针强制类型转换和指针运算,这些操作都属于典型的低层系统编程,虽然分配器不需要大量的代码,但是它们也还是细微而不可忽视的。熟悉诸如C++或者java之类高级语的学生通常在他们第一次遇到这种类型的编程时,会遭遇一个概念上的障碍。为了帮助你清除这个障碍,我们将基于隐式空闲链表,使用立即边界标记合并方式,从头至尾地讲述一个简单分配器的实现。

例子略

13. 显式空闲链表

隐式空闲链表为我们提供了一种简单的介绍一些基本分配器概念的方法。然而,因为块分配与堆块的总数呈线性关系,所以对于通用的分配器,隐式空闲链表是不适合的(尽管对于堆块数量预先就知道是很小的特殊的分配器来说,它是比较好的)。

一种更好的方法是将空闲块组织为某种形式的显式数据结构。

13.1 显式空闲链表结构

因为根据定义,程序是不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。例如,堆可组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(祖先)和succ(后继)指针,如图10.50所示。

图10.50
在这里插入图片描述

13.2 显式空闲链表快排序策略

使用双向链表,而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。不过,释放一个块的时间可以是线性的,也可能是个常数,这取决于我们在空闲链表中对块排序所选择的策略。

1. 一种方法是用后进先出(LIFO)的顺序维护链表

将新释放的块放置在链表的开始处,使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块。在这种情况下,释放一个块可以在常数时间内完成。如果使用了边界标记,那么合并也可以在常数时间内完成。

2. 另一种方法是按照地址顺序来维护链表

其中链表中每个块的地址都小于它祖先的地址。在这种情况下,释放一个块需要线性时间的搜索,来定位合适的祖先。平衡点在于,按照地址排序的首
次适配比LIFO排序的首次适配有更高的存储器利用率,接近最佳适配的利用率。

13.3 显式空闲链表优缺点

一般而言,显式链表的缺点是空闲块必须足够大,以包含所有需要的指针,以及头部和可能的脚部。

这就导致了更大的最小块大小,也潜在地提高了内部碎片的程度。

14. 分离的空闲链表

14.1 分离存储

就像我们己经看到的,一个使用单向空闲块链表的分配器需要与空闲块数量成线性关系的时间来分配块。

一种流行的减少分配时间的方法,通常称为:分离存储,
维护多个空闲链表,其中每个链表中的块有大致相等的人小。

一般的思路是将所有可能的块大小分成一些等价类,也叫做大小类,有很多种方式来定义大小类。
例如,我们可以根据2的幂来划分块大小:

1,2,3-4,5-8,9-16,……2049-4096,4097-无穷大

分配器维护着一个空闲链表数组,每个大小类一个空闲链表,按照大小的升序排列。当分配器需要一个大小为n的块时,它就搜索相应的空闲链表。如果它不能找到合适的块与之匹配,它就搜索下一个链表,以此类推。

14.2 两种基本的方法:简单分离存储和分离适配

有关动态存储分配的文献描述了很多种分离存储方法,主要的区别在于它们加何定义大小类,何时进行合并,何时向操作系统请求额外的堆存储器,是否允许分割,等等。为了使你大致了解有哪些可能性,我们会描述两种基本的方法:简单分离存储和分离适配

14.2.1 简单分离存储

使用简单分离存储,每个大小类的空闲链表包含大小相等的块,每个块的大小就是这个大小类中最大元素的大小。

例如,如果某个大小类定义为{17-32},那么这个类的空闲链表全由大小为32的块组成。

为了分配一个给定大小的块,我们检查相应的空闲链表。
如果链表非空,我们简单地分配其中第一块的全部。空闲块是不会分割以满足分配请求的。
如果链表为空,分配器就向操作系统请求一个固定大小的额外存储器组块(典型地是页面大小的整数倍),将这个组块分成大小相等的块,并将这些块链接起来形成新的空闲链表。要释放一个块,分配器只要简单地将这个块插入到相应的空闲链表的前部。

优点:
这种简单方法有许多优点。
分配和释放块都是很快的常数时间操作。而且,每个组块(chunk)中都是大小相等的块,不分割,不合并,这意味着每个块只有很少的存储器开销。既然每个组块只有大小相同的块,那么一个己分配块的大小就可以从它的地址中推断出来。因为没有合并,所以己分配块的头部就不需要一个己分配/空闲标记。因此已分配块不需要头部,同时因为没有合并,它们也不需要脚部。因为分配和释放操作都是在空闲链表的起始处操作,所以链表只需要是单向的,而不用是双向的了。关键点在于,惟一在任何块中都需要的字段是每个空闲块中的一个字的succ指针,因此最小块大小就是一个字。

缺点:
一个显著的缺点是,简单分离存储很容易造成内部和外部碎片。
因为空闲块是不会被分割的,所以可能会造成内部碎片。
更糟的是,某些引用模式会引起极多的外部碎片,因为是不会合并空闲块的。

解决缺点:
研究者提出了一种粗糙的合并形式来对付外部碎片问题,分配器记录操作系统返回的每个存储器组块(chunk)中的空闲块的数量。无论何时,如果有一个组块完全由空闲块组成,那么分配器就从它的当前大小类中删除这个组块,使得它对其他大小类可用。

14.2.2 分离适配

定义:
使用这种方法,分配器维护着一个空闲链表的数组。
每个空闲链表是和一个大小类相关联的,并且被组织成某种类型的显式或隐式链表。每个链表包含潜在的大小不同的块。这些块的大小是大小类的成员。有许多种不同的分离适配分配器。

这里,我们描述了一种简单的版本。
为了分配一个块,我们必须确定请求的大小类,并且对适当的空闲链表做首次适配,查找一个合适的块。如果我们找到了一个,那么我们(可选地)分割它,并将剩余的部分插入到适当的空闲链表中。如果我们找不到合适的块,那么我们就搜索下一个更大的大类的空闲链表。如此重复,直到找到一个合适的块。如果没有空闲链表中有合适的块,那么我们就向操作系统请求额外的堆存储器,从这个新的堆存储器中分配出一个块,将剩余的部分放置在最大的大小类中,要释放一个块,我们执行合并,并将结果放置到相应的空闲链表中。

例子:
分离适配方法是一种常见的选择,c标准库中提供的GNU malloc包就是采用的这种方法,因为这种方法既快速,对存储器的使用也很有效率。搜索时间减少了,因为搜索被限制在堆的某个部分,而不是整个堆。

存储器利用率得到了改善,因为有一个有趣的事实:

对分离空闲链表的简单的首次适配搜索相当于对整个堆的最佳适配搜索。

14.2.3 伙伴系统

伙伴系统(buddy system)是分离适配的一种特例,其中每个大小类都是2的幂。
基本的思路是假设一个堆的大小为2的m次方个字,我们为每个块大小2的k次方维护一个分离空闲链表(0<k<=m)。
请求块大小向上舍入到最接近的2的幂。最开始时,只有一个大小为2的m次方字的空闲块。

为了分配一个大小为2的k次方的块,我们找到第一个可用的、大小为2的j次方的块,其中k<=j<=m。如果j=k,那么我们就完成了。否则,我们递归地二分这个块,直到j=k。当我们进行这样的分割时,每个剩下的半块(也叫做伙伴),被放置在相应的空闲链表中。要释放一个大小为2的k次方块,我们继续合并空闲的伙伴。当我们遇到一个己分配的伙伴时,我们就停止合并。

关于伙伴系统的一个关键事实是,给定地址和块的大小,很容易计算出它的伙伴的地址。
例如,一个块,大小为32字节,地址为:

xxx一x00000
它的伙伴的地址为
xxx一x10000

换句话说,一个块的地址和它的伙伴只有一位不相同。

伙伴系统优缺点:
伙伴系统分配器的主要优点是它的快速搜索和快速合并。
主要缺点是要求块大小为2的幂可能导致显著的内部碎片。
因此,伙伴系统分配器不适合通用目的的工作负载。
然而,对于某些与应用相关的工作负载,其中块大小预先知道是2的幂,伙伴系统分配器就很有吸引力了。

参考:
Linux内存管理之slab 1:slab原理
https://blog.csdn.net/lqy971966/article/details/112980005
Linux内存管理之slab 2:slab API
https://blog.csdn.net/lqy971966/article/details/119801912

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值