内存分配器dlmalloc 2.8.3源码浅析

原创
2016-06-07 15:49:39 1317浏览

通过这种分法,任一号箱的字节范围被对应在一颗树上,在查找某一大小的空闲 chunk 时则按照这种规律来进行“快速”搜索,比如应用程序 malloc (278),则由于278 在 [256,320)范围内,因此先进入树 T1 ,接着由于 278 在 [256,288)范围内,因此由进入 T3 ,接

通过这种分法,任一号箱的字节范围被对应在一颗树上,在查找某一大小的空闲chunk时则按照这种规律来进行“快速”搜索,比如应用程序malloc( 278 ),则由于278[256, 320)范围内,因此先进入树T1,接着由于278[256, 288)范围内,因此由进入T3,接着进入T8,……。

之所以说这种搜索是“快速”的,因为这里的范围划为很巧妙,我们来看一下各字节范围用二进制表示的情况:

根节点T的左子树T1 [256, 320)为: [1 0000 0000 1 00xx xxxx]

而根节点T的右子树T2 [320, 384)为: [1 01xx xxxx 1 01xx xxxx]

其中的x表示为1或者0,可以看到T1T2的管理范围很好区分,即通过判断第6bit位是否为1即可,为1表示在右子树T2范围内,为0表示在左子树T1范围内。

再来看看树T1的左子树T3和右子树T4情况:

T3管理[256, 288)为:[1 0000 0000 1 000x xxxx]

T4管理[288, 320)为:[1 0010 0000 1 001x xxxx]

可以看到T3T4的管理范围也很好区分,即通过判断第5bit位是否为1即可,为1表示在右子树T4范围内,为0表示在左子树T3范围内。

其它的类似……。因此,我们在查找某字节在哪个范围内的树上只需要顺序的比较各bit位就可以了,对比Trie树,我们可以认为dltree是关键码只有01Trie

4. 核心结构体malloc_state

在讲解核心结构体malloc_state之前,先来看看另外一个结构体segment。前面内容介绍的chunk块是dlmalloc内比较细粒度的管理结构,比它们更大的内存块被称之为段(segment),其结构体以及相关定义如下:

struct malloc_segment {

char* base; /* base address */

size_t size; /* allocated size */

struct malloc_segment* next; /* ptr to next segment */

flag_t sflags; /* mmap and extern flag */

};

#define is_mmapped_segment(S) ((S)->sflags & IS_MMAPPED_BIT)

#define is_extern_segment(S) ((S)->sflags & EXTERN_BIT)

typedef struct malloc_segment msegment;

typedef struct malloc_segment* msegmentptr;

英文注释很清晰,字段base表示该segment段的起始地址,size表示该segment段的大小,sflags是一个标记字段(两个标记,一个用于标记该segment段是否为通过mmap申请,一个用于标记该segment段是否已经分配),而字段next用于指向下一个segment段,从而形成单链表结构。记录该单链表表头的变量同样定义在结构体malloc_state内,如下:

msegment seg;

这是个结构体变量,而不是结构体指针变量,这一点需要注意。刚才已经提到多个segment段是通过next形成单链表结构来组织管理的,dlmalloc也没有对它们做其它特别的索引处理,因此查找某一个segment段需要在该单链表内线性扫描查找,不过大多数情况下,segment段应该是非常少的,所以并不会造成多大的性能损失。

dlmalloc中对malloc_chunk、malloc_tree_chunk、malloc_segment给出了一个统一的管理结构体,那就是前面反复提到的结构体malloc_state,其具体定义如下:

struct malloc_state {

binmap_t smallmap;

binmap_t treemap;

size_t dvsize;

size_t topsize;

char* least_addr;

mchunkptr dv;

mchunkptr top;

size_t trim_check;

size_t magic;

mchunkptr smallbins[(NSMALLBINS+1)*2];

tbinptr treebins[NTREEBINS];

size_t footprint;

size_t max_footprint;

flag_t mflags;

#if USE_LOCKS

MLOCK_T mutex; /* locate lock among fields that rarely change */

#endif /* USE_LOCKS */

msegment seg;

};

typedef struct malloc_state* mstate;

该结构体中的一些字段在前面已经详细分析过了,比如smallbins、treebins和seg。其它几个字段的作用,现在也一一说明如下:

smallmap是一个位图,用于标记对应的smallbins链表是否为空,1表示非空,0表示空。

treemap也是一个位图,用于标记对应的treebins树是否为空,1表示非空,0表示空。

dv和dvsize指向一个chunk块(dvsize记录该chunk块大小),该chunk块具有一个特点就是:它是从最近的一次被用来服务小于256字节内存申请的chunk块中分裂出来的chunk块。比较拗口,简单点说就是为了更有效的利用程序的局部性原理,即对于应用程序的一连续小块内存申请请求,dlmalloc总是尽量从同一个chunk块获取空闲内存来满足这些请求,因此分配给应用程序的内存都是在挨得比较近的地址空间内,从局部性原理可以知道,这种内存分配方式在一定程度上可以提高应用程序的性能。当然,这种分配方式只是尽量,如果有其它更好的chunk块选择(比如刚好有某个chunk大小就是应用程序申请的内存大小)则会选择其它chunk块来分配内存,这在后面具体代码的分析过程中会看到。

top 和topsize也是指向一个chunk块(topsize记录该chunk块大小),该chunk块比较特殊,它不被任何分箱管理(即既不位于smallbins,也不位于treebins。),它位于当前活跃segment段(即最近被用来分配空间服务应用程序内存请求的)的最顶端,在最顶端的好处就是它可以自由伸缩(通过库函数sbrk()),因此大小可变。在segment段中间的那些chunk块大小一般不可变,但是有两种情况会变动,一是分配出去一部分内存用于满足应用程序申请,此时chunk块变小;二是,当应用程序释放内存(free())时,dlmalloc将检查该释放内存是否可与前后空闲内存合并,此时就可能导致chunk块变大。

字段least_addr用来记录可获取的最小内存地址,直白点说就是相当于一个边界点,用于做越界安全检查。

字段trim_check用来记录一个临界值。我们知道应用程序free内存空间时,该释放的内存空间并没有直接返还给系统而是被送回dlmalloc中进行管理。当释放的内存空间越来越多时,dlmalloc中管理的空闲chunk块也会变得越来越大(即由于多个连续空闲块合并的结果),对于一个segment段中间的空闲chunk块没有办法释放给系统(因为释放中间的空闲chunk块会将segment段切断),但是对于顶端(top)的chunk块则可以自由伸缩的,缩小顶端的chunk也就是将空闲内存真正的返还给系统。那什么时候需要缩小顶端的chunk块呢?字段trim_check就是记录的这个临界值。当顶端的chunk块大小大于字段trim_check记录的值时就要进行缩减操作了,具体情况在后面源码分析时再看。

其它几个字段不是我们主要关注的内容且比较简单,比如字段magic用于做确认检查;字段mflags用于标记一些属性,比如启用mmap、禁用连续分配,……;字段footprint记录从系统获得的字节数目;字段max_footprint记录从系统获得的最大字节数目;字段mutex用于多线程互斥锁等等,在此就不做过多介绍了。

dlmalloc定义了一个结构体malloc_state的全局变量,相关代码如下:

static struct malloc_state _gm_;

#define gm (&_gm_)

#define is_global(M) ((M) == &_gm_)

#define is_initialized(M) ((M)->top != 0)

变量_gm_是结构体malloc_state的静态全局变量,因此当使用dlmalloc的应用程序被加载运行时,变量_gm_自动初始化为全0,这对于dlmalloc是十分重要的(例如_gm_结构体字段seg也为全0);另外还有一个gm宏,其值被定义为_gm_的地址,因此可以当指针一样使用它。其它两个宏is_global和is_initialized很明朗,无需多说。

总结起来,可以看到dlmalloc利用静态全局变量_gm_管理着segment段,小块空闲chunk块、大块空闲chunk块以及其它相关信息,所有的内存分配和释放都是围绕着_gm_进行的。

5. 内存分配相关函数

本节主要对dlmalloc内存分配器的核心函数dlmalloc()以及相关函数进行讲解,函数dlmalloc用于服务应用程序的内存空间申请请求,因此也是我们平常使用得最多的两个函数(另外一个dlfree())之一。下面我们先来直接看源码并同时进行分析。(下面给出的源码都已标号,标号为源文件malloc-2.8.3.c内的实际行号,未标号的行都为我给出的分析注解内容。)

5.1 函数dlmalloc

4023. void* dlmalloc(size_t bytes) {

下面的英文注释给出了dlmalloc选择空闲内存来服务应用程序请求的分配策略:

对于小块内存请求(即小于256字节减去块头开销的内存申请请求):

1. 优先选择大小和请求大小刚好一致的空闲chunk块(即在请求大小对应的箱号内找到空闲chunk块),这么做的好处很明显,那就是这种分配不用分割chunk块就可以满足请求服务。如果大小刚好一致的空闲chunk块不存在(即在请求大小对应的箱号内为空)则选择大小比较一致的空闲chunk块,这种比较一致是指空闲chunk块大小和请求大小相差一个箱号(即相差8字节),如果在比请求大小对应的箱号大一的箱子内找到空闲chunk块也可以拿来分配以满足请求服务,否则进入下一步。

2. 如果dv chunkchunk块内分割出一部分内存以满足请求服务。否则的话,进入下一步。

3. 1步中查找空闲chunk块只在请求大小对应的和比其大一的两个箱号内查找,这一步就不做这些限制了,只要smallbins和treebins管理的chunk空闲链/树内有满足请求(即需要比请求字节数目大)的chunk空闲块存在(当然也是选择大小和请求字节数目最接近的chunk空闲块),则分割出一部分内存以满足请求服务,同时将剩余部分作为新的dv chunk。否则的话,进入下一步。

4. 如果top chunkchunk块内分割出一部分内存以满足请求服务。否则的话,进入下一步。

5. dlmalloc从系统获取内存空间。

对于大块内存请求(对于大块内存都是由treebins管理的):

1. treebins管理的空闲chunk块中找一个大小最接近请求字节数目的chunk块(假设为chunkA),如果chunkAdv chunk块更合适(即如果dv chunk块本身就太小而无法满足请求服务或者太大以至于chunkA的大小比dv chunk块大小更接近请求字节数目,我们应注意到dlmalloc总是选择从大小和当前请求字节数目最接近的chunk块中分配内存来服务请求,也就是所谓的最佳适应算法。这些常见内存分配算法比如首次适应算法、循环首次适应算法、最差适应算法等在操作系统原理课程上应该有讲解过。)则使用它。否则的话,进入下一步。

2. 如果dv chunk块足够大则使用它以满足请求服务。否则的话,进入下一步。

3. 如果top chunk块足够大则使用它以满足请求服务。否则的话,进入下一步。

4. 如果请求字节数目超过了预设的mmap调用界限则直接mmap一块内存来满足请求服务。否则的话,进入下一步。

5. dlmalloc从系统获取内存空间。

上面这些步骤的跳转使用了goto语法,以保证所有执行路径的最后都能够执行宏语句POSTACTION。

4024. /*

4025. Basic algorithm:

4026. If a small request (< 256 bytes minus per-chunk overhead):

4027. 1. If one exists, use a remainderless chunk in associated smallbin.

4028. (Remainderless means that there are too few excess bytes to

4029. represent as a chunk.)

4030. 2. If it is big enough, use the dv chunk, which is normally the

4031. chunk adjacent to the one used for the most recent small request.

4032. 3. If one exists, split the smallest available chunk in a bin,

4033. saving remainder in dv.

4034. 4. If it is big enough, use the top chunk.

4035. 5. If available, get memory from system and use it

4036. Otherwise, for a large request:

4037. 1. Find the smallest available binned chunk that fits, and use it

4038. if it is better fitting than dv chunk, splitting if necessary.

4039. 2. If better fitting than any binned chunk, use the dv chunk.

4040. 3. If it is big enough, use the top chunk.

4041. 4. If request size >= mmap threshold, try to directly mmap this chunk.

4042. 5. If available, get memory from system and use it

4043.

4044. The ugly goto's here ensure that postaction occurs along all paths.

4045. */

PREACTION是一个宏,作用和后面的POSTACTION宏相对,用于进行互斥锁定。我们知道“互斥锁是用来保证一段时间内只有一个线程在执行一段代码”,而下面的这段代码在多线程情形下恰好需要这一保证,因此有这一宏的存在。具体来看:

#define PREACTION(M) ((GLOBALLY_INITIALIZE() || use_lock(M))? ACQUIRE_LOCK(&(M)->mutex) : 0)

#define use_lock(M) ((M)->mflags & USE_LOCK_BIT)

#define ACQUIRE_LOCK(l) pthread_mutex_lock(l)

未初始化(GLOBALLY_INITIALIZE())或者的确需要锁定(use_lock(M))则调用函数pthread_mutex_lock()(Unix/Linux环境,Windows环境类似,以下同)对互斥锁上锁。

4046.

4047. if (!PREACTION(gm)) {

4048. void* mem;

4049. size_t nb;

MAX_SMALL_REQUEST被定义为小块内存请求的最大值:

#define MAX_SMALL_REQUEST (MAX_SMALL_SIZE - CHUNK_ALIGN_MASK - CHUNK_OVERHEAD)

根据对齐和其它(比如是否具有foot)等设置,该值具体数据稍有不同(比如为247或248等),但都比256小,在这里我们简单的认定它为256

4050. if (bytes <= MAX_SMALL_REQUEST) {

4051. bindex_t idx;

4052. binmap_t smallbits;

对请求内存数目进行对齐处理,另外如果请求内存数目小于最小请求值(MIN_REQUEST)则取最小值chunk块大小(16字节)。

4053. nb = (bytes < MIN_REQUEST)? MIN_CHUNK_SIZE : pad_request(bytes);

small_index是一个宏,用于根据请求字节数目计算对应大小的空闲chunk块所在的箱号:

#define SMALLBIN_SHIFT (3U)

#define small_index(s) ((s) >> SMALLBIN_SHIFT)

4054. idx = small_index(nb);

将箱号的位图标记移到最右边位。

4055. smallbits = gm->smallmap >> idx;

4056.

注意0x3U的二进制为0000 0011(只给出了低8位),因此如果它和smallbits进行位与为真,则smallbits的低2位有三种情况:

第一种情况:1 1

第二种情况:0 1

第三种情况:1 0

1表示该对应箱号内存在对应大小的空闲chunk块,前两种情况都表示存在大小刚好和请求大小一致的空闲chunk块;而第三种情况表示不存在大小刚好和请求大小一致的空闲chunk块,但是存在大小和请求大小只相差一个箱号的空闲chunk块。

4057. if ((smallbits & 0x3U) != 0) { /* Remainderless fit to a smallbin. */

4058. mchunkptr b, p;

在第三种情况(1 0)时需要将箱号(idx)加1,下面这行代码将这三种情况都无区别的处理了,即在第一二种情况时箱号(idx)并不会加1,很精巧的代码!

4059. idx += ~smallbits & 1; /* Uses next bin if idx empty */

找到对应箱号的chunk块链头节点:

#define smallbin_at(M, i) ((sbinptr)((char*)&((M)->smallbins[(i)<<1])))

4060. b = smallbin_at(gm, idx);

4061. p = b->fd;

4062. assert(chunksize(p) == small_index2size(idx));

将第一块空闲chunk块(假设为chunkA)从链表中拆出,用于分配。环形双向链表的头节点拆除就不多讲了。

4063. unlink_first_small_chunk(gm, b, p, idx);

本块chunkA被用于分配使用,因此需要修改边界标记:

宏small_index2size用于根据箱号计算对应箱内空闲chunk块字节数目:

#define small_index2size(i) ((i) << SMALLBIN_SHIFT)

#define SMALLBIN_SHIFT (3U)

宏set_inuse_and_pinuse根据设置(FOOTERS是否存在,即是否有footers),下面给出有footers的情况(没有footers的情况相对简单)下set_inuse_and_pinuse宏的定义:

#define set_inuse_and_pinuse(M,p,s)\

((p)->head = (s|PINUSE_BIT|CINUSE_BIT),\

(((mchunkptr)(((char*)(p)) + (s)))->head |= PINUSE_BIT),\

mark_inuse_foot(M,p,s))

该宏第二行用于标记本chunkA在使用中(即将被分配使用以满足请求)并且前一块也在使用中(这是显然的,因为有合并的操作,所以不会存在两个连续的空闲块)。第三行是修改邻接的下一chunk块的PINUSE_BIT标记,表明邻接的下一chunk块的前一邻接chunk块(即当前正要被分配使用的chunkA)在使用中。第四行修改footers标记。前面曾经说过prev_foot用于记录前一邻接chunk块的大小,那是在前一邻接chunk块空闲情况,如果前一邻接chunk块处于使用状况,prev_foot则用于记录它们所在的malloc_state信息,如下:

/* Set foot of inuse chunk to be xor of mstate and seed */

#define mark_inuse_foot(M,p,s)\

(((mchunkptr)((char*)(p) + (s)))->prev_foot = ((size_t)(M) ^ mparams.magic))

mparams.magic起安全检测作用。

4064. set_inuse_and_pinuse(gm, p, small_index2size(idx));

宏chunk2mem和mem2chunk用于在chunk块头地址和实际可用内存起始地址之间进行相互转换,这点根据chunk块结构体malloc_chunk和malloc_tree_chunk的定义很容易理解:

/* conversion from malloc headers to user pointers, and back */

#define chunk2mem(p) ((void*)((char*)(p) + TWO_SIZE_T_SIZES))

#define mem2chunk(mem) ((mchunkptr)((char*)(mem) - TWO_SIZE_T_SIZES))

4065. mem = chunk2mem(p);

debug编译时用于做安全检测。

4066. check_malloced_chunk(gm, mem, nb);

内存申请请求得以满足,跳到最后执行解除互斥锁操作,返回分配的内存起始地址。

就算不考虑块头、对齐等开销,malloc分配的内存也可能会比应用程序实际应分配内存多,即前面的第三种情况:1 0,这种情况时,dlmalloc分配的内存将多出8个字节。由于8个字节不足以组成一个chunk块,因此也就没有必要进行分割,而是全部分配给当前申请请求,下面还可以看到这种情况。

4067. goto postaction;

4068. }

4069.

dv chunk块不够大,因此在所有空闲chunk块中查找可以满足该请求的chunk块。

4070. else if (nb > gm->dvsize) {

在smallbins中存在可满足该请求的空闲chunk块。

4071. if (smallbits != 0) { /* Use chunk in next nonempty smallbin */

4072. mchunkptr b, p, r;

4073. size_t rsize;

4074. bindex_t i;

下面两个宏用于位操作,我们来看看:

idx2bit宏取第i位为1,其它位都为0的掩码,举个例子:idx2bit(3) 为 “0000 1000”(只显示8位,下同)。

#define idx2bit(i) ((binmap_t)(1) << (i))

left_bits宏取x中值为1的最小bit位的左边(不包括值为1的该最小bit) 所有位都为1,其它位都为0的掩码,举个例子:left_bits(6)为“1111 1100”,因为6的二进制位“0000 0110”,值为1的最小bit位为第1位,因此结果为第1位左边(不包括第1) 所有位都为1,其它位都为0的掩码。

#define left_bits(x) ((x<<1) | -(x<<1))

leftbits为所有可满足当前申请请求的箱号。

4075. binmap_t leftbits = (smallbits << idx) & left_bits(idx2bit(idx));

选择最佳适合的箱号对应的bitmap位码,即获取值为1的最小bit位:x中值为1的最小bit位为1,其它位都为0的掩码。

#define least_bit(x) ((x) & -(x))

4076. binmap_t leastbit = least_bit(leftbits);

compute_bit2idx 是一个宏,它使得i leastbit的二进制中位为1的最小位置索引,从0开始。举个例子:

leastbit0000 0100,则compute_bit2idx(leastbit, i)的结果是使得i的值为2

因此作用是根据bitmap位码计算对应的箱号。

4077. compute_bit2idx(leastbit, i);

获取箱号对应的链表头节点。

4078. b = smallbin_at(gm, i);

4079. p = b->fd;

4080. assert(chunksize(p) == small_index2size(i));

拆出链表的第一个空闲chunk块以进行内存分配。

4081. unlink_first_small_chunk(gm, b, p, i);

计算分割该chunk块(用于服务内存申请请求)后,该chunk块剩余的字节数。

4082. rsize = small_index2size(i) - nb;

4083. /* Fit here cannot be remainderless if 4byte sizes */

剩余的字节数太小无法组建一个新的空闲块,因此直接全部分配。

这里的判断利用了短路运算符的特性,我们应注意到当前待分配的chunk块大小和申请请求字节大小至少相差两个箱号的字节数目(即剩余字节数至少有16字节),当SIZE_T_SIZE == 4时,是不可能出现rsize < MIN_CHUNK_SIZE为真的情况的。换句话说,只有当SIZE_T_SIZE!=4的情况下,rsize < MIN_CHUNK_SIZE才有可能为真。至于为什么,可由MIN_CHUNK_SIZE的具体定义找到原因:

#define MIN_CHUNK_SIZE\

((MCHUNK_SIZE + CHUNK_ALIGN_MASK) & ~CHUNK_ALIGN_MASK)

#define MCHUNK_SIZE (sizeof(mchunk))

当SIZE_T_SIZE == 4时(对齐832环境),MIN_CHUNK_SIZE为16,自然不会比剩余字节数rsize大。其它对齐设置和环境类似推理。

4084. if (SIZE_T_SIZE != 4 && rsize < MIN_CHUNK_SIZE)

4085. set_inuse_and_pinuse(gm, p, small_index2size(i));

4086. else {

分割并将剩余字节组建一个新的空闲chunk块。

#define set_size_and_pinuse_of_inuse_chunk(M, p, s)\

((p)->head = (s|PINUSE_BIT|CINUSE_BIT),\

mark_inuse_foot(M, p, s))

#define mark_inuse_foot(M,p,s)\

(((mchunkptr)((char*)(p) + (s)))->prev_foot = ((size_t)(M) ^ mparams.magic))

4087. set_size_and_pinuse_of_inuse_chunk(gm, p, nb);

新组建空闲chunk块结构起始地址

#define chunk_plus_offset(p, s) ((mchunkptr)(((char*)(p)) + (s)))

4088. r = chunk_plus_offset(p, nb);

设置新组建空闲chunk块的标记,包括本块大小、本块的前一邻接块在使用中标识以及设置prev_foot数据为前一邻接块大小。

#define set_size_and_pinuse_of_free_chunk(p, s)\

((p)->head = (s|PINUSE_BIT), set_foot(p, s))

#define set_foot(p, s) (((mchunkptr)((char*)(p) + (s)))->prev_foot = (s))

4089. set_size_and_pinuse_of_free_chunk(r, rsize);

将新组建空闲chunk块替换原来的dv chunk块:

#define replace_dv(M, P, S) {\

size_t DVS = M->dvsize;\

if (DVS != 0) {\

mchunkptr DV = M->dv;\

assert(is_small(DVS));\

insert_small_chunk(M, DV, DVS);\

}\

M->dvsize = S;\

M->dv = P;\

}

4090. replace_dv(gm, r, rsize);

4091. }

获取对应的实际分配内存起始地址、做debug检测,跳转等。

4092. mem = chunk2mem(p);

4093. check_malloced_chunk(gm, mem, nb);

4094. goto postaction;

4095. }

4096.

在smallbins中没有可满足该请求的空闲chunk块,则试图在treebins中查找可满足该请求的空闲chunk块。函数tmalloc_small()的分析在后面给出。

4097. else if (gm->treemap != 0 && (mem = tmalloc_small(gm, nb)) != 0) {

4098. check_malloced_chunk(gm, mem, nb);

4099. goto postaction;

4100. }

4101. }

4102. }

超过最大请求值。

4103. else if (bytes >= MAX_REQUEST)

4104. nb = MAX_SIZE_T; /* Too big to allocate. Force failure (in sys alloc) */

4105. else {

请求的内存大小在MAX_SMALL_REQUESTMAX_REQUEST之间。

首先进行对齐处理:CHUNK_OVERHEAD为边界标记占用的空间,CHUNK_ALIGN_MASK为对齐设置值。

#define pad_request(req) \

(((req) + CHUNK_OVERHEAD + CHUNK_ALIGN_MASK) & ~CHUNK_ALIGN_MASK)

4106. nb = pad_request(bytes);

试图在treebins中查找可满足该请求的空闲chunk块。函数tmalloc_large()的分析在后面给出。

4107. if (gm->treemap != 0 && (mem = tmalloc_large(gm, nb)) != 0) {

4108. check_malloced_chunk(gm, mem, nb);

4109. goto postaction;

4110. }

4111. }

4112.

dv chunk块内分配(注意各个执行路径):

4113. if (nb <= gm->dvsize) {

4114. size_t rsize = gm->dvsize - nb;

4115. mchunkptr p = gm->dv;

剩余空间足够大,则进行分割和组建新chunk块。

4116. if (rsize >= MIN_CHUNK_SIZE) { /* split dv */

4117. mchunkptr r = gm->dv = chunk_plus_offset(p, nb);

4118. gm->dvsize = rsize;

4119. set_size_and_pinuse_of_free_chunk(r, rsize);

4120. set_size_and_pinuse_of_inuse_chunk(gm, p, nb);

4121. }

4122. else { /* exhaust dv */

否则的话,直接全部分配完。

4123. size_t dvs = gm->dvsize;

4124. gm->dvsize = 0;

4125. gm->dv = 0;

4126. set_inuse_and_pinuse(gm, p, dvs);

4127. }

4128. mem = chunk2mem(p);

4129. check_malloced_chunk(gm, mem, nb);

4130. goto postaction;

4131. }

4132.

top chunk块内分配(注意各个执行路径):

4133. else if (nb < gm->topsize) { /* Split top */

4134. size_t rsize = gm->topsize -= nb;

4135. mchunkptr p = gm->top;

4136. mchunkptr r = gm->top = chunk_plus_offset(p, nb);

4137. r->head = rsize | PINUSE_BIT;

4138. set_size_and_pinuse_of_inuse_chunk(gm, p, nb);

4139. mem = chunk2mem(p);

4140. check_top_chunk(gm, gm->top);

4141. check_malloced_chunk(gm, mem, nb);

4142. goto postaction;

4143. }

4144.

最后的办法,直接从系统获取内存空间。函数sys_alloc()的分析在后面给出。

4145. mem = sys_alloc(gm, nb);

4146.

4147. postaction:

和前面的PREACTION宏对应,用于进行解锁操作:

#define POSTACTION(M) { if (use_lock(M)) RELEASE_LOCK(&(M)->mutex); }

#define RELEASE_LOCK(l) pthread_mutex_unlock(l)

4148. POSTACTION(gm);

4149. return mem;

4150. }

4151.

4152. return 0;

4153. }

5.2 函数tmalloc_small

3693. /* allocate a small request from the best fitting chunk in a treebin */

这里的参数nb总是小于等于256,应注意这点。

3694. static void* tmalloc_small(mstate m, size_t nb) {

3695. tchunkptr t, v;

3696. size_t rsize;

3697. bindex_t i;

3698. binmap_t leastbit = least_bit(m->treemap);

3699. compute_bit2idx(leastbit, i);

3700.

3701. v = t = *treebin_at(m, i);

请求字节数目nb <= 256,而tree里的每个chunk块都大于等于256,所以此处不会出现负数,以下某些地方类似考虑。

3702. rsize = chunksize(t) - nb;

3703.

前面曾经说过dltree具有一个十分有用的特性(见前面对dltree的分析部分),下面这个循环就是利用这个特性查找最优分配的节点(即是空闲chunk块大小和请求空间nb最接近的节点),也就是最小的chunk块节点。

#define leftmost_child(t) ((t)->child[0] != 0? (t)->child[0] : (t)->child[1])

同样应注意到“请求字节数目nb<=256,而tree里的每个chunk块都大于等于256”这一点来帮助理解。

3704. while ((t = leftmost_child(t)) != 0) {

3705. size_t trem = chunksize(t) - nb;

3706. if (trem < rsize) {

3707. rsize = trem;

3708. v = t;

3709. }

3710. }

3711.

安全检测。

3712. if (RTCHECK(ok_address(m, v))) {

3713. mchunkptr r = chunk_plus_offset(v, nb);

3714. assert(chunksize(v) == rsize + nb);

3715. if (RTCHECK(ok_next(v, r))) {

将其拆出dltree

3716. unlink_large_chunk(m, v);

剩余空间太小则直接全部分配。

3717. if (rsize < MIN_CHUNK_SIZE)

3718. set_inuse_and_pinuse(m, v, (rsize + nb));

3719. else {

否则将剩余空间组建成新的chunk块并替换为新的dv chunk块。

3720. set_size_and_pinuse_of_inuse_chunk(m, v, nb);

3721. set_size_and_pinuse_of_free_chunk(r, rsize);

3722. replace_dv(m, r, rsize);

3723. }

3724. return chunk2mem(v);

3725. }

3726. }

3727.

3728. CORRUPTION_ERROR_ACTION(m);

3729. return 0;

3730. }

5.3 函数tmalloc_large

3620. /* allocate a large request from the best fitting chunk in a treebin */

MAX_SMALL_REQUEST对齐值<= nb < MAX_REQUEST对齐值

3621. static void* tmalloc_large(mstate m, size_t nb) {

3622. tchunkptr v = 0;

设置初始值,注意rsizesize_t类型变量,因此一个很小的负数事实上是一个很大的正数。

3623. size_t rsize = -nb; /* Unsigned negation */

3624. tchunkptr t;

3625. bindex_t idx;

计算nb字节数所对应的dltree树结构。

3626. compute_tree_index(nb, idx);

3627.

dltree树结构不为空,即存在和nb字节数同在一个箱子内(不理解“同在一个箱子内”则请查看前面讲解dltree内容部分)的空闲chunk块。

3628. if ((t = *treebin_at(m, idx)) != 0) {

3629. /* Traverse tree for this bin looking for node with size == nb */

遍历dltree试图找到一个size==nb的空闲chunk块。

前面曾经讲到过dltree就是关键码只有01Trie树。并且根据treebins箱的分法,0号箱和1号箱的关键码都只需7位长(因为其范围为128,表xxx的第二列),2号箱和3号箱的关键码都只需8位长(因为其范围为256,表xxx的第二列),……,依次类似。

宏leftshift_for_tree_index(idx)计算的是32 减去 idx号箱对应的关键码长度 的值。

#define leftshift_for_tree_index(i) \

((i == NTREEBINS-1)? 0 : \

((SIZE_T_BITSIZE-SIZE_T_ONE) - (((i) >> 1) + TREEBIN_SHIFT - 2)))

#define NTREEBINS (32U)

#define SIZE_T_BITSIZE (sizeof(size_t) << 3)

#define TREEBIN_SHIFT (8U)

#define SIZE_T_ONE ((size_t)1)

直接来看具体的值吧,依旧是32位平台下:

箱号idx => leftshift_for_tree_index(idx)值

0=>25

1=>25

2=>24

3=>24

4=>23

5=>23

6=>22

7=>22

8=>21

9=>21

10=>20

11=>20

12=>19

13=>19

14=>18

15=>18

16=>17

17=>17

18=>16

19=>16

20=>15

21=>15

22=>14

23=>14

24=>13

25=>13

26=>12

27=>12

28=>11

29=>11

30=>10

31=>0

因此下面这个sizebits的值就是将nb中起关键码作用的位移到最左边的结果值。

3630. size_t sizebits = nb << leftshift_for_tree_index(idx);

3631. tchunkptr rst = 0; /* The deepest untaken right subtree */

3632. for (;;) {

3633. tchunkptr rt;

相差多大?

宏chunksize用于计算chunk块大小。

#define chunksize(p) ((p)->head & ~(INUSE_BITS))

3634. size_t trem = chunksize(t) - nb;

比之前选定的chunk块更合适?

3635. if (trem < rsize) {

3636. v = t;

没有比它更合适的了,就是它了。

3637. if ((rsize = trem) == 0)

3638. break;

3639. }

记录当前节点的右子树根节点。

3640. rt = t->child[1];

根据关键码值进入相应的子树。

3641. t = t->child[(sizebits >> (SIZE_T_BITSIZE-SIZE_T_ONE)) & 1];

rst保存包含有最接近nb字节数大小chunk块的dltree子树。

3642. if (rt != 0 && rt != t)

3643. rst = rt;

将要继续查找的子树为空,则保存rstt,然后跳出for循环。

3644. if (t == 0) {

3645. t = rst; /* set t to least subtree holding sizes > nb */

3646. break;

3647. }

3648. sizebits <<= 1;

3649. }

3650. }

3651.

tv都为0表示请求字节大小对应的treebin为空,因此只有在更大的箱号内查找。

3652. if (t == 0 && v == 0) { /* set t to root of next non-empty treebin */

和前面的某些代码类似,计算包含大于请求字节数目chunk块的所有箱号对应掩码。

3653. binmap_t leftbits = left_bits(idx2bit(idx)) & m->treemap;

存在。

3654. if (leftbits != 0) {

3655. bindex_t i;

选择最合适的分箱。

3656. binmap_t leastbit = least_bit(leftbits);

计算对应箱号。

3657. compute_bit2idx(leastbit, i);

箱子对应dltree根节点。

3658. t = *treebin_at(m, i);

3659. }

3660. }

3661.

执行到这已经可以确定以t为根节点的dltree中所有chunk块都可满足申请请求,下面这个循环只不过试图在这个dltree中找到一个最合适的chunk块来用于内存分配。最合适的chunk块被保存在变量v内。

3662. while (t != 0) { /* find smallest of tree or subtree */

3663. size_t trem = chunksize(t) - nb;

3664. if (trem < rsize) {

3665. rsize = trem;

3666. v = t;

3667. }

leftmost_child宏优先选取左子树,在左子树为空的情况下则选取右子树。

#define leftmost_child(t) ((t)->child[0] != 0? (t)->child[0] : (t)->child[1])

3668. t = leftmost_child(t);

3669. }

3670.

3671. /* If dv is a better fit, return 0 so malloc will use it */

前面在treebins中选择的chunk块存在(即变量v不为空),并且它比dv chunk块更适合(即选择的chunk块大小比dv chunk块大小更接近当前请求字节数目)用于内存分配以服务当前申请请求。如果dv chunk块更适合用来分配,则本函数将返回0,因此会在返回到dlmalloc函数内再进行在dv chunk块的内存分配操作。

3672. if (v != 0 && rsize < (size_t)(m->dvsize - nb)) {

下面同样是分割、组建新chunk块、设置边界标记等,在此不再累述。

3673. if (RTCHECK(ok_address(m, v))) { /* split */

3674. mchunkptr r = chunk_plus_offset(v, nb);

3675. assert(chunksize(v) == rsize + nb);

3676. if (RTCHECK(ok_next(v, r))) {

3677. unlink_large_chunk(m, v);

3678. if (rsize < MIN_CHUNK_SIZE)

3679. set_inuse_and_pinuse(m, v, (rsize + nb));

3680. else {

3681. set_size_and_pinuse_of_inuse_chunk(m, v, nb);

3682. set_size_and_pinuse_of_free_chunk(r, rsize);

3683. insert_chunk(m, r, rsize);

3684. }

3685. return chunk2mem(v);

3686. }

3687. }

3688. CORRUPTION_ERROR_ACTION(m);

3689. }

3690. return 0;

3691. }

3692.

5.4 函数sys_alloc

3322. /* Get memory from system using MORECORE or MMAP */

3323. static void* sys_alloc(mstate m, size_t nb) {

3324. char* tbase = CMFAIL;

3325. size_t tsize = 0;

3326. flag_t mmap_flag = 0;

3327.

相关设置值的初始化。

3328. init_mparams();

3329.

3330. /* Directly map large chunks */

dlmalloc向系统申请内存有两种方式:一为ORECORE(使用函数sbrk())方式;一为MMAP(使用函数mmap())方式。由于MMAP方式一般都是取到不连续的内存空间,因此只有在某些情况下(见下面)才使用它。

如果设置允许调用mmap函数并且字节数nb已超过了预设的可以调用mmap界限则利用mmap函数向系统申请内存。

3331. if (use_mmap(m) && nb >= mparams.mmap_threshold) {

函数mmap_alloc()的分析后面给出。

3332. void* mem = mmap_alloc(m, nb);

3333. if (mem != 0)

3334. return mem;

3335. }

3336.

根据相对优劣排序依次按照如下三种方法从系统获取内存:

1,利用MORECORE分配连续空间

2,利用MMAP分配新空间

3,利用MORECORE分配不连续空间

3337. /*

3338. Try getting memory in any of three ways (in most-preferred to

3339. least-preferred order):

3340. 1. A call to MORECORE that can normally contiguously extend memory.

3341. (disabled if not MORECORE_CONTIGUOUS or not HAVE_MORECORE or

3342. or main space is mmapped or a previous contiguous call failed)

3343. 2. A call to MMAP new space (disabled if not HAVE_MMAP).

3344. Note that under the default settings, if MORECORE is unable to

3345. fulfill a request, and HAVE_MMAP is

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn核实处理。