基于C语言的动态内存分配器

资源下载地址:https://download.csdn.net/download/sheziqiong/88324880
资源下载地址:https://download.csdn.net/download/sheziqiong/88324880

基于C语言的动态内存分配器

一、实验基本信息

实验目的

理解现代计算机系统虚拟存储的基本知识

掌握 C 语言指针相关的基本操作

深入理解动态存储申请、释放的基本原理和相关系统函数

用 C 语言实现动态存储分配器,并进行测试分析

培养 Linux 下的软件系统开发与测试能力

实验环境与工具

硬件环境

CPU;2GHz;2G RAM;256GHD Disk 以上

软件环境

Windows7 64 位以上;VirtualBox/Vmware 11 以上;Ubuntu 16.04 LTS 64 位/优麒麟 64 位

开发工具

clion

二、实验预习

总分 20 分

动态内存分配器的基本原理(5 分)

动态内存分配器维护者一个进程的虚拟内存区域堆,每个块就是一个连续的虚拟内存片,已分配的块显式的保留为供应用程序使用,空闲块可用来分配。空闲块保持空闲,直到它显示的被应用所分配。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

带边界标签的隐式空闲链表分配器原理(5 分)

一个块是由一个字的头部、有效载荷,以及可能的一些额外的填充组成的,头部编码了这个快的大小,以及这个块是已分配的还是空闲的。每进行一个操作,就需要链表进行一次搜索,如果要分配内存,就找到大小符合的块进行分配,,如果要释放,就将头部和尾部的信息修改为空闲,然后搜索相邻的块进行合并成一个大块。

显示空间链表的基本原理(5 分)

在每个空闲块里都包含一个 pred(前驱)和后继指针,形成一个双向链表,是的首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。

红黑树的结构、查找、更新算法(5 分)

结构:

每个节点或者是黑色,或者是红色。

根节点是黑色。

每个叶子节点(NIL)是黑色。

如果一个节点是红色的,则它的子节点必须是黑色的。

从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

查找:

与普通二叉树的查找方法相同。

更新:

插入:将新插入的节点标记为红色并按照普通二叉树的插入方法插入到红黑树中,然后对树进行调整使其重新满足红黑树的 5 条性质,具体调整方案如下:

情形 1:新节点 N 位于树的根上,没有父节点。在这种情形下,我们把它重绘为黑色以满足性质 2。因为它在每个路径上对黑节点数目增加一,性质 5 匹配。

情形 2:新节点的父节点 P 是黑色,所以性质 4 没有失效(新节点是红色的)。在这种情形下,树仍是有效的。性质 5 也未受到威胁,尽管新节点 N 有两个黑色叶子子节点;但由于新节点 N 是红色,通过它的每个子节点的路径就都有同通过它所取代的黑色的叶子的路径同样数目的黑色节点,所以依然满足这个性质。

情形 3:如果父节点 P 和叔父节点 U 二者都是红色,则我们可以将它们两个重绘为黑色并重绘祖父节点 G 为红色(用来保持性质 5)。现在我们的新节点 N 有了一个黑色的父节点 P。因为通过父节点 P 或叔父节点 U 的任何路径都必定通过祖父节点 G,在这些路径上的黑节点数目没有改变。但是,红色的祖父节点 G 可能是根节点,这就违反了性质 2,也有可能祖父节点 G 的父节点是红色的,这就违反了性质 4。为了解决这个问题,我们在祖父节点 G 上递归地进行情形 1 的整个过程。(把 G 当成是新加入的节点进行各种情形的检查)

情形 4:父节点 P 是红色而叔父节点 U 是黑色或缺少,并且新节点 N 是其父节点 P 的右子节点而父节点 P 又是其父节点的左子节点。在这种情形下,我们进行一次左旋转调换新节点和其父节点的角色;接着,我们按情形 5 处理以前的父节点 P 以解决仍然失效的性质 4。注意这个改变会导致某些路径通过它们以前不通过的新节点 N 或不通过节点 P,但由于这两个节点都是红色的,所以性质 5 仍有效。

情形 5:父节点 P 是红色而叔父节点 U 是黑色或缺少,新节点 N 是其父节点的左子节点,而父节点 P 又是其父节点 G 的左子节点。在这种情形下,我们进行针对祖父节点 G 的一次右旋转;在旋转产生的树中,以前的父节点 P 现在是新节点 N 和以前的祖父节点 G 的父节点。我们知道以前的祖父节点 G 是黑色,否则父节点 P 就不可能是红色(如果 P 和 G 都是红色就违反了性质 4,所以 G 必须是黑色)。我们切换以前的父节点 P 和祖父节点 G 的颜色,结果的树满足性质 4。性质 5 也仍然保持满足,因为通过这三个节点中任何一个的所有路径以前都通过祖父节点 G,现在它们都通过以前的父节点 P。在各自的情形下,这都是三个节点中唯一的黑色节点。

删除:仅讨论最复杂的要删除的节点和它的儿子二者都是黑色的情况,我们首先把要删除的节点替换为它的儿子。出于方便,称呼这个儿子为 N(在新的位置上),称呼它的兄弟(它父亲的另一个儿子)为 S。我们还是使用 P 称呼 N 的父亲,SL 称呼 S 的左儿子,SR 称呼 S 的右儿子。

情形 1: N 是新的根。在这种情形下,我们就做完了。我们从所有路径去除了一个黑色节点,而新根是黑色的,所以性质都保持着。

情形 2: S 是红色。在这种情形下我们在 N 的父亲上做左旋转,把红色兄弟转换成 N 的祖父,我们接着对调 N 的父亲和祖父的颜色。完成这两个操作后,尽管所有路径上黑色节点的数目没有改变,但现在 N 有了一个黑色的兄弟和一个红色的父亲(它的新兄弟是黑色因为它是红色 S 的一个儿子),所以我们可以接下去按情形 4、情形 5 或情形 6 来处理。

情形 3: N 的父亲、S 和 S 的儿子都是黑色的。在这种情形下,我们简单的重绘 S 为红色。结果是通过 S 的所有路径,它们就是以前不通过 N 的那些路径,都少了一个黑色节点。因为删除 N 的初始的父亲使通过 N 的所有路径少了一个黑色节点,这使事情都平衡了起来。但是,通过 P 的所有路径现在比不通过 P 的路径少了一个黑色节点,所以仍然违反性质 5。要修正这个问题,我们要从情形 1 开始,在 P 上做重新平衡处理。

情形 4: S 和 S 的儿子都是黑色,但是 N 的父亲是红色。在这种情形下,我们简单的交换 N 的兄弟和父亲的颜色。这不影响不通过 N 的路径的黑色节点的数目,但是它在通过 N 的路径上对黑色节点数目增加了一,添补了在这些路径上删除的黑色节点。

情形 5: S 是黑色,S 的左儿子是红色,S 的右儿子是黑色,而 N 是它父亲的左儿子。在这种情形下我们在 S 上做右旋转,这样 S 的左儿子成为 S 的父亲和 N 的新兄弟。我们接着交换 S 和它的新父亲的颜色。所有路径仍有同样数目的黑色节点,但是现在 N 有了一个黑色兄弟,他的右儿子是红色的,所以我们进入了情形 6。N 和它的父亲都不受这个变换的影响。

情形 6: S 是黑色,S 的右儿子是红色,而 N 是它父亲的左儿子。在这种情形下我们在 N 的父亲上做左旋转,这样 S 成为 N 的父亲(P)和 S 的右儿子的父亲。我们接着交换 N 的父亲和 S 的颜色,并使 S 的右儿子为黑色。子树在它的根上的仍是同样的颜色,所以性质 3 没有被违反。但是,N 现在增加了一个黑色祖先:要么 N 的父亲变成黑色,要么它是黑色而 S 被增加为一个黑色祖父。所以,通过 N 的路径都增加了一个黑色节点。

此时,如果一个路径不通过 N,则有两种可能性:

它通过 N 的新兄弟。那么它以前和现在都必定通过 S 和 N 的父亲,而它们只是交换了颜色。所以路径保持了同样数目的黑色节点。

它通过 N 的新叔父,S 的右儿子。那么它以前通过 S、S 的父亲和 S 的右儿子,但是现在只通过 S,它被假定为它以前的父亲的颜色,和 S 的右儿子,它被从红色改变为黑色。合成效果是这个路径通过了同样数目的黑色节点。

在任何情况下,在这些路径上的黑色节点数目都没有改变。所以我们恢复了性质 4。

三、分配器的设计与实现

  • 总分 50 分
  • 总体设计(10 分)
  • 介绍堆、堆中内存块的组织结构,采用的空闲块、分配块链表/树结构和相应算法等内容。
  • 我的优化主要是在给的隐式空闲链表分配器的基础上,将其拓展成显式空闲链表分配器,就是在空闲链表内加入两个指针,一个指向前一个空闲块的地址,一个指向后一个空闲块的地址。
  • 再有分配请求时,就从空闲链表的起点开始,寻找大小合适的空闲块,一旦找到合适大小的空闲块,就将这个块从空闲链表中删除,然后给用户返回这个块的指针供用户使用。
  • 如果没有合适大小的空闲块,就重新拓展堆空间,将拓展后的空间分配出去。
  • 在遇到释放空间的请求时,对这个块的前后块进行访问,看是否是空闲,如果有空闲块,就将其合并成一个块(在合并之前需要将相邻的空闲块从空闲链表中删除),加入空闲链表中。
  • 遇到 realloc()请求时,就重新申请一块请求大小的块,然后将原来的块的内容复制到新申请的块中,将原来的块释放掉。
  • 总结:我的优化使用了 显示空闲链表 + 基于边界标签的空闲块合并 + 首次适配
  • 关键函数设计(40 分)

int mm_init(void)函数(5 分)

函数功能:初始堆空间,设置序言块

处理流程:首先申请 16 字节的堆空间,将 8 字节设置为起始和终止的标志块,然后再将中间的 8 字节设置为序言快,并初始化一个比较合适大小的块。

要点分析:

设置好起始和终止块,即要设置好边界条件,避免在分配和访问过程中越界。

void mm_free(void *ptr)函数(5 分)

函数功能:将 ptr 指向的空间释放掉,使得分配器能够再次将这块内存分配使用

参 数:这个参数指向的是想要释放掉的空间的首地址

处理流程:先将这个块的头部和尾部的标记信息设置成空闲,然后调用合并函数将与之相邻的空闲块合并成一个空闲块,然后将这个空闲块加入空闲链表

要点分析:设置头部和尾部的空闲块信息,合并相邻的空闲块,将空闲块加入空闲块链表。

void *mm_realloc(void *ptr, size_t size)函数(5 分)

函数功能:将原本的空闲块的大小重新该为 size 大小

参 数:ptr 指向需要改变大小的块,size 的大小是修改后的大小

处理流程:先申请一块大小为 size 的块,然后将原来块上的内容复制到新分配的块上,将旧的块释放掉

要点分析:在新分配块后,一定要将原来块上得到内容复制到新的块上,否则就会造成原来的块上内容的丢失

int mm_check(void)函数(5 分)

函数功能:检查左右的块是否以 8 字节对其,正在使用的块是否有重叠,超出堆空间。是否所有的空闲块都已经合并,是否每个空闲块都在空闲链表中

处理流程:遍历堆上的所有块,一次判断是否有以上这几种常见错误的发生。再遍历一次空闲链表,判断是否所有的空闲链表中的块都是空闲块。

要点分析:在检查是否有错误发生时,判断条件要仔细检查,如果有条件错误,就会有一些隐蔽的错误查不出来,一般这些错误都是很严重的错误。

void *mm_malloc(size_t size)函数(10 分)

函数功能:根据请求的大小,分配大于该大小的内存块,返回指向该内存块的指针

参 数:size 是用户请求的内存块大小

处理流程:先将 size 的大小 8 字节对其,然后再在空闲链表中查找块大小,如果有合适的块大小,就将这个块从空闲链表中删除,将这个块的首地址返回给用户

要点分析:需要遍历空闲链表,寻找匹配的空闲块,这里是制约性能的关键之处,按照首次适配的原则就可以减少搜索的时间,即遇到符合大小的块就停止搜索。

但是会降低块的利用率。

static void *coalesce(void *bp)函数(10 分)

函数功能:将一个块与相邻的空闲块合并

处理流程:先判断相邻块的使用情况,然后根据具体的情况合并空闲块,再将合并后的空闲块加入空闲链表,具体的情况见要点分析。

要点分析:有四种情况:

只有前面的块空闲,需要将前面的块从空闲链表中删除,然后将两个块的大小合并,形成一个新块,将其加入空闲链表

只有后面的块空闲,需要将后面的块从空闲链表中删除,然后将两个块的大小合并,形成一个新块,将其加入空闲链表

前后两个块都空闲,处理同上

前后两个块都不空闲,就直接将当前块加入空闲链表

四、测试

总分 10 分

4.1 测试方法

使用自己写的 mm_check()函数进行简单的检查(在 malloc 函数中调用 mm_check()函数)

使用实验提供的检查工具,使用 mdriver [-hvVa] [-f ]

指令调用测试程序

4.2 测试结果评价

在第一个测试中,没有报出 mm_check()检查的错误

在第二个测试中,检查全部正确,最终的评分为 82 分,表明吞吐率和使用率都很不错,但是还有优化的空间。

4.3 自测试结果

见截图 4.2-1

截图 4.2-1

资源下载地址:https://download.csdn.net/download/sheziqiong/88324880
资源下载地址:https://download.csdn.net/download/sheziqiong/88324880

猜你喜欢

转载自blog.csdn.net/newlw/article/details/132799392
今日推荐