dlmalloc-2.6.6源码分析

本文详细介绍了Doug Lea的malloc(dlmalloc-2.6.6)的内存分配和回收机制,包括边界标记、分箱管理以及核心函数mALLOc和fREe的工作原理。文章阐述了如何通过双向链表管理和合并空闲chunk,以及如何通过分箱实现高效内存分配。此外,还讨论了dlmalloc的空间节省策略和内存碎片管理。

 

 

目    录

1. Doug Lea malloc简介 2

2. 边界标记 3

3. 分箱管理 7

4. 内存分配相关函数 14

4.1函数mALLOc( ) 14

4.2函数malloc_update_mallinfo( ) 22

5. 内存回收相关函数 25

5.1函数fREe( ) 25

6. dlmalloc2.8.4 28

7. 参考文档 28


1. 简介

Doug Lea malloc是一个用C语言实现的非常流行的内存分配器,由纽约州立大学Oswego分校计算机系教授Doug Lea于1987年撰写,许多人将其称为Doug Lea的malloc,或者简称dlmalloc,目前最新版本为2.8.4。

由于具备高效且占用空间较小等特点,dlmalloc被广泛使用,用Doug Lea自己的话说,就是“它在一些linux版本里面作为默认的malloc被使用,被编译到一些公共的软件包里,并且已经被用于各种PC环境及嵌入式系统,以及许多甚至我也不知道的地方”。

dlmalloc的由来,从Doug Lea自己写的文章看,似乎是这样的:1986年到1991年Doug Lea是libg++(即GNU C++ library)的主要作者,当时他写了大量有着动态分配内存的C++程序,结果发现程序跑得比预期慢,内存消耗也比预想的要大很多,追究下去,发现是所在系统内存分配器的问题。于是开始用C++为新类写一些特殊用途的分配器,但他很快意识到这并非一个好的策略,应该提供一个在C++和C环境下都能运行得很好的通用内存分配器,于是dlmalloc诞生了。在之后的日子里,Doug Lea和一些志愿者一直都在不断的维护优化这个内存分配器。dlmalloc之所以能被广泛应用,与其高标准的追求和不断的精益求精应该有着不可分割的关系。

另外,值得一提的是,Doug Lea是JAVA编程界的大师级人物,也是JCP中的一员。同时Doug还是一个无私的人,苹果越分越少,知识却越分越多,他深信知识的分享能激荡出不一样的火花。

本文以dlmalloc-2.6.6为分析对象,之所以选择这个版本而不是最新的版本,原因如下,一是公司项目操作系统用的是eCos,而eCos用的是dlmalloc-2.6.6;二是网友lenky0401已经很详细的分析了dlmalloc-2.8.3(见“参考文档”一节)。另外一个顺带的好处就是,通过两个版本的比较,可以找到从2.6.6到2.8.3的变迁及其缘由。   

尽管dlmalloc经历了诸多版本的变化,然而malloc算法的两个核心元素一直没变:边界标记和分箱管理。


2. 边界标记

在继续深入之前,有必要解释一下chunk的概念,这个概念对内存分配器而言十分重要。

chunk,“大块”的意思,在dlmalloc中指包含了用户空间、heap控制信息空间及出于对齐需求而多出来的空间的内存空间,是dlmalloc分配释放的基本操作对象。

有两种类型的chunk,已分配的chunk和未分配的chunk,两者交错排列,占据了整个heap空间。注意,没有相邻的两个未分配chunk,因为在调用free()释放被使用过的chunk时,dlmalloc将合并任何相邻的空闲chunk。交错的两种chunk看起来像这样:

 


figure 01

图1


Dlmalloc使用双向链表来管理空闲chunk,其节点数据结构体定义如下,

struct malloc_chunk

{

  INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */

  INTERNAL_SIZE_T size;       /* Size in bytes, including overhead. */

  struct malloc_chunk* fd;          /* double links -- used only if free. */

  struct malloc_chunk* bk;

};

成员prev_size记录了物理位置上相邻的前一个chunk的大小,利用prev_size可以找到前一个chunk,这在free( )时合并前一个空闲块时派上了用场;

成员size记录了该chunk的大小,dlmalloc32位处理器上总是8字节对齐,故size的低三位对size而言是无效的,dlmalloc利用这三位来记录一些信息,具体如下:

#define PREV_INUSE 0x1

bit[0]:物理位置上相邻的前一个chunk是否被分配使用的标志,如果为0x1,说明被分配;

#define IS_MMAPPED 0x2

bit[1]:如果为0x1,则表明该chunk通过mmap( )分配而得,那么在释放时调用munmap( )

fdbk则分别指向双向链表中前一个节点和后一个节点。

其物理布局看起来像这样:


图2


可以看出,chunk指针指向heap内部控制信息,图中head和foot区域的Size of chunk必须是一样的,如此nextchunk才能根据Size of chunk准确找到chunk的位置。

另一种是已分配的chunk,其结构体和未分配chunk结构体一样,只是不会使用fd和bk两个成员,因为被分配后已经不需要这两个域了,其物理布局看起来像下图,chunk指后面8字节的偏移处,即mem区域,是返回给用户的内存指针,该chunk的heap控制信息占据了8个字节,

图3


在调用malloc( )时首先会将用户申请的size转换为系统可用的size,

#define request2size(req) /

 (((long)((req) + (SIZE_SZ + MALLOC_ALIGN_MASK)) < /

  (long)(MINSIZE + MALLOC_ALIGN_MASK)) ? MINSIZE : /

   (((req) + (SIZE_SZ + MALLOC_ALIGN_MASK)) & ~(MALLOC_ALIGN_MASK)))

在32位处理器上等同下列表达式,

#define request2size(req) /

 (((long)((req) + (0x4 + 0x7)) < /

  (long)(0x10 + 0x7)) ? ((0x10 + 0x7) & ~(0x7)) : /

   (((req) + (0x4 + 0x7)) & ~(0x7)))

从这个宏定义中,我们可以获取三点信息:

一是系统可用的size和用户申请的size的差值,最小是0x4;

二是系统可用的size最小为16个字节,即sizeof(malloc_chunk);

三是系统可用的size 8字节对齐;   

说到这,或许你已经发现一个问题了,如果用户申请20个字节的空间,姑且称之为A,系统会分配24字节,而chunk的heap控制信息占了8个字节,那留给用户使用的只剩下18个字节了。如此看来,岂不是会覆盖下一个chunk(姑且称之为B)的“Size of previous chunk”区域?

这个问题问得好,学而思,而后得解,我们才能更加充分认识到这个设计的思想。为解答这个问题,我们先了解什么时候需要定位前一个chunk?只有在释放一块空间,判断前一个chunk是否空闲时才需要该动作。换而言之,当一个chunk被分配使用时,它根本不需要下一个chunk被释放时来合并它,既然不需要,就利用起来吧。于是,B的“Size of previous chunk”区域也被纳入到A的用户空间中了。

图4

从这一点讲,上图中的“Size of previous chunk, if allocated”的表述是不对的,应该是“Size of previous chunk, if freed”。

    

我曾分配了大小为0x98c的一块空间, 打印出来的控制信息证明了我的观点。

图5


域为0x0,属于上一个chunk的用户空间;

Size of chunk为0x991,bit[0]=0x1说明上一个chunk被分配使用,0x990是该chunk的大小,加上nextchunk的Size of previous chunk域4个字节,总共0x994,刚好比用户申请的0x98c多出8个字节;

Nextchunk的Size of chunk域为0x607f,0x607e为nextchunk的大小,bit[0]=0x1表明上一个chunk被分配使用。

3. 分箱管理

bin的英文含义是”箱柜“,当我们谈到bin,是指某个双向链表的头节点,该链表的成员节点存放着某一特定范围size的空闲chunk。通过size,我们可以快速的定位bin index,然后遍历其指向的链表,寻找合适的chunk进行分配,或者将释放的chunk插入到链表中合适的地方。

6

程序定义了一个全局静态数组av_[]存放每种bin的头节点,

typedef struct malloc_chunk* mbinptr;

static mbinptr av_[128 * 2 + 2]

数组类型mbinptr是一个指针,大小为4个字节,数组大小为(128×2+2)*4 = 1032字节,

这就引出一个问题,既然存放头节点,节点类型为malloc_chunk,一个节点就需要16 bytes,总共有128个头节点,理应需要128*16 = 2048字节才对,现在av_[ ]1032字节,是如何放下所有的头节点信息的呢?对于头节点而言,有效的是fdbk,成员prev_sizesize并没有用到,既然没用,那空间能否节约下来呢?可以的,看看dlmalloc是如何做到的。

#define bin_at(i)      ((mbinptr)((char*)&(av_[2*(i) + 2]) - 2*4))

以分配16 bytes为例,其箱号为 16 / 8 = 2,于是,bin_at(2)-->((mbinptr)((char*)&(av_[6]) - 2*4)),最终bin_at(2)将地址 &av_[4] 强行转换为mbinptr指针,用这个指针访问fdbk,得到的其实是av_[6]av_[7]中存放的内容。

我们看看dlmalloc中两个特殊的分箱top和last_remainder

#define top      (bin_at(0)->fd)       /* The topmost chunk */

#define last_remainder (bin_at(1))       /* remainder from last split */

top最初被称为wilderness chunk,指向dlmalloc可用内存的最高端的边界chunk,因为在边界处,top是唯一一个可以任意扩展的块(在Unix上可以通过库函数sbrk( ))。top比较特殊,它不受任何分箱管理,当其它分箱没有可用的chunk时才会用到top。在dlmalloc初始化刚完成时,整个受dlmalloc管理的内存就是一个chunktop即指向这个chunk

last_remainder总是指向最近被分割chunk的剩下那一部分。如果malloc( )在分配时没找到“精确匹配”的块,则优先去查看last_remainder是否够用。从局部性原理来讲,连续申请分配内存的代码总是趋向于有共同的生命周期,它们释放的chunk也就有更大的机会合并成一个大的chunk

 

了解完toplast_remainder,我们继续往下看。last_remainder的箱号为1bin_at(1)将地址 &av_[2] 强行转换为mbinptr指针,访问fdbk,得到的其实是av_[4]av_[5]中存放的内容,即

dlmalloc是目前一个十分流行的内存分配器,其由Doug Lea(主页为http://gee.cs.oswego.edu/)从1987年开始编写,到目前为止,最新版本为2.8.3(可以从ftp://g.oswego.edu/pub/misc/malloc.c获取),由于其高效率等特点被广泛的使用(比如一些linux系统等用的就是dlmalloc或其变形,比如ptmalloc,主页为http://www.malloc.de/en/index.html)和研究(各位可以搜索关键字“GCspy”)。 dlmalloc的实现只有一个源文件(还有一个头文件),大概5000行,其内注释占了大量篇幅,由于有这么多注释存在的情况下,表面上看上去很容易懂,的确如此,在不追求细节的情况,对其大致思想的确很容易了解(没错,就只是了解而已),但是dlmalloc作为一个高品质的佳作,实现上使用了非常多的技巧,在实现细节上不花费一定的精力是没有办法深入理解其为什么这么做,这么做的好处在哪,只有当真正读懂后回味起来才发现它是如此美妙。 lenky0401个人博客将陆续推出对dlmalloc的解析(针对Doug Lea Malloc的最新版Version 2.8.3,未做说明的情况下以32位平台,8字节对齐作为假定平台环境设置考虑),由于个人水平有限,因此也不能完全保证对dlmalloc的所有理解都准备无误, 但是所有内容均出自个人的理解而并非存心妄自揣测来愚人耳目,所以如果读者发现其中有什么错误,请勿见怪,如果可以则请来信告之,并欢迎来信讨论(lenky0401@163.com)。 这一系列文章是lenky0401在看完dlmalloc的大部分代码后的再总结,不能保证对dlmalloc的整体完全把握,贴出这些只是希望可以提前收到对此有研究的网友的指点,以便在最后对这一系列文章整理而形成的PDF文档中错误能少一些。至于对于现在贴出来的内容中包含的错误给大家造成的不便提前说声抱歉。:) 描述的内容不会包含dlmalloc全部代码,但会将这其中涉及到的一些技巧尽量讲出,我相信对dlmalloc源代码不感兴趣的朋友也可以学到这些独立的技巧而使用在自己的编程实践中。:) 最后,转载请保留本博客地址连接[http://lenky0401.cublog.cn],谢谢。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值