openKylin论坛

 找回密码

Glibc 的malloc源代码分析 [复制链接]

本帖最后由 liuxing 于 2013-5-6 20:40 编辑

Glibc 的 malloc 源代码分析

有人写了一个测试程序

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <malloc.h>

main()
{
  int alloc_time = 20000;
  char* a[alloc_time];
  char* b[alloc_time];
for(int j=0; j<5; j++)
{
  for(int i=0; i<alloc_time; i++)
  {
   
a[ i ]= (char*)malloc(52722);

    memset(a[ i ], 1, 52722);
    b
[ i ]= (char*)malloc(1);
    memset(b
[ i ] ,1, 1);
  }
  for(int i=0; i<alloc_time; i++)
  {
    free(a[ i ]
);
    free(b[ i ]
);
  }
}
  while(1)
  {
    sleep(10);
  }
}

发现 该程序在测试机上运行会占用 1G 内存,不释放,为了解决这个问题,特别去研究了一下glibc 中malloc 的源代码。






一.对于小于 128k 的块在 heap 中分配。

1. 堆是通过 brk 的方式来增长或压缩的,如果在现有的堆中不能找到合适的 chunk ,会通过增长堆的方式来满足分配,如果堆顶的空闲块超过一定的阀值会收缩堆,所以只要堆顶的空间没释放,堆是一直不会收缩的。

2. 堆中的分配信息是通过两个方式来记录。

第一.是通过 chunk 的头, chunk 中的头一个字是记录前一个 chunk 的大小,第二个字是记录当前 chunk的大小和一些标志位,从第三个字开始是要使用的内存。所以通过内存地址可以找到 chunk ,通过 chunk 也可以找到内存地址。还可以找到相邻的下一个 chunk ,和相邻的前一个 chunk 。一个堆完全是由 n 个chunk 组成 。

第二.是由 3 种队列记录 ,只用空闲 chunk 才会出现在队列中,使用的 chunk 不会出现在队列中。如果内存块是空闲的它会挂到其中一个队列中,它是通过内存复用的方式,使用空闲 chunk 的第 3 个字和第 4 个字当作它的前链和后链(变长块是第 5 个字和第 6 个字),省的再分配空间给它。

第一种队列是 bins , bins 有 128 个队列,前 64 个队列是定长的,每隔 8 个字节大小的块分配在一个队列,后面的 64 个队列是不定长的,就是在一个范围长度的都分配在一个队列中。所有长度小于 512字节(大约)的都分配在定长的队列中 。后面的 64 个队列是变长的队列,每个队列中的 chunk 都是从小到大排列的。

第二种队列是 unsort 队列(只有一个队列),(是一个缓冲)所有 free 下来的如果要进入 bins 队列中都要经过 unsort 队列。

第三种队列是 fastbins ,大约有 10 个定长队列,(是一个高速缓冲)所有 free 下来的并且长度是小于80 的 chunk 就会进入这种队列中。进入此队列的 chunk 在 free 的时候并不修改使用位,目的是为了避免被相邻的块合并掉。

3.malloc 的步骤

l         先在 fastbins 中找,如果能找到,从队列中取下后(不需要再置使用位为 1 了)立刻返回。

l         判断需求的块是否在小箱子 ( bins 的前 64 个 bin )范围,如果在小箱子的范围,并且刚好有需求的块,则直接返回内存地址;如果范围在大箱子( bins 的后 64 个 bin )里,则触发 consolidate 。(因为在大箱子找一般都要切割,所以要优先合并,避免过多碎片)

l         然后在 unsort 中取出一个 chunk ,如果能找到刚好和想要的 chunk 相同大小的 chunk ,立刻返回,如果不是想要 chunk 大小的 chunk ,就把他插入到 bins 对应的队列中去。转 3 ,直到清空,或者一次循环了 10000 次。

l         然后才在 bins 中找,找到一个最小的能符合需求的 chunk 从队列中取下,如果剩下的大小还能建一个chunk ,就把 chunk 分成两个部分,把剩下的 chunk 插入到 unsort 队列中去,把 chunk 的内存地址返回。

l         在 topchunk (是堆顶的一个 chunk ,不会放到任何一个队列里的)找,如果能切出符合要求的,把剩下的一部分当作 topchunk ,然后返回内存地址。

l         如果 fastbins 不为空,触发 consolidate 即把所有的 fanbins 清空 (是把 fanbins 的使用位置 0 ,把相邻的块合并起来,然后挂到 unsort 队列中去),然后继续第 3 步。

l         还找不到话就调用 sysalloc ,其实就是增长堆了。然后返回内存地址。

4.free 的步骤

l         如果和 topchunk 相邻,直接和 topchunk 合并,不会放到其他的空闲队列中去。

l         如果释放的大小小于 80 字节,就把它挂到 fastbins 中去,使用位仍然为 1 ,当然更不会去合并相邻块。

l         如果释放块大小介于 80-128k ,把 chunk 的使用位置成 0 ,然后试图合并相邻块,挂到 unsort 队列中去,如果合并后的大小大于 64k ,也会触发 consolidate ,(可能是周围比较多小块了吧),然后才试图去收缩堆。(收缩堆的条件是当前 free 的块大小加上前后能合并 chunk 的大小大于 64k ,并且要堆顶的大小要达到阀值,才有可能收缩堆)

l         对于大于 128k 的块,直接 munmap

二.大于 128k 的块通过 mmap 的方式来分配。



根据以上的分析,我们可以得出为什么会还占用 1G 的内存。

测试程序有两重循环,内层循环先分配 1G ,然后全部释放,外层执行这个步骤 5 次,但为什么最后一次释放会不成功呢。

主要是因为按照这个程序分配后,内存变成由小块和大块交替出现,释放小块的时候,直接把小块放到 fastbins中去,而且他的使用位还是 1 ,释放大块的时候,它试图合并相邻的块,但是和它相邻的块的使用位还是 1 ,所以它不能把相邻的块合并去来,而且释放的块的大小小于 64k ,也不会触发 consolidate ,即不会把 fastbins 清空 ,所以当所有的块都被释放完后,所有的小块都在 fastbins 里面,使用位都为 1 ,大块都挂在 unsort 队列里面。全部都无法合并。所以使用的堆更加无法压缩。

如果在外循环后面再分配 2000 字节然后释放的话,所有内存将全部清空。这是因为在申请 2000 字节的时候,由 malloc 的第二步,程序会先调用 consolidate ,即把所有的 fastbins 清空,同时把相邻的块合并起来,等到所有的 fastbins 清空的时候,所有的块也被合并去来了,然后调用 free2000 字节的时候,这块被将被合并起来 ,成为 topchunk ,并且大小远大于 64k ,所有堆将会收缩,内存归还给系统。




楼主
发表于 2013-5-4 21:55:24
回复

使用道具 举报

Glibc 的malloc源代码分析 [复制链接]

没仔细看,但感觉很高深!
沙发
发表于 2013-5-4 21:57:19
回复

使用道具 举报

Glibc 的malloc源代码分析 [复制链接]

豆丁 发表于 2013-5-4 21:57
没仔细看,但感觉很高深!

如果你学习操作系统-内存管理以后在看看这篇文章,读起来轻松一下。
板凳
 楼主| 发表于 2013-5-4 23:02:05
回复

使用道具 举报

Glibc 的malloc源代码分析 [复制链接]

特地注册了个号来提醒版主,您可能搞错了。a,b都是指针数组,您把malloc的返回值附给a和b不妥吧?或许改为a[i] = (char*) malloc(52722);比较合适。
地板
发表于 2013-5-6 20:04:51
回复

使用道具 举报

Glibc 的malloc源代码分析 [复制链接]

东方巽雷 发表于 2013-5-6 20:04
特地注册了个号来提醒版主,您可能搞错了。a,b都是指针数组,您把malloc的返回值附给a和b不妥吧?或许改为a ...

谢谢提醒
5#
 楼主| 发表于 2013-5-6 20:29:30
回复

使用道具 举报

openKylin

GMT+8, 2024-5-2 15:56 , Processed in 0.019313 second(s), 17 queries , Gzip On.

Copyright ©2022 openKylin. All Rights Reserved .

ICP No. 15002470-12 Tianjin

快速回复 返回顶部 返回列表