1 版本
v2.8.6 Wed Aug 29 06:57:58 2012 Doug Lea
* fix bad comparison in dlposix_memalign
* don't reuse adjusted asize in sys_alloc
* add LOCK_AT_FORK -- thanks to Kirill Artamonov for the suggestion
* reduce compiler warnings -- thanks to all who reported/suggested these
tips:
顺便提一下 Doug Lea 推荐了一本《Hacker ' s Delight》,该书是介绍各种位运算奇技著作。
2 术语
介绍dlmalloc之前,还需要了解一些相关术语及其含义,这些到后面就不再做过多解释了.
Payload: 有效负载.指的是实际交给应用程式使用的记忆体大小.
Overhead: 负载,开销.本意是为了满足分配需求所消耗的内存量,实际在代码注释中多指除了payload之外的额外开销(有些书中也称之为cookie).
Chunk: 区块.是内存分配的基本单位,类似物质世界中的原子不可再分. dlmalloc对记忆体的管理基本上都是以chunk为单位.一个典型的chunk是由使用者程序使用的部分(payload)以及额外的标记资讯(overhead)组成.
Bin: 分箱.用来管理相同或同一区间大小的chunk.在dlmalloc中分为sbin和tbin两种.
Mspace: 分配空间.说白了就是dlmalloc中内存池的叫法.在dlmalloc中可以管理多个mspace.如果不显式宣告,将会使用一个全域性的匿名空间,或者使用者可以自行划分空间交给dlmalloc管理.
Segment: 区段.一般情况下,内存分配都是在一片连续区间内开采(exploit).但也会遇到不连续的情况,这就需要分成若干个区段记录.多个区段可以同属一个mspace.
Fenceposts: 栅栏.大多数分配器中, fencepost起到非连续内存间的隔离作用.一般这种隔离被用做安全检查.分配器会在fenceposts所在位置写上特殊标记,一旦非连续内存间发生写入溢位(overwrite)就可以通过异常的fenceposts值发出警告.
Bookkeeping: 记录资讯.不同于每个chunk中的overhead,这里指的是整个mspace控制块的记录信息.往往这部分信息都固定在mspace开始的一段空间,或者干脆就放在地址空间的静态区中.
Granularity: 粒度.这个粒度指的是从system heap上获取内存的最小单位.一般来说该值至少为一个page size, 且必须以2为底.
Mmap: 本意是类unix系统的文档映射.但在dlmalloc中表示的更宽泛,这里指代可以在程序地址空间中开辟非连续内存空间的系统调用.
Morecore: 指可以在程序地址空间中开辟连续内存空间的系统调用.在类unix系统下morecore指的是sbrk系统调用.
Program break: 前面提到的sbrk()实际也是一个库函式,真正起作用的是brk()系统调用.这个函式其实就是break的缩写.所谓的break是一个代表程序heap区top-most位置的指标.当我们通过sbrk/brk向系统请求记忆体时,系统做的仅仅是移动break指针,内存就这样被划拨到heap中了.而当释放记忆体时,就反方向移动该指针,内存就返回给系统.
Footprint: 从系统获得的内存量.指的是当前dlmalloc从system heap获取的内存总和.设立footprint一方面是为了方便统计,另一方面也可以限制dlmalloc从系统获取的最大内存量.
Trimming: 裁剪.被dlmalloc管理的内存被free后,并不直接返还给系统,而是当积累到一定程度会通过一些演算法判断system heap是否收缩(shrink),这个过程在dlmalloc中称作auto -trimming.
2 地址对齐
我们为什么要进行内存对齐?
需要从两个方面来考量:
第一个方面,我们知道CPU 最终执行的是机器码,一条汇编指令编译成一条机器码,用到的数据是从寄存器读取的,寄存器的有32位大小,64位大小的不同,这要看CPU的支持类型。寄存器的数据又是从cache中通过总线load到的,如果load一个64位的数据,而这个数据没有8字节对齐,可能需要load两次才能把这个数据加载。另一个方面,如果cache Miss,就需要从内存中把数据load到cache中,那么一次能够load多少呢?cache line的大小,双通道是64字节,所以我们经常看到程序中的结构体大都是cache line对齐的,一旦Miss之后会严重牺牲性能(会有几十甚至上百个时钟周期的代价)。如何判断内存地址对齐呢?
#define is_align(A,n) ((A) % (n) == 0)
因为取模运算是通过软件实现的,不是CPU硬件完成的所以代价是很大的。
那么有没有好的方法呢?看一下的规律:
对齐的这个数是8 是2^3,余数都是被除数的后三位。假如:
2^m = n
(n是以2为底的m次幂)
则A除以n的余数相当于二进位制下A的最后m位呢?答案是肯定的。
只要做到对齐的数n是2为底的m次就可以了。
如果我们求出m,就可以通过取出A的最后m位来判断是否对齐.
以2为底的指数在二进位制下还有一个有用的特性,
比如, 8的二进位制表示为1000b,而8 – 1表示为111b.
16的二进位制表示为10000b,而16 – 1表示为1111b.
而32的二进位制表示为100000b, 32 – 1表示为11111b.
这样我们又发现了第二个规律,以2为底的m次幂的二进位制数减去1时,由于连续借位,最终出现除了MSB之外,低位全部被1填充的情况.因此我们有:
如果n为以2为底的m (m > 0, m = 1, 2, 3, …)次幂指数,当以n为对齐边界时,称n – 1为该对齐边界的对齐掩码(align mask).
2.1 上对齐和下对齐
那么对于一个非对齐的数值进行处理实际上存在两种方式,上对齐(align up)和下对齐(align_down).还是以8为对齐边界来讨论,
图中, 28如果采用下对齐的方式,对齐到24,则只需要简单舍去其余数即可:
如果选择上对齐, 则需要另外的方法,
如上图所示, 首先将28加上一个偏移,使其跳跃到下一个对齐区间里去,然后再对其使用下对齐方式,就间接实现了在当前区间中上对齐.这个偏移值为对齐边界减1.为什么这里会减1呢?这里与对齐掩码什么的无关了,因为若被对齐数本身已经处于对齐状态,再加上一个对齐长度并与对齐掩码运算后就会留在下一个对齐区间中,这个结果显然是不正确的了.因此得到的程式码如下,
计算对齐偏移量
有时候不需要直接获取对齐后的地址,而是计算出一个对齐偏移量,
3 结构
3.1 chunk
chunk被划分为两种类型,小于256字节的称为small chunk,而大于等于256字节的被称为tree chunk.
3.1.1 chunk布局
蓝色区域的部分代表当前的已分配chunk,而黄色区域代表上一个和下一个chunk的部分区域.
蓝色区域开始看, 在已分配chunk的开始记录当前chunk的size信息,同时在最后两个bit位分别记录当前chunk和前一个chunk是否被使用,简称为C和P两个bit位.之所以可以做到这一点,是因为在dlmalloc中所有的chunk size至少对齐到大于8并以2为底的指数边界上.这样,至少最后3位都是0,因此这些多余的bit位就可以拿来记录更多的信息. size信息后面是payload部分,显然,当前chunk的使用信息也会记录在下一个chunk开始的P位上.
而对于一个空闲chunk, 其内存布局应该是这样的,
与之前的一致, 蓝色区域代表当前空闲chunk, 黄色区域代表相邻的前后chunk.可以看到空闲chunk与之前的区别在于开始的size后多了next和prev指针,它们把具有相同大小的空闲chunk链接到一起.另一个区别是,在空闲chunk最后同样记录该chunk的大小信息.那么为什么同样的信息要记录两次呢?在下一个小节中会解释这个问题.
另外, 还有一点需要说明的是,空闲chunk的相邻chunk必然是已分配的chunk.因为如果存在相邻两个chunk都是空闲的,那么dlmalloc会把它们合并为一个更大的chunk.
3.1.2 边界标记法(Boundary Tag)
据作者说,此方法最早是大神Knuth提出的.实现边界标记法使用了名为
malloc_chunk的结构体,它的定义如下:
struct malloc_chunk {
size_t prev_foot; /* Size of previous chunk (if free). */
size_t head; /* Size and inuse bits. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
};
typedef struct malloc_chunk mchunk;
typedef struct malloc_chunk* mchunkptr;
该结构体(以后简称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 Allocate main heap
[root@devel-ng-exporter-225 vpp]# pwd
/mnt/vdb1/vpp/build-root/build-vpp_debug-native/vpp
[root@devel-ng-exporter-225 vpp]# cgdb bin/vpp
bin/vpp -c /etc/vpp/startup.conf
main -> clib_mem_init_thread_safe
225│ void *
226│ clib_mem_init_thread_safe (void *memory, uword memory_size)
227│ {
228├> return clib_mem_init (memory, memory_size);
229│ }
默认情况下是 2^30 = 1G的内存,当然也可以在/etc/vpp/startup.conf中设置想要的大小。
clib_mem_init_thread_safe (memory=0x0, memory_size=1073741824)
->create_mspace (capacity=1073741824, locked=1)
-> init_mparams () /* Initialize mparams */
malloc_params包含全局属性,包括那些可以使用mallopt动态设置的属性。实例mparams在init_mparams中初始化。注意,“magic”的非零值也用作初始化标志。
$3 = {magic = 3735935678, page_size = 4096, granularity = 65536, mmap_threshold = 18446744073709551615, trim_threshold = 2097152, default_mflags = 7}
4019│ mspace create_mspace(size_t capacity, int locked) {
4020│ mstate m = 0;
4021│ size_t msize;
4022│ ensure_initialization();
4023│ msize = pad_request(sizeof(struct malloc_state));
4024│ if (capacity < (size_t) -(msize + TOP_FOOT_SIZE + mparams.page_size)) {
4025├> size_t rs = ((capacity == 0)? mparams.granularity :
4026│ (capacity + TOP_FOOT_SIZE + msize));
4027│ size_t tsize = granularity_align(rs);
4028│ char* tbase = (char*)(CALL_MMAP(tsize));
4029│ if (tbase != CMFAIL) {
4030│ m = init_user_mstate(tbase, tsize);
4031│ m->seg.sflags = USE_MMAP_BIT;
4032│ set_lock(m, locked);
4033│ }
4034│ }
4035│ return (mspace)m;
4036│ }
(size_t) -(msize + TOP_FOOT_SIZE + mparams.page_size, 有符号类型(负数)转化为了无符号类型数(size_t),表示最大可用空间数。
真实大小:size_t rs = capacity + TOP_FOOT_SIZE + msize
内存是0x10000对齐。是0x10000的整数倍。我们看到 tsize = 0x40010000
使用mmap获得一个匿名的内存映射。
有了内存了现在就要构建 dlmalloc了,进入了malloc space
初始化函数是:
static mstate init_user_mstate(char* tbase, size_t tsize)
-> size_t msize = pad_request(sizeof(struct malloc_state));
为什么使用pad_request而不是直接使用sizeof(struct malloc_state)?
/* pad request bytes into a usable size */
#define pad_request(req) \
(((req) + CHUNK_OVERHEAD + CHUNK_ALIGN_MASK) & ~CHUNK_ALIGN_MASK)
这个地方是chunk,16字节对齐(因为最小的chunk就是16字节),如果不对齐就在footers补充一些空字节. 但是强调的是如果是extra debugging 用CHUNK_OVERHEAD是16字节两个 size_t的大小,否则就是8个字节。
static mstate init_user_mstate(char* tbase, size_t tsize) {
size_t msize = pad_request(sizeof(struct malloc_state));
mchunkptr mn;
mchunkptr msp = align_as_chunk(tbase); //这个地址必须chunk 16字节对齐
mstate m = (mstate)(chunk2mem(msp));
memset(m, 0, msize);
(void)INITIAL_LOCK(&m->mutex);
msp->head = (msize|INUSE_BITS);
m->seg.base = m->least_addr = tbase;
m->seg.size = m->footprint = m->max_footprint = tsize;
m->magic = mparams.magic;
m->release_checks = MAX_RELEASE_CHECK_RATE;
m->mflags = mparams.default_mflags;
m->extp = 0;
m->exts = 0;
disable_contiguous(m);
init_bins(m);
mn = next_chunk(mem2chunk(m));
init_top(m, mn, (size_t)((tbase + tsize) - (char*)mn) - TOP_FOOT_SIZE);
check_top_chunk(m, m->top);
return m;
}
#define disable_contiguous(M) ((M)->mflags |= USE_NONCONTIGUOUS_BIT)
disable_contiguous(m);
上面初始化的时候可以申请不连续的空间,那么这样就可以使用mmap()。
dlmalloc 向系统申请内存有两种方式:一为 ORECORE(使用函数 sbrk())方式; 一为 MMAP(使用函数 mmap())方式。由于 MMAP 方式一般都是取到不连续的内存 空间,因此只有在某些情况下(见下面)才使用它。
如果设置允许调用 mmap 函数并且字节数 nb 已超过了预设的可以调用 mmap 界限 则利用 mmap 函数向系统申请内存。
if (use_mmap(m) && nb >= mparams.mmap_threshold) {
关于sbrk() 改变的 program break 位置在堆的增长方向处,可以得到连续地址,原理可以看下图:
参考:https://blog.csdn.net/sgbfblog/article/details/7772153
init_bins(m);
初始化 smallbins 分箱。dlmalloc程序使用smallbins数组(后面我们就简单的称这种分箱为“smallbins分箱”)来记 录这 32个双向环形链表表头, 该字段定义在结构体malloc_state内。
这个地方说明一下,对于大小在256字节以下的chunk块是通过malloc_chunk组织管理的,256字节 以下的chunk块一共有256/8=32类,8字节(最小是16字节这个不用),、16字节、24字节、32字节,……,256字节,因此dlmalloc维护了32个双向环形链表。
为什chunk块大小是以2为底的n(n=1,2,3 ……)次幂呢?
/* Initialize bins for a new mstate that is otherwise zeroed out */
static void init_bins(mstate m) {
/* Establish circular links for smallbins */
bindex_t i;
for (i = 0; i < NSMALLBINS; ++i) {
sbinptr bin = smallbin_at(m,i);
bin->fd = bin->bk = bin;
}
}
现在我们看看内存初始化的之后的处理:
void *
clib_mem_init (void *memory, uword memory_size)
{
u8 *heap;
if (memory)
{
heap = create_mspace_with_base (memory, memory_size, 1 /* locked */ );
mspace_disable_expand (heap);
}
else
heap = create_mspace (memory_size, 1 /* locked */ );//我们在 “init_user_mstate示意图”中看到了他的处理。
clib_mem_set_heap (heap);//调用 clib_mem_set_per_cpu_heap
if (mheap_trace_main.lock == 0)
clib_spinlock_init (&mheap_trace_main.lock);
return heap;
}
难道每个核心对应一个内存堆? (看设计是这样)初始化到现在对应一个CPU 0,heap的数组:clib_per_cpu_mheaps
70│ always_inline void *
71│ clib_mem_set_per_cpu_heap (u8 * new_heap)
72│ {
73│ int cpu = os_get_thread_index (); //0
74├> void *old = clib_per_cpu_mheaps[cpu];
75│ clib_per_cpu_mheaps[cpu] = new_heap;
76│ return old;
77│ }
mspace_get_aligned (msp=0x7fffb342f010, n_user_data_bytes=64, align=64, align_offset=0) at /mnt/vdb1/vpp/src/vppinfra/dlmalloc.c:4174
(gdb) bt
#0 mspace_get_aligned (msp=0x7fffb342f010, n_user_data_bytes=64, align=64, align_offset=0) at /mnt/vdb1/vpp/src/vppinfra/dlmalloc.c:4174
#1 0x00007ffff5907873 in clib_mem_alloc_aligned_at_offset (size=64, align=64, align_offset=0, os_out_of_memory_on_failure=1) at /mnt/vdb1/vpp/src/vppinfra/mem.h:118
#2 0x00007ffff59078c9 in clib_mem_alloc_aligned (size=64, align=64) at /mnt/vdb1/vpp/src/vppinfra/mem.h:142
#3 0x00007ffff5907c64 in clib_spinlock_init (p=0x7ffff5b2a100 ) at /mnt/vdb1/vpp/src/vppinfra/lock.h:59
#4 0x00007ffff5908b14 in clib_mem_init (memory=0x0, memory_size=1073741824) at /mnt/vdb1/vpp/src/vppinfra/mem_dlmalloc.c:220
#5 0x00007ffff5908b3d in clib_mem_init_thread_safe (memory=0x0, memory_size=1073741824) at /mnt/vdb1/vpp/src/vppinfra/mem_dlmalloc.c:228
#6 0x0000000000406f21 in main (argc=19, argv=0x6ff740) at /mnt/vdb1/vpp/src/vpp/vnet/main.c:276
引用到的完档:
https://blog.csdn.net/vector03/article/details/40977679
https://blog.csdn.net/vector03/article/details/40979181