第3篇-C堆内存管理-原理篇

前言

我前面一篇详细介绍了堆内存管理的有关概念,你往下读该篇的内容,我确信你已经阅读了我前面2篇有关堆内存管理的随笔。

  • 《第1篇:C++ 堆内存管理器-allocator》
  • 《第2篇:C堆内存管理-概念篇》

本系列文章并不是malloc/free的api说明书,如果你对C/C++的内存管理一无所知,建议请搞懂了再往下阅读,我自己主页有个专辑专门就是归纳《C/C++内存管理》的有关编程技术。
还有下面这个图是贯穿整个C/C++堆内存管理的所有知识点。所以这个图要牢记在大脑中。

第3篇-C堆内存管理-原理篇_第1张图片

现在我们知道了动态内存分配的基础,那么现在看看如何在实现过程中,我们要解决那些问题呢?

  1. 对于给定的指针,free函数如何知道要释放的内存尺寸?
  2. 我们如何跟中堆内随机分布的闲置区块?
  3. 当堆中大量闲置区块可以满足上层应用的mallc请求,那么如何快速找到一个尺寸合适闲置区块返回给上层应用。
  4. 当分配区域时,找到的闲置区块比请求的尺寸数量要打,那么额外多额外空间,我们如何处理?
  5. 如何将free操作后的堆内存返回给堆空间?

解决方案

首先这里解决第一个提出的问题,通常在分配内存时,我们会在有效负载的的前一个overhead写入当前分配区域的长度,这个区块的前置overhead通常被称为内存块的头部(Heap Block Header 或简称 header),如下图所示。

第3篇-C堆内存管理-原理篇_第2张图片

需要注意的是,响应每个malloc请求的已分配区块都需要一个头部信息。虽然这会带来空间开销,但这是值的,举个例子,当调用malloc(5)时,如上图指针p2指向的是payload的首个字节,因为指针p2是返回给上层应用程序,因此payload的内存块对于内存程序是可见的。而payload的前一个字(4字节)的块,malloc会默认设置该payload的头字段,并且将该已分配区块尺寸写入该头字段,因此当我们尝试使用将给定的指针传递给free函数,free函数会读取该头字段,就知道需要释放的内存的尺寸有多大。

备注:上图的mallc(5)的内存申请数量和该已分配区块的头字段内的尺寸数相差1,因为头字段占用一个

下一个 问题就是是如何跟中堆内随机分布的闲置区块?在C标准的堆分配器的实现中存在两种方法

  • 隐式链表(Implict List),分配器的底层逻辑通过一个内部链表将所有区块的头字段链接起来,并且遍历该链表,查看这些区域的头部字段的尺寸以及状态(未分配,已分配),甚至可以根据区块的尺寸进行排序,因为我们总是可以读取堆上的区块的头字段信息
第3篇-C堆内存管理-原理篇_第3张图片
  • 显式链表(Explicit List):包含堆中所有闲置区块的链表
    第3篇-C堆内存管理-原理篇_第4张图片

当然还有其他复杂的方法:

  • 隔离闲置的链表(Segregated Free List):可以跟踪不同尺寸的置区块之间的链表,也可以说可以跟踪闲置区块之间是已分配的区块
  • 根据尺寸已排序的链表(Blocks Sorted List by sizes):可以使用平衡二叉树,指针位于每个空闲块中,长度用作关key值。

其实上面的后三种方法,都是从都一种典型的方法变种变种而来,链表的节点就是每个目标区块的头一个字的字段信息。因此他们的空间消耗其实差别是不大的。唯一区别实现时,优先遍历闲置区块,还是遍历已分配区块,还是两者同时都遍历。但普遍的高效实现是使用上述的最后一种。我们不妨从最经典原型的隐式链表说起.

隐式链表

对于堆空间中的每个区块,我们需要知道两件事情就是区块尺寸状态,而我们每个区块头一个字,在x86_64的就是8个字节的头字段,完全足够容纳这两个信息,下面是头字段的基本信息。

第3篇-C堆内存管理-原理篇_第5张图片
头字段格式
  • 在x86_64下是按8字节对齐的,低地址的位总以0填充,因此我们可以充分利用这些填充位作为区块状态的标记,即已分配(allocated)为1,否则为0
  • 头字段的高地址位就用来存储尺寸,或者你可以在添加其他统计信息

这里还有一个需要注意的是,当我们实现的malloc读取区块的头字段时,必须对状态标记位做屏蔽处理。

下图是一个8字节(2个字)对齐的隐式链表的堆内存块示例

第3篇-C堆内存管理-原理篇_第6张图片

现在我们来解答一下前文的遗留问题
Q: 隐式链表如何知道到达堆的末端?
A:这个我们在末端定义一个尺寸空间为0并且已分配的头节点作为标示即可。

Q:如何将free操作后的堆内存返回给堆空间?
A::其实,这个很简单对于给定的指针p,因为指针p指向的是payload的首个字节,我们只要对指针p执行移位操作移动到状态标记位,将allocated的标记从1改为0即可。

Q:对于malloc请求,那么如何快速找到一个尺寸合适闲置区块返回给上层应用?
A:通过while循环遍历链表,找到状态标记为0并且每次迭代头字段比较size和内存申请数量n,如果size>n大就返回该未分配区块的内存地址。但隐式链表由于没有对闲置区块按照尺寸进行排序,那么返回的闲置块区可能会比内存数大得多,此时会造成堆空间的资源浪费。

举个例子malloc(4)的请求,并且该请求后续不会增长,由于隐方链表由于先找到A会返回A的闲置区块而不是B,显然请求数4远远比闲置区块数9少得多,而B的闲置区块数是恰好是4,我们从资源利用是应该选择B才合理的。


第3篇-C堆内存管理-原理篇_第7张图片
未排序的链表会在分配内存块时,可能造成内存块利用率下降

显然解决这个问题就是要对隐式链表中的未分配的区块头字段按照尺寸数执行升序排列,才能解决这个。例如下图


第3篇-C堆内存管理-原理篇_第8张图片
升序排列后的链表

继续更新.....

你可能感兴趣的:(第3篇-C堆内存管理-原理篇)