2. 标记结构
本章节将介绍基本的内存标记结构,包括chunk, tree chunk, sbin, tbin, segment, mstate等.这些重要的机构组成了dlmalloc分配算法的基础.算法
2.1 chunk
chunk是dlmalloc中最基本的一种结构,它表明了一块通过划分后被管理的内存单元. dlmalloc全部对内存的操做几乎都聚焦在chunk上.须要注意的是, chunk虽然看似基础,但不表明它能管理的内存小.事实上chunk被划分为两种类型,小于256字节的称为small chunk,而大于等于256字节的被称为tree chunk.缓存
2.1.1 chunk布局
在dlmalloc的源码中有一幅用字符画成的示意图,套用Doug Lea本人的话说 “is misleading but accurate and necessary”.初次接触这种结构划分会以为既古怪又别扭. 所以,接下来的几节内容须要认真体会,让大脑适应这种错位的思考.数据结构
对于一个已分配chunk, 它在内存中多是这个样子,多线程

上图为了便于理解特地使用了两种颜色来描述,蓝色区域的部分表明当前的已分配chunk,而黄色区域表明上一个和下一个chunk的部分区域.app
解释一下其中的含义, 从蓝色区域开始看, 在已分配chunk的开始记录当前chunk的size信息,同时在最后两个bit位分别记录当前chunk和前一个chunk是否被使用,简称为C和P两个bit位.之因此能够作到这一点,是由于在dlmalloc中全部的chunk size至少对齐到大于8并以2为底的指数边界上.这样,至少最后3位都是0,所以这些多余的bit位就能够拿来记录更多的信息. size信息后面是payload部分,显然,当前chunk的使用信息也会记录在下一个chunk开始的P位上.less
而对于一个空闲chunk, 其内存布局应该是这样的,函数

与以前的一致, 蓝色区域表明当前空闲chunk, 黄色区域表明相邻的先后chunk.能够看到空闲chunk与以前的区别在于开始的size后多了next和prev指针,它们把具备相同大小的空闲chunk连接到一块儿.另外一个区别是,在空闲chunk最后一样记录该chunk的大小信息.那么为何一样的信息要记录两次呢?在下一个小节中会解释这个问题.布局
另外, 还有一点须要说明的是,空闲chunk的相邻chunk必然是已分配的chunk.由于若是存在相邻两个chunk都是空闲的,那么dlmalloc会把它们合并为一个更大的chunk.post
2.1.2 边界标记法(Boundary Tag)
上一节中介绍的chunk布局其实只是理论上的东西,而实现它使用了被称为边界标记法 (boundary tag)的技术.据做者说,此方法最先是大神Knuth提出的.实现边界标记法使用了名为malloc_chunk的结构体,它的定义以下,测试

该结构体(之后简称mchunk)由四个field组成.最开始是prev_foot,记录了上一个邻接chunk的最后4个字节.接下来是head,记录当前chunk的size以及C和P位.最后两个是fd, bk指针,只对空闲chunk起做用,用于连接相同大小的空闲chunk.
为了不读者感到困惑, 在上一节的图中并无画出对应的mchunk, 如今补充完整以下,

上图用不一样颜色画了几个连续交错的chunk,并故意在中间断开,标注出mchunk以及chunk的实际范围,由此获得以下的结论,
1. mchunk的做用是将连续内存划分为一段段小的区块, 并在内部保留这些区块的信息.
2. mchunk最后的fd, bk是能够复用的,对于空闲chunk它们表明连接指针,而已使用chunk中这两个field实际上存放的是payload数据.
3. mchunk与chunk不是一个等同的概念.这一点是容易让人混淆和困惑的. mchunk只是一个边界信息,它实际上横跨了两个相邻chunk.尽管通常认为mchunk能够指代当前的chunk,由于你能够从它推算出想要的地址.但从逻辑上,它既不表明当前chunk也不表明prev chunk.
4. prev_foot字段一样是一个可复用的字段. 通常状况下它有三种含义,若是前一个相邻chunk是空闲状态,它记录该chunk的大小(图中mchunk C).其目的就是你能够很方便的从当前chunk得到上一个相邻chunk的首地址,从而快速对它们进行合并.这也就是上一小节介绍的空闲chunk会在head和footer存在两个chunk size的缘由,位于footer的size实际上保存在下一个mchunk中.若是前一个相邻chunk处于in used状态,那么该字段可能有两种状况.一种状况是FOOTERS宏为1,这时它保存一个交叉检查值,用于在free()的时候进行校验.若是该宏为0,则它没有多余的含义,单纯只是前面chunk的payload数据.
5. 最后还有一点须要注意的是, 尽管前面提到全部chunk size都是对齐到至少等于8的2的指数.但这不意味着mchunk就对齐到这一边界上,由于如前所述mchunk和chunk是两码事.我本人最先在看这部分代码时曾天真的觉得mchunk也是对齐的,结果到后面竟然彻底看不懂,后来才发现这家伙根本没有对齐.
到目前为止, 已经了解了dlmalloc的边界标记法是如何实现的.可能有人会对此感到不觉得然,做者为何要把代码写成这个样子呢?其实,要理解这个意图请换位思考一下,若是实现一个相似的东西,你会怎么写呢?人们很快会发现,不按照这种方式设计,想要达到一样目的几乎是很困难的.由于一个chunk在头和尾存在head和footer(也可能不存在footer),中间是一片长度没法肯定的区域.假设按照正常从头至尾的顺序写,这个结构体中间填充多长的内容是无法肯定的.所以, Doug Lea巧妙的把除了payload以外的overhead部分合并在一块儿,做为boundary tag来分割整个内存区域,既能作到很方便的计算,又准确地表达了chunk的结构.
2.1.3 chunk操做
针对mchunk的设计,源码中定义了大量的宏来对其进行操做.因为数量不少加上嵌套,对于阅读源码会形成不小的障碍,所以这里会对一些经常使用宏进行梳理和简单讲解.

定义size_t的大小和bit位数.注意, dlmalloc中对size_t规定为必须为无符号类型,且与指针类型(void*)等宽度.所以若是目标平台还在使用有符号的size_t,只能使用较旧版本的dlmalloc.

常量定义, 这里使用size_t做为强制类型是为了不在某些平台上出现错误.


对齐边界和对齐掩码, 边界默认定义为两倍指针宽度, 固然还能够修改的更大,但必须以2为底的指数.有关对齐掩码相关知识请参考1.4.1节.

判断给定地址是否对齐在边界上,原理一样在1.4.1节中解释,再也不赘述.

计算给定地址须要对齐的偏移量,请参考1.4.3节中的介绍.

chunk size(其实是mchunk size,后面再也不特地说明)以及overhead大小.注意,若是FOOTERS等于1, overhead将多出4个字节,其实就是在chunk最后放置交叉检查项多出的负载.另外,无论是否有FOOTERS, chunk size自己是不变的.

最小chunk大小. dlmalloc中即便调用malloc(0)也是会分配内存的.这种状况下彻底不考虑用户payload了,仅仅将chunk size作了对齐.该值的意义在于,这是dlmalloc中除了mmap chunk之外的最坏负载损耗.

这是两个最经常使用的宏之一, 在chunk和用户地址以前切换.

该宏的意思是, 将指定chunk的用户地址对齐,返回对齐后的新chunk地址.理解很容易,从chunk获取用户地址,计算对齐偏移,再移动chunk指针.从这里能够看到, chunk中对齐的并不是是mchunk指针,而是用户地址.

将用户请求的内存值转化成实际的内部大小.该宏也是经常使用宏之一,请求大小先加上overhead(根据FOOTERS而有所不一样),再对齐到边界上.

该宏是对上面一系列宏的封装, 若是用户请求小于最小请求, 直接使用最小chunk大小,不然认为正常,转化成内部可用大小.

上面这一组定义了C位和P位,其中FLAG4_BIT并无实际用途,只是做为将来的扩展.

测试C和P位,分别用掩码作与运算.着重说一下最后两个判断. is_inuse是用C和P的掩码分别测试,而后与P的掩码对比.有人会问这个直接用前面的cinuse不就行了吗?由于存在一种特殊状况,就是被mmap出来的chunk是不见得有邻接chunk的,因此其C和P都被置0. is_inuse就是为了判断包括mmap chunk在内的全部类型chunk的使用状况.理解了这个, is_mmapped就很容易理解了.

计算指定chunk的大小,清除最后3bit后获得的就是size.

对P位的操做.

这是两个计算偏移量的宏, 返回结果被认为是mchunk, 通常用做chunk切割.

这两个也是很是重要的宏. 分别用来计算前一个或后一个邻接chunk的首地址. next_chunk先获取当前chunk的size做为偏移量,再移动chunk指针. prev_chunk则是直接加上前一个chunk的footer做为偏移量.须要注意的是, prev_chunk仅仅适用于前一个邻接chunk为空闲块的状况.如前所述,非空闲chunk这里放置的多是用户数据或者交叉检查项.实际上这个宏就是用于free时试图与前一个free chunk合并时的判断.

获取下一个chunk的P位,应该与当前chunk的C是一致的.该宏通常用做交叉检查项.

获取和设置当前chunk的footer,由于footer被放在下一个邻接chunk的prev_foot中,所以须要加上size做为偏移量.

该宏是个复合操做, 用于对free chunk进行设置.由于free chunk在head和footer都保存其size,因此首先将其head中写入size.又由于free chunk的P必定是1(不然会与前面合并),所以还须要与P掩码进行一次位与.接着,利用前面的set_foot宏在footer中一样写入size.

这个宏与上面的区别在于多了一个next chunk的P位维护.由于是针对free chunk, next chunk的P须要置0.
2.1.4 tree chunk
以前介绍的chunk其实都属于小型chunk,而较大型的chunk是经过名为malloc_tree_chunk的结构体来描述的.

同mchunk相似tree chunk(之后简称tchunk)开始的field是如出一辙的,这就保证了两种类型在基础结构上具备较好的兼容性,下降了代码编写难度. tchunk与前者的区别在于管理上使用不一样的结构. mchunk是经过双链表组织起来的,而tchunk使用了一种树形结构,在介绍分箱时会详细说明.所以tchunk中child分别表明了左右子树节点, parent表明父节点, index表明该chunk所在分箱的编号.而fd, bk与mchunk一致,都是用于连接相同size的chunk.另外,须要说明的是,上述结构体字段一样只适用于free chunk,在used chunk上它们都是可复用的
=======================================================================================
咱们写过不少C程序了,常常会分配内存。记得刚学C语言时老师说过,能够向两个地方申请内存:一个是栈、一个是堆。小块内存向栈申请,函数调用结束后程序会自动释放内存。大块内存向堆申请,记得必定要本身释放,不然会形成内存泄漏。向堆申请内存直接调用malloc()就能够了,参数是你申请的内存量。释放内存时直接调用free()就能够了,参数是内存块指针。
看似平静的海面,海底则波涛汹涌。当时尚未学操做系统原理,更没有读过Linux内核代码。如今仔细想一想才发现申请动态内存是一件多么麻烦的事情。动态内存管理涉及到两个层面的问题:内核层面和用户层面。系统中的内存如何管理这是内核考虑的事情,总不能让应用程序随便使用系统中的内存吧。内核向应用程序提供了接口(为此Linux提供了两个系统调用brk和mmap),当应用程序须要申请内存时向内核提出请求,内核查找并分配一块可用内存供应用程序使用。这部份内容属于内核范畴,不属于C基础库,所以不深刻说了。那么用户层面作什么呢?用户层面须要合理管理内存申请和释放请求。好比:brk()能够扩充或收缩堆的大小,你总不能每分配一次内存就调用一次brk()吧?释放内存时更麻烦,你必须保证内存块的释放顺序。好比先申请了内存块a,而后申请了内存块b,而后释放a(b仍然在使用),若是释放a时调用了brk()就会出问题。你不能在使用b的同时释放a。
好在出现了一个叫作“内存分配器”的东西,内存分配器接管了应用程序申请内存和释放内存的请求,应用程序不再须要直接调用brk()和mmap()了,而是向内存分配器提交申请。有了内存分配器,咱们只须要记住malloc()和free()两个接口函数就能够了,其余繁杂事情所有交给内存分配器负责了。申请内存时,内存分配器会一次向内核申请大量内存,而后分批交给应用程序,从而提升了效率。释放内存时,应用程序也是将内存释放给内存分配器,内存分配器在合适的时候再将内存释放会内核。
dlmalloc就是一种内存分配器,由Doug Lea在1987年开发完成,这是Android系统中使用的内存分配器。而Linux系统中采用的是ptmalloc,ptmalloc在dlmalloc的基础上进行了改进,以更好适应多线程。dlmalloc采用两种方式申请内存,若是应用程序单次申请的内存量小于256kb,dlmalloc调用brk()扩展进程堆空间,可是dlmalloc向内核申请的内存量大于应用程序申请的内存量,申请到内存后dlmalloc将内存分红两块,一块返回给应用程序,另外一块做为空闲内存先保留起来。下次应用程序申请内存时dlmalloc就不须要向内核申请内存了,从而加快内存分配效率。当应用程序调用free()释放内存时,若是内存块小于256kb,dlmalloc并不立刻将内存块释放回内存,而是将内存块标记为空闲状态。这么作的缘由有两个:一是内存块不必定能立刻释放会内核(好比内存块不是位于堆顶端),二是供应用程序下次申请内存使用(这是主要缘由)。当dlmalloc中空闲内存量达到必定值时dlmalloc才将空闲内存释放会内核。若是应用程序申请的内存大于256kb,dlmalloc调用mmap()向内核申请一块内存,返回返还给应用程序使用。若是应用程序释放的内存大于256kb,dlmalloc立刻调用munmap()释放内存。dlmalloc不会缓存大于256kb的内存块,由于这样的内存块太大了,最好不要长期占用这么大的内存资源。
dlmalloc中,申请到的内存被分割成若干个内存块,dlmalloc采用两种不一样的数据结构表示这些内存块。小内存块保存在链表中,用struct malloc_chunk表示;大内存块保存在树形结构中,用struct malloc_tree_chunk表示。struct malloc_chunk结构以下:
[cpp] view plaincopy