mallo内存管理

1malloc是怎样实现一个虚拟内存分配器的?底层是怎样工作的?

malloc的核心是底层调用了sbrk(),如下图linux的内存布局,其中包括3GB的用户空间和1GB的内核空间,用户空间从下到上依次是代码段、数据段、未初始化的数据段、堆、栈。
在这里插入图片描述
从图中可以看到有一个program break的brk指针,它代表的意义是:当前进程能访问的最大堆的虚拟内存空间的顶部指针,通过sbrk()可以调整他的大小,malloc的核心也在于这里,当进程申请堆空间时,如果当前的堆空间不足,malloc会调用sbrk向上移动该brk指针,从而扩大进程能够访问的虚拟内存空间。

2 malloc()和free()函数

void *malloc(size_t size);
void free(void *ptr);
malloc会传入一个size大小的堆内存大小,调用malloc()后malloc()的分配器会分配根据size返回一个8字节或者4字节对齐的大小,并且返回一个堆内存的基址,对于分配的这段内存块,malloc()会用一个固定大小的头部信息记录分配的信息,包括整个块的大小

对于free(),free()会回收当前已分配的内存块,并且会将这个块加入到空闲链表中,空闲链表的定义会在下面介绍,对于free()传入的是malloc()返回的一个基址,记住free()传入的这个基址一定要是malloc返回的位置,free()其它的位置会出现未定义的行为,因为free()会通过基址偏移回去寻找头部中的整个块大小然后free(),这也是为什么free()不需要指定大小的原因。
在这里插入图片描述

在malloc()的时候如果每次都调用sbrk()去移动堆顶指针的话,性能会很低下,因此malloc底层维护了一个分配器,分配器只有在内存不够的时候才会去调用sbrk进行内存分配,在free()后将分配后的内存块加入空闲链表,而不是将内存返还给操作系统,下次分配的时候去空闲链表中查找。那么分配器利用了什么数据结构来维护进程频繁地malloc和free呢?答案是空闲链表。空闲链表,空闲链表的方式有:隐式空闲链表和显示空闲链表。别从字面上去理解这个意思,空闲链表的链表不一定全是没有分配的块,有可能是已经分配的块

3 隐式空闲链表

整个堆内存是一片连续的虚拟内存空间,隐式空闲链表的堆块格式如图
在这里插入图片描述
头部的最后三位的其中一位用来标记该内存块的分配状态(因为有效负载大小是8字节对齐的,最后三位没有用,就用最后三位来做标记),隐式空闲链表的组织方式如图
在这里插入图片描述
隐式空闲链表的优点是简单
缺点是任何操作的开销,例如放置分配的块,要求对空闲链表进行搜索,该搜索的时间与堆中已分配的块和空闲块的总数成线性关系,因此出现了显式空闲链表

4 显式空闲链表

显式空闲链表用一个pred前驱指针和succ的后继指针指向其余的空闲块,用双向链表组织的数据结构,它的堆块格式如图
在这里插入图片描述
显式空闲链表使对空闲链表的搜索减少到空闲块的线性时间,它的缺点是占得内存大,因为它需要pred
和succ的后继的内存在保存前驱和后继结点
对于显示空闲链表,他有两种方式来维护链表:

  1. 后进先出(LIFO)的方式 将新释放的块放置在链表头部
  2. 按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址。在这种情况下,释放一个块的时候需要去寻找合适的前驱结点,因此它需要线性的时间来搜索合适的前驱。

在知道分配器对分配块的数据结构组织方式后,还需要考虑几个问题:

  1. 怎么去搜索然后放置一个新的分配的块?
  2. 如果开始malloc(24)个字节然后free()后,该块有24个字节的大小,但是我们再次malloc(8),那么我们只需要8个字节大小的空间,如果全部分配,那么会产生很多的内部碎片,因此我们需要怎么去分割这个空闲块?
  3. 假设malloc(4) free(4) malloc(8) free(8)…那么内存中会有很多小的空闲块,当我们再malloc(12)个字节大小的空间时,其实是有空间可以分配的,只需要将前面两个malloc和free的空闲块合并即可,没必要再次调用sbrk()去进行扩容,因此我们需要进行合并,否则会产生很多的外部碎片,而合并也会涉及到一个何时合并的问题?第一种方式是在free()后去合并,二是没有内存空间分配,想去调用sbrk()再次分配内存的时候进行合并
    下面解答上面这3个问题

5 怎么去搜索然后放置一个新的分配的块

三种方式:

  1. 首次适配
    首次适配就是去查找空闲链表,找到第一个满足要求的空闲块
    优点是它能让大的空闲块留在链表的后面
    缺点是在靠近链表的头部会留下很多小空闲块的外部碎片
  2. 下次适配
    和首次适配差不多,只是下次适配不是每次都从链表的开始处开始搜索,而是每次从上一次查询的结束地方开始
  3. 最佳适配
    最佳适配会搜索整个空闲块,然后查找最合适的那个空闲块

6 怎么去分割空闲块

在这里插入图片描述

7 怎么去合并?

如果采用图9-35的堆块格式来进行合并,如果采用立即合并的方式,我们只能合并他的后继结点,因为我们知道头部的固定大小,可以访问到下一个堆块的头部,但是我们不能访问到他的前驱结点,如果需要合并它的前驱结点需要重新遍历链表,找到它的前驱结点然后合并,那么它的时间复杂度是0(n),n为空闲链表的个数,那么出现了带边界标记的合并方法,它的堆块格式如图
在这里插入图片描述

8分离存储

它采用的方式是添加一个和头部信息一样的脚部块,通过这个脚部块我们可以在合并的时候查找到它的前驱结点块,合并前驱结点的复杂度可以降低到O(1)
上面说了空闲链表,那么对于空闲链表的存储方式也有很多种,用于减少分配时间,一般的思路是将所有可能的块大小分成一些等价类,叫做大小类,有很多种方式来定义大小类,例如,可以根据2的幂来划分块大小:
{1},{2},{3,4},{5-8},…{1025-2048},{2049-4096},{4097-∞}
也可以将晓得块分配到他们的大小类里,而将大块按照2的幂分类:
{1},{2},{3},{4}…{1025-2048},{2049-4096},{4097-∞}
分配器维护着一个空闲链表数组,每个大小类一个空闲链表,按照大小的升序排列,当分配器需要一个大小为n的块时,他就搜索相应的空闲链表,如果不能找到合适的块与之匹配,它就搜索下一个链表,以此类推
对于分离存储得到方法有很多种,这里介绍3种

8.1 简单分离存储

每个大小类的空闲链表包含大小相等的块,如大小类定义为{17-32},那么这个类的空闲链表全由大小为32的块组成。
分配:
分配的时候去查找相应的空闲链表,如果有空闲的直接分配第一个块的全部,不会分割,如果链表为空那么才会调用sbrk()去申请内存再添加到空闲链表后再进行分配
释放:
释放一个块,分配器就简单地将这个块插入到相应空闲链表的头部。
优点有很多:

  1. 分配和释放都是O(1)的时间复杂度
  2. 不分割不合并,因此不需要头部和脚部标记,节省内存
    缺点也很多:
  3. 因为不分割,所以会有很多内部碎片
  4. 因为不合并,所以会有很多外部碎片。这个怎么理解?当我们频繁分配然后回收小字节的内存块的时候,空闲链表会有很多小字节的空闲块,如果以后我们都不申请小字节的块,那么会产生很多的外部碎片

8.2 分离适配存储

分离存储:
分配:
为了分配一个块,他会请求相应的大小类,并且对适当的空闲链表做首次适配,查找一个合适的块,如果找到了一个,那么就(可选地)分割它,并且将剩余的部分插入到适当的空闲链表中,如果找不到合适的块,就搜索下一个空闲链表,如此重复,知道找到一个合适的块。如果空闲链表中没有合适的块,那么就向操作系统请求额外的堆内存,从这个新的堆内存中分配出一个块,将剩余的部分放置在适当大小的大小类中
释放:
在释放的时候会执行合并,然后将结果放置到相应的空闲链表中

malloc采用的是这种方法

8.3 伙伴系统

分配:
伙伴系统是分离适配的一种特例,假如一个堆内存1M,最小块大小是64KB,因此,order=4(2^10 / 64=4),如果申请一个32KB(order=1,order会向上舍入到最接近2的幂)大小的空间,他会将一个1M大小的堆分割成2块512KB(order=3)大小的堆A,B,他们互为伙伴,但是它没有达到order的要求,因此他会递归地分割内存块,直到order满足要求,他将A分割成两个256KB大小的堆C,D,然后将C分割成两个128KB大小的堆E,F,再将E分割成两个64KB大小的堆G,H,此时满足order=1的要求,然后分配一个64KB大小内存块给申请的用户。
释放:
释放的时候他会去寻找他的伙伴是否空闲,只有他的伙伴空闲他才会合并,递归结束的条件也是伙伴不空闲。
如合并G,H如果H空闲,他会合并,最后变成了E,如果F(E的伙伴)也空闲他会继续向上合并,如果F已分配,合并结束。
伙伴系统两个伙伴的地址只相差一位:
如一个块,大小为32字节,地址为:
xxx…x00000
它的伙伴地址为
xxx…x10000
具体详细的可参考:
https://blog.csdn.net/vanbreaker/article/details/7605367

参考文献:
深入理解计算机系统第三版

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值