Dlmalloc266源码分析.docx

上传人:b****6 文档编号:7043679 上传时间:2023-01-16 格式:DOCX 页数:27 大小:304.35KB
下载 相关 举报
Dlmalloc266源码分析.docx_第1页
第1页 / 共27页
Dlmalloc266源码分析.docx_第2页
第2页 / 共27页
Dlmalloc266源码分析.docx_第3页
第3页 / 共27页
Dlmalloc266源码分析.docx_第4页
第4页 / 共27页
Dlmalloc266源码分析.docx_第5页
第5页 / 共27页
点击查看更多>>
下载资源
资源描述

Dlmalloc266源码分析.docx

《Dlmalloc266源码分析.docx》由会员分享,可在线阅读,更多相关《Dlmalloc266源码分析.docx(27页珍藏版)》请在冰豆网上搜索。

Dlmalloc266源码分析.docx

Dlmalloc266源码分析

 

Dlmalloc-2.6.6源码分析

1.DougLeamalloc简介

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

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

dlmalloc的由来,从DougLea自己写的文章看,似乎是这样的:

1986年到1991年DougLea是libg++(即GNUC++library)的主要作者,当时他写了大量有着动态分配内存的C++程序,结果发现程序跑得比预期慢,内存消耗也比预想的要大很多,追究下去,发现是所在系统内存分配器的问题。

于是开始用C++为新类写一些特殊用途的分配器,但他很快意识到这并非一个好的策略,应该提供一个在C++和C环境下都能运行得很好的通用内存分配器,于是dlmalloc诞生了。

在之后的日子里,DougLea和一些志愿者一直都在不断的维护优化这个内存分配器。

dlmalloc之所以能被广泛应用,与其高标准的追求和不断的精益求精应该有着不可分割的关系。

另外,值得一提的是,DougLea是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看起来像这样:

图1

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

structmalloc_chunk

{

INTERNAL_SIZE_Tprev_size;/*Sizeofpreviouschunk(iffree).*/

INTERNAL_SIZE_Tsize;/*Sizeinbytes,includingoverhead.*/

structmalloc_chunk*fd;/*doublelinks--usedonlyiffree.*/

structmalloc_chunk*bk;

};

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

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

#definePREV_INUSE0x1

bit[0]:

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

#defineIS_MMAPPED0x2

bit[1]:

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

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

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

图2

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

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

图3

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

#definerequest2size(req)\

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

(long)(MINSIZE+MALLOC_ALIGN_MASK))?

MINSIZE:

\

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

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

#definerequest2size(req)\

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

(long)(0x10+0x7))?

((0x10+0x7)&~(0x7)):

\

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

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

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

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

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

说到这,或许你已经发现一个问题了,如果用户申请20个字节的空间,姑且称之为A,系统会分配24字节,而chunk的heap控制信息占了8个字节,那留给用户使用的只剩下18个字节了。

如此看来,岂不是会覆盖下一个chunk(姑且称之为B)的“Sizeofpreviouschunk”区域?

这个问题问得好,学而思,而后得解,我们才能更加充分认识到这个设计的思想。

为解答这个问题,我们先了解什么时候需要定位前一个chunk?

只有在释放一块空间,判断前一个chunk是否空闲时才需要该动作。

换而言之,当一个chunk被分配使用时,它根本不需要下一个chunk被释放时来合并它,既然不需要,就利用起来吧。

于是,B的“Sizeofpreviouschunk”区域也被纳入到A的用户空间中了。

图4

从这一点讲,上图中的“Sizeofpreviouschunk,ifallocated”的表述是不对的,应该是“Sizeofpreviouschunk,iffreed”。

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

图5

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

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

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

3.分箱管理

bin的英文含义是”箱柜“,当我们谈到bin,是指某个双向链表的头节点,该链表的成员节点存放着某一特定范围size的空闲chunk。

通过size,我们可以快速的定位binindex,然后遍历其指向的链表,寻找合适的chunk进行分配,或者将释放的chunk插入到链表中合适的地方。

图6

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

typedefstructmalloc_chunk*mbinptr;

staticmbinptrav_[128*2+2]

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

这就引出一个问题,既然存放头节点,节点类型为malloc_chunk,一个节点就需要16bytes,总共有128个头节点,理应需要128*16=2048字节才对,现在av_[]才1032字节,是如何放下所有的头节点信息的呢?

对于头节点而言,有效的是fd和bk,成员prev_size和size并没有用到,既然没用,那空间能否节约下来呢?

可以的,看看dlmalloc是如何做到的。

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

以分配16bytes为例,其箱号为16/8=2,于是,bin_at

(2)-->((mbinptr)((char*)&(av_[6])-2*4)),最终bin_at

(2)将地址&av_[4]强行转换为mbinptr指针,用这个指针访问fd和bk,得到的其实是av_[6]和av_[7]中存放的内容。

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

#definetop(bin_at(0)->fd)/*Thetopmostchunk*/

#definelast_remainder(bin_at

(1))/*remainderfromlastsplit*/

top最初被称为wildernesschunk,指向dlmalloc可用内存的最高端的边界chunk,因为在边界处,top是唯一一个可以任意扩展的块(在Unix上可以通过库函数sbrk())。

top比较特殊,它不受任何分箱管理,当其它分箱没有可用的chunk时才会用到top。

在dlmalloc初始化刚完成时,整个受dlmalloc管理的内存就是一个chunk,top即指向这个chunk。

last_remainder总是指向最近被分割chunk的剩下那一部分。

如果malloc()在分配时没找到“精确匹配”的块,则优先去查看last_remainder是否够用。

从局部性原理来讲,连续申请分配内存的代码总是趋向于有共同的生命周期,它们释放的chunk也就有更大的机会合并成一个大的chunk。

了解完top和last_remainder,我们继续往下看。

last_remainder的箱号为1,bin_at

(1)将地址&av_[2]强行转换为mbinptr指针,访问fd和bk,得到的其实是av_[4]和av_[5]中存放的内容,即bin_at

(2)的prev_size域和bin_at

(1)的fd域重叠,bin_at

(2)的size域和bin_at

(1)的bk域重叠,看起来像这样(方格内的数字以4个字节为单位):

图7

同理,bin_at

(1)的prev_size域和bin_at(0)的fd域重叠,bin_at

(1)的size域和bin_at(0)的bk域重叠在一起。

通过这种叠加使用,dlmalloc使得本该占据2048字节空间的需求变成了1032字节。

这里体现了DougLea的一个设计宗旨:

MinimizingSpace,即用于heap控制的内存要最小化。

原话是这样说的,

Theallocatorshouldnotwastespace:

itshouldobtainaslittlememoryfromthesystemaspossible,andshoudmaintainmemoryinwaysthatminimizefragmentation--"holes"incontiguouschunksofmemorythatarenotusedbytheprogram.

好吧,你说,必须得承认,dlmalloc确实很省空间,但是从上面这个图看来,av_[0]和av_[1]似乎没有被用到,浪费好像不符合MinimizingSpace的原则哦。

当然不会,dlmalloc为达到快速检索分箱的目的,使用了一个小技巧,#definebinblocks(bin_at(0)->size)/*bitvectorofnonemptyblocks*/

即用binblocks建立了所有分箱的一个bitmap,binblocks的bit来表示连续的4个相邻的bin是否为空,只要有一个不为空,其对应的bit置1。

binblocks实际上是av_[1],一个32位数据类型,32×4=128,正好对应128个bins。

扫描时先判断binblocks的相应位,只有当bit不为空时才会真正的去扫描对应的bin。

每一个bin的用途描述如下:

#definetop(bin_at(0)->fd)/*Thetopmostchunk*/

#definelast_remainder(bin_at

(1))/*remainderfromlastsplit*/

由上宏定义可知,

top(topmostchunk)-->0

last_remainder-->1

对小于512bytes的内存申请,箱号=size/8,分箱如下:

0x0~0x1ff-->2~63;

大于等于512bytes的分箱如下(以下数据用程序打印出来):

0x200~0x23f-->64

0x240~0x27f-->65

0x280~0x2bf-->66

0x2c0~0x2ff-->67

0x300~0x33f-->68

0x340~0x37f-->69

0x380~0x3bf-->70

0x3c0~0x3ff-->71

0x400~0x43f-->72

0x440~0x47f-->73

0x480~0x4bf-->74

0x4c0~0x4ff-->75

0x500~0x53f-->76

0x540~0x57f-->77

0x580~0x5bf-->78

0x5c0~0x5ff-->79

0x600~0x63f-->80

0x640~0x67f-->81

0x680~0x6bf-->82

0x6c0~0x6ff-->83

0x700~0x73f-->84

0x740~0x77f-->85

0x780~0x7bf-->86

0x7c0~0x7ff-->87

0x800~0x83f-->88

0x840~0x87f-->89

0x880~0x8bf-->90

0x8c0~0x8ff-->91

0x900~0x93f-->92

0x940~0x97f-->93

0x980~0x9bf-->94

0x9c0~0x9ff-->95

0xa00~0xbff-->96

0xc00~0xdff-->97

0xe00~0xfff-->98

0x1000~0x11ff-->99

0x1200~0x13ff-->100

0x1400~0x15ff-->101

0x1600~0x17ff-->102

0x1800~0x19ff-->103

0x1a00~0x1bff-->104

0x1c00~0x1dff-->105

0x1e00~0x1fff-->106

0x2000~0x21ff-->107

0x2200~0x23ff-->108

0x2400~0x25ff-->109

0x2600~0x27ff-->110

0x2800~0x29ff-->111

0x2a00~0x2fff-->112

0x3000~0x3fff-->113

0x4000~0x4fff-->114

0x5000~0x5fff-->115

0x6000~0x6fff-->116

0x7000~0x7fff-->117

0x8000~0x8fff-->118

0x9000~0x9fff-->119

0xa000~0xffff-->120

0x10000~0x17fff-->121

0x18000~0x1ffff-->122

0x20000~0x27fff-->123

0x28000~0x3ffff-->124

0x40000~0x7ffff-->125

size>=0x80000-->126

dlmalloc的实现使用两个宏来完成对于bin链表的插入和删除操作。

宏定义frontlink(P,S,IDX,BK,FD)将某个chunk放入对应的bin链表,定义如下:

#definefrontlink(P,S,IDX,BK,FD)\

{\

if(S

{\

IDX=smallbin_index(S);\

mark_binblock(IDX);\

BK=bin_at(IDX);\

FD=BK->fd;\

P->bk=BK;\

P->fd=FD;\

FD->bk=BK->fd=P;\

}\

else\

{\

IDX=bin_index(S);\

BK=bin_at(IDX);\

FD=BK->fd;\

if(FD==BK)mark_binblock(IDX);\

else\

{\

while(FD!

=BK&&Sfd;\

BK=FD->bk;\

}\

P->bk=BK;\

P->fd=FD;\

FD->bk=BK->fd=P;\

}\

}

对应的逻辑示意图如下:

图8

如果chunksize小于512,则很好挂载,先除以8找到箱号,然后插入到头节点和头节点fd域指向的第一个节点之间,因为所有的chunk大小都一样;

如果chunksize大于等于512,则稍微麻烦一点,沿着头节点fd指针开始寻找,或者碰到size相等或更大的chunk,则插在该chunk之前;如果需挂载chunk的size

在该bin中最大,则插在最后一个chunk和头节点之间。

这种最糟糕的情况会导致遍历完整个链表才能找到插入的地方,从执行效率来讲,并非最佳。

在dlmalloc2.8.3版本中,这一部分不再使用双向链表,而是使用二叉树来管理,在搜素上会更快速。

宏定义unlink(P,BK,FD)则将一个chunk从它所在的链表取走,类似于将一个节点从双向链表中解除。

其定义如下:

/*takeachunkoffalist*/

#defineunlink(P,BK,FD)\

{\

BK=P->bk;\

FD=P->fd;\

FD->bk=BK;\

BK->fd=FD;\

}

4.内存分配相关函数

本节主要对dlmalloc内存分配器的核心函数mALLOc()以及相关函数进行讲解,函数mALLOc用于服务应用程序的内存空间申请请求,因此也是我们平常使用得最多的两个函数(另外一个fREe())之一。

下面我们先来直接看源码并同时进行分析。

(下面给出的源码都已标号,标号为源文件malloc-2.6.6.c内的实际行号,未标号的行都为我给出的分析注解内容。

4.1函数mALLOc()

2110#if__STD_C

2111Void_t*mALLOc(size_tbytes)

2112#else

2113Void_t*mALLOc(bytes)size_tbytes;

2114#endif

2115{

2116mchunkptrvictim;/*inspected/selectedchunk*/

2117INTERNAL_SIZE_Tvictim_size;/*itssize*/

2118intidx;/*indexforbintraversal*/

2119mbinptrbin;/*associatedbin*/

2120mchunkptrremainder;/*remainderfromasplit*/

2121longremainder_size;/*itssize*/

2122intremainder_index;/*itsbinindex*/

2123unsignedlongblock;/*blocktraverserbit*/

2124intstartidx;/*firstbinofatraversedblock*/

2125mchunkptrfwd;/*misctempforlinking*/

2126mchunkptrbck;/*misctempforlinking*/

2127mbinptrq;

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 自然科学 > 化学

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1