dlmalloc浅析【转】
转自:https://wertherzhang.com/dlmalloc%E6%B5%85%E6%9E%90/#dlmalloc_1
- version 1.0 by Werther Zhang @ 2014.03.15 Write done @WizNote
- Version 1.1 by Werther Zhang @ 2014.03.20 Export to @Word
- Version 1.2 by Werther Zhang @ 2016.07.02 Move to @leanote
来源: https://pengzhangdev.github.io/dlmalloc%E6%B5%85%E6%9E%90/
dlmalloc介绍¶
dlmalloc在某些程度上说,是最好的内存管理工具之一。它是由Doug Lea 在1987年开始编写,所以大部分人会称呼它为Doug Lea's Malloc, 简称dlmalloc。
算法概览¶
dlmalloc 根据内存粒度的大小分别使用chunk和segment进行管理。Segment是通过sbrk分配,类似进程的数据段,属于极少遇到的情况。Dlmalloc中大量存在的内存块是chunk。Chunk的结构如下。
被用户使用的chunk结构看起来像下面这样(非精确图):
而未被使用的chunk看起来如下(非精确图):
在虚拟地址上,dlmalloc会保证空闲的内存块存在“孤岛效应”,也就是说,任一块空闲内存块的前后必定是被使用的内存块。因为在free或者其他任何时候,dlmalloc总是会合并空闲的内存块。
对 于chunk,根据其大小,分为大内存和小内存。小内存由32个双向链表通过分箱(每一个箱子对应一个大小,每个箱子中存放一个双向链表)进行管理,而大 内存统一由bitwise tries树通过分箱(每一个箱子对应一个内存范围,每一个箱子中存放一棵树,每一棵树中若有等大小的内存,由双向链表管理) 进行管理。
每一个chunk的大小必须为8byte的整数倍。所以在’size of chunk’ 域的低三位存放了该chunk属性的标志位。
P (PINUSE_BIT) 位, 保 存在内存块大小(一般是双字节的倍数)的未使用的低位, 是一个表示前一个内存块是否被使用的bit位.如果这个bit位被清理了,那么在当前内存块 之 前的一个字 大小数据保存着前一个内存块的大小,用于寻找前一个内存块.而 第一个内存块这个bit位总是被置上的,防止访问不存在的内存区域.如果某个 内 存块的 pinuse被设置了,你就无法决定上一个内存块的大小,并且如果你真的尝 试这 么做了,有可能内存访问出错.
C (CINUSE_BIT) bit位, 保存在内存块的第二低的bit位,冗余地记录着当前块是 否被使用了(除非当前内存块是被映射的).这个冗余信息用于在free和realloc操 作时的检查,并且减少在执行free和合并内存块时的间接操作.
任何新分配的内存块必须都设置了cinuse和pinuse.这意味着,任何分配了的内 存块的边界要么是一个先前分配并且仍然在使用(in-use)的内 存块,要么是它自 己的大内存区域(segment)的地址.这样确保所有的内存申请(allocations)都是从任何能找到的内存块的“最低”部分 获取.进一步地说,不可能存在一个空闲内 存块在物理上紧邻着另 一个空闲内存块,所以每一个空闲内存块被证实在使用 (inuse)的内存块或者内存尾 部之前和之后.
注意: 当前块的 ’foot’实际上代表着下一个内存块的prev_foot.这使对齐等操 作处理变得容易但会使人在扩展或维护这份代码时感到困惑.
以下是对特殊的内存块的说明:
- 特 殊的内存块’top’是最顶上的可用内存块(i.e., 紧邻着可用内存的边界). 这块内存块会被特殊对待. Top 内存块不会被包含在任何的内存分 类箱里, 只有在没有任何其他内存块可使用时,才被使用,并且在它非常大时(查看 M_TRIM_THRESHOLD) 会被释放一部分回系统.在实际 上,top内存块一般被认 为是比其他所有的内存块都大.Top内存块从来不会更新它的尾部数据区域因 为根本没有内存块会在索引上紧跟其后.但是,空间 还是会分配给它 (TOP_FOOT_SIZE) 用户做内存块的拆分和合并,当空间需要扩展时.
- dv chunk 是保存了最近被使用并切割过的内存块,保存在全局的gm中。它不归属到双向链表或者树中管理。但当它被替换时,也会加入内存管理中。
Bitwise trie 树, 实际上是结合了二叉树和trie树(又叫字典树, 前缀数).二叉树只有左右子树. Trie树是一种有序树,用于保存关联数组,类似 hash table, 有key 和 value 结构. 如下图.key 实际上就是到达value的路径.而bitwise trie, 它是将 key设置为0/1, 所以, 是一棵二叉树.在dlmalloc的bitwise trie tree中, 0 代表进入左子树, 1 代表进入右子 树. 而key的长度对应路径的深度.参考 [treebins和树管理图解] 的图,假设我们现在要查找的是512对应的结点, 则将520对 应二进制码是1000001000, 对应的箱子号是2号,则其表示的内存范围为256, 特征码长度为8, 也就是00001000.考虑到, 所有请 求大小为8byte倍数,所以, 实际特征码为00001, 树深为5, 前4层为左子树,第五层为右子树.但刚才描述的是trie树和 bitwise trie 树, 不是dlmalloc使用的树.
非MSPACE代码逻辑分析¶
struct malloc_chunk {
size_t prev_foot; /* 如果前一个内存块空闲,表示前一个内存块的大小 */
size_t head; /* 大小和inuse bit位*/
struct malloc_chunk* fd; /* 如果是空闲内存块,指向双向链表*/
struct malloc_chunk* bk;
};
typedef struct malloc_chunk mchunk;
typedef struct malloc_chunk* mchunkptr;
typedef struct malloc_chunk* sbinptr; /* 内存块分类箱的类型 */
head 域中变量的说明:¶
PINUSE_BIT 在前一个相邻的内存块被使用时,这个标志位被置上. CINUSE_BIT 在当前内存块被使用时,这个标志位被置上. FLAG4_BIT 在当前版本的dlmalloc中未被使用
如 何做到在head中既存放块大小又存放标志位的呢?首先提到一点是,所有的块的 大小都是按最少8bit对齐的,换句话说,表示大小的数字,低3位必定为 0,所以就有 效地利用了低3位存放标志位.所以,获取chunk的大小用下面的宏,将head的低三 位清成0,取出.
#define chunksize(p) ((p)->head & ~(FLAG_BITS))
下面介绍下dlmalloc维护的一个全局数据结构.
2579 struct malloc_state {
2580 binmap_t smallmap; // 32bit, 小内存箱子的位图.
2581 binmap_t treemap; // 32bit, 大内存箱子的位图
2582 size_t dvsize; // dv chunk 的大小
2583 size_t topsize; // top chunk的大小
2584 char* least_addr; // dlmalloc管理的内存的最小地址,也就是最小的segment 基地址.
2585 mchunkptr dv; // dv chunk. 最近被分割使用的chunk
2586 mchunkptr top; // top chunk. 顶部,靠近有效内存的chunk,详见总结图.
2587 size_t trim_check; // 检查top chunk大小是否超的函数.
2588 size_t release_checks;
2589 size_t magic;
2590 mchunkptr smallbins[(NSMALLBINS+1)*2]; // 32个链表头
2591 tbinptr treebins[NTREEBINS]; // 32棵树
2592 size_t footprint;
2593 size_t max_footprint;
2594 size_t footprint_limit; /* zero means no limit */
2595 flag_t mflags;
2596 #if USE_LOCKS
2597 MLOCK_T mutex; /* locate lock among fields that rarely change */
2598 #endif /* USE_LOCKS */
2599 msegment seg; // segment链表.
2600 void* extp; /* Unused but available for extensions */
2601 size_t exts;
2602 };
mchunkptr smallbins[(NSMALLBINS+1)*2];
这里会有个疑问,理论上, 32个链表头,我们会使用struct malloc_chunk smallbins[NSMALLBINS];
但这里不使用的原因是, 对于链表头而言, malloc_chunk
的 prev_foot
和head
两个域是没有被使用的,实际需要的大小是2个指针大 小.所以,dlmalloc使用了覆盖的方法. 前一个 malloc_state 的fb/bk 踩了后一个的 malloc_state的 prev_foot/head.所以大小应该为 32 * 8 + 8, 也就是33 * 2 个指针大小.
被覆盖的数据结构¶
当内存块未被使用时,他们作为列表或者树的节点.
小内存(“Small”) 块存储在环形双向链表中,看起来像如下这样.
而 大 的内存块使用内存块大小为关键字的bitwise digital tree (又叫aka tree).因为malloc_tree_trunks只是 用于大小大于256bytes的空闲内存块,他们 的大小不会受到用户申请的内存大小的限制.每 一个节点的结构看起来像如下这 样.
每一棵树都拥有唯一的内存块大小.而具有同样大小的内存块会被安排在双向链 表里,与最老的内存块一起(指,以FIFO的规则,下一个要被使用的内存块).如果一 个具有同样大小的内存块被插入,它就会用类似小内存的fb/bk的指针一样的方式,从 原有的节点移除.
每 一 棵树包含大小为2的乘方范围的内存块(最小为0x100 <= x < 0x180),在树 的每一层都会被分成一半,即小的一半 (0x100 <= x < 0x140)作为左子树,大的一 半作为右子树(0x140 <= x < 0x180).
通过使用这种规则,每一个节点的左子树包含的内存块大小都小雨其右子树.
Smallbins和双向链表管理小内存图解。
Treebins和树管理大内存的图解:
dlmalloc代码分析¶
dlmalloc 对小内存分配有如下5个规则(按优先级顺序):
- 如果与请求内存大小匹配的箱子存在空闲,则使用当前箱子,否则使用临近的 箱子。在能不分割内存的情况下尽量不分割内存。
- 如果dv chunk足够大,那么使用dv chunk。 dv chunk是指最近一次小内存 申请时使用的内存块。 这个规则是,尽量保证分配的内存连续。
- 在smallbin和treebin中寻找可以使用的内存块,并分割。将剩下的内存 块保存到dv chunk中。
- 如果top chunk 足够大,则使用top chunk
- 如果请求内存实在太大,则使用系统分配内存。
大内存分配的规则:
- 在treebin中找到最适合的最小内存,如果它比dv chunk的更合适,就使用它, 如果有需要就分割它。
- 如果dv chunk 比其他所有的更合适,使用dv chunk。
- 如果top 足够大,使用top chunk。
- 如果请求的大小 >= mmap threshold, 则使用系统的mmap。
- 直接从系统分配内存并使用。
小内存规则一¶
4597 if (bytes <= MAX_SMALL_REQUEST) {
// MAX_SMALL_REQUEST 实现
2577 #define MAX_SMALL_REQUEST (MAX_SMALL_SIZE - CHUNK_ALIGN_MASK - CHUNK_OVERHEAD)
这里bytes为申请的内存大小, MAX_SMALL_REQUEST
就是之前提到过的最大的小内存块的大小,就是256byte,即,256byte以下的所有内存都是在双向链表中匹配.
4600 nb = (bytes < MIN_REQUEST)? MIN_CHUNK_SIZE : pad_request(bytes);
2225 #define MIN_REQUEST (MIN_CHUNK_SIZE - CHUNK_OVERHEAD - SIZE_T_ONE)
2228 #define pad_request(req) \
2229 (((req) + CHUNK_OVERHEAD + CHUNK_ALIGN_MASK) & ~CHUNK_ALIGN_MASK)
这里nb就是加上协议数据后的实际dlmalloc会分配的内存块大小.前文笔者提到过,小内存块的最小值为8byte,所以,不论申请的内存多小,都使用最小值.
4601 idx = small_index(nb);
2572 #define SMALLBIN_SHIFT (3U)
2825 #define small_index(s) (bindex_t)((s) >> SMALLBIN_SHIFT)
这里idx得到的是该内存块对应的smallbin箱子的箱号.前文在提到head时,提到过,小内存块的大小为8byte的倍数,所以,右移3位来定位对应箱子的箱号.
4602 smallbits = gm->smallmap >> idx;
smallmap 是各个箱子的位图,32bit,对应32个箱子,每一位为1表示该箱号中有对 应大小的内存块,为0则表示没有.该行代码是把对应箱号的比特位移到最右侧.
4604 if ((smallbits & 0x3U) != 0) {
4605 mchunkptr b, p;
4606 idx += ~smallbits & 1; /* Uses next bin if idx empty */
4607 b = smallbin_at(gm, idx);
这里 0x3U 低8位就是 0000 0011. 所以,smallbits&0x3U 为真的条件如下:(上文提到,smallbits的最右位表示idx箱号是否有空闲块)
- 低2位为 11. idx有空闲块,比idx大1箱号的有空闲块.
- 低2位为 10. idx无空闲块,比idx大1箱号的有空闲块.
- 低2位为 01. idx有空闲块,比idx大1箱号的无空闲块.
4606行是在重新定位到真正有空闲块的箱号. ~smallbits & 1 在当前箱子为0的 情况下,值为1;当前箱子为1的情况下,值为0.所以是有限是用正好满足大小的箱 子.
2831 #define smallbin_at(M, i)
((sbinptr)((void*)&((M)->smallbins[(i)<<1])))
i 就是 idx, 而M则是gm(需要在上文预先描述gm结构体成员作用).smallbins(需在上文描述)就是双向链表数组,也就是对应箱号内部的空闲块链表的首地址.
值得高兴的是,我们拿到链表了,接下来就是取出空闲块,和一些标志位的处理了.
4608 p = b->fd;
4609 assert(chunksize(p) == small_index2size(idx));
4610 unlink_first_small_chunk(gm, b, p, idx);
4611 set_inuse_and_pinuse(gm, p, small_index2size(idx));
4612 mem = chunk2mem(p);
4613 check_malloced_chunk(gm, mem, nb);
4614 goto postaction;
fd域是前一个链表节点.而B是表头, 也就是说,我们取链表的第一个元素,取到我们需要的内存块地址.这个assert其实就是确认下,当前的箱号的内存块大小跟当前内存块的大小是否匹配.
3629#define unlink_first_small_chunk(M, B, P, I) {\
3630 mchunkptr F = P->fd;\
3631 assert(P != B);\
3632 assert(P != F);\
3633 assert(chunksize(P) == small_index2size(I));\
3634 if (B == F) {\
3635 clear_smallmap(M, I);\
3636 }\
3637 else if (RTCHECK(ok_address(M, F) && F->bk == P)) {\
3638 F->bk = B;\
3639 B->fd = F;\
3640 }\
3641 else {\
3642 CORRUPTION_ERROR_ACTION(M);\
3643 }\
3644}
2921#define clear_smallmap(M,i) ((M)->smallmap &= ~idx2bit(i))
这里 B P F的关系是 F -> P -> B , 所以,理论上, P 不等于F 也不等于B,如 果相等,就意味着是空链表(只有表头).而如果B == F, 意味着该链表中只有一 个空闲内存块.取出该内存块之后,咱们要把该箱子标记为空(3635, 2921).当然, 更多的情况是从链表中移除节点P.
// 4611 行函数实现
3058#define set_inuse_and_pinuse(M,p,s)\
3059 ((p)->head = (s|PINUSE_BIT|CINUSE_BIT),\
3060 ((mchunkptr)(((char*)(p)) + (s)))->head |= PINUSE_BIT)
这就是在P 的head中置上CINUSE位和P 的下一块内存的head中设置上PINUSE位.
最后mem = chunk2mem(p); 就是取出传递个用户的有效内存地址,. check_malloced_chunk(gm, mem, nb); 是调试用的,检查该分配的内存块的各个 属性是否正常.
到此,小内存的,正好符合或正好临近箱子有空闲块的逻辑分析完成,咱们拿到了 需要的内存.
小内存规则三¶
下面,是上述情况不满足,也就是当前内存请求对应的箱号idx的smallbits低2位为 00 ,也就是说,没有空闲块.
4617 else if (nb > gm->dvsize) {
4618 if (smallbits != 0) {
(dvsize需要在gm的分析中描述掉) smallbits != 0 意味着,在比请求的内存块大的箱子中,总有空闲块存在.所以接下来的目的是找到最小的空闲块.
4622 binmap_t leftbits = (smallbits << idx) & left_bits(idx2bit(idx));
4623 binmap_t leastbit = least_bit(leftbits);
4624 compute_bit2idx(leastbit, i);
2917#define idx2bit(i) ((binmap_t)(1) << (i))
2929#define least_bit(x) ((x) & -(x))
2932#define left_bits(x) ((x<<1) | -(x<<1))
4622 行 位与的右操作数是一个32bit的数,该数的低(idx+1)位为0,其余位为1;左操作数就是对应低idx位为0,同时代码逻辑走到这里,我们可以确定,低(idx+2)为0。 least_bit 的功能是,保留leftbits中从右往左的第一个为1的位,其余位为0. 则该leastbit对应的就是符合请求的最小内存块的位图。compute_bit2idx 是将leastit位图转换成箱号。 这里 i 就是获取到的箱号。
4625 b = smallbin_at(gm, i);
4626 p = b->fd;
4627 assert(chunksize(p) == small_index2size(i));
4628 unlink_first_small_chunk(gm, b, p, i);
拿到箱号之后,这块的逻辑与上文规则一的逻辑一样。
4629 rsize = small_index2size(i) - nb;
取出当前箱子的内存块大小,减去用户请求的大小,剩下的就是剩余的内存块,这个剩余内存块会放到dv chunk中。
4631 if (SIZE_T_SIZE != 4 && rsize < MIN_CHUNK_SIZE)
4632 set_inuse_and_pinuse(gm, p, small_index2size(i));
4633 else {
4634 set_size_and_pinuse_of_inuse_chunk(gm, p, nb);
4635 r = chunk_plus_offset(p, nb);
4636 set_size_and_pinuse_of_free_chunk(r, rsize);
4637 replace_dv(gm, r, rsize);
4638 }
2269#define chunk_plus_offset(p, s) ((mchunkptr)(((char*)(p)) + (s)))
2284#define set_size_and_pinuse_of_free_chunk(p, s)\
2285 ((p)->head = (s|PINUSE_BIT), set_foot(p, s))
3063#define set_size_and_pinuse_of_inuse_chunk(M, p, s)\
3064 ((p)->head = (s|PINUSE_BIT|CINUSE_BIT))
3584#define insert_small_chunk(M, P, S) {\
3585 bindex_t I = small_index(S);\
3586 mchunkptr B = smallbin_at(M, I);\
3587 mchunkptr F = B;\
3588 assert(S >= MIN_CHUNK_SIZE);\
3589 if (!smallmap_is_marked(M, I))\
// 如果是对应箱号原状态为0,则置1.
3590 mark_smallmap(M, I);\
3591 else if (RTCHECK(ok_address(M, B->fd)))\
3592 F = B->fd;\
3593 else {\
3594 CORRUPTION_ERROR_ACTION(M);\
3595 }\
3596 B->fd = P;\
3597 F->bk = P;\
3598 P->fd = F;\
3599 P->bk = B;\
3600}
3648#define replace_dv(M, P, S) {\
3649 size_t DVS = M->dvsize;\
3650 assert(is_small(DVS));\
3651 if (DVS != 0) {\
3652 mchunkptr DV = M->dv;\
3653 insert_small_chunk(M, DV