目前市面上有不少分析Jemalloc老版本的博文,但最新版本5.3.0却少之又少。而且5.3.0的架构与5之前的版本有较大不同,本着“与时俱进”、“由浅入深”的宗旨,我将逐步分析最新release版本Jemalloc5.3.0的实现。
另外,单讲实现代码是极其枯燥的,我将尽量每个原理知识点都用一个简简单单的小程序引出,这样便于大家测试和上手调试。另外,我还会用GDB打印数据结构、变量的值,方便理解当时的状态或算法。
本文用到了不少GDB技巧,欢迎订阅我的GDB专栏
《GDB调试技巧实战》强化调试技能哦。
从例子出发
一直在想,这个“①深”到底要深入哪块哪?后来想到多少理论都不见得比得上一个例子,今天我们就通过一个例子,把各个模块串起来(pa, emap, tcache, tsd, arena),从头撸到尾。首先打个预防针:今天主要看内存内容,有很多计算,不过看懂了之后对jemalloc5.3.0就有了基本的了解了。
例子如下:
//gcc test.c `jemalloc-config --libdir`/libjemalloc.a `jemalloc-config --libs` -g
#include <malloc.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char* argv[])
{
void* p1 = malloc(16);
void* p[300];
for(int i=1;i<300;i++)
p[i] = malloc(1024);
for(int i=1;i<300;i++)
free(p[i]);
return 0;
}
$ gcc test.c `jemalloc-config --libdir`/libjemalloc.a `jemalloc-config --libs` -g
$ MALLOC_CONF="tcache_gc_incr_bytes:4096" gdb ./a.out
1. boot
main以前是boot的过程,我直接把上次讲boot的图挪过来(但是这个图描述的只是主要boot函数走完了,还不是最终形态)。
今天我们不读代码,而是直接从内存(主要是emap)内容反推剩余的boot过程,并结合各个变量的值对原图修修补补。
OK,开始吧,我们先让程序运行到main,从arena_emap_global入手看看有哪些edata_t, extent(这些都是jemalloc里最重要的数据结构).
注意:到main后要s一下进入malloc内部,不然得不到变量arena_emap_global.
去掉重复的,有三个不同的edata_t, 看下他们对应的extent:
三个extent是在同一块内存分割出来的(因为e_addr+size正好是下一个e_addr, size加起来正好是2M),实际上后两个是由第一张图里的trail分裂得来的(extent split,为了 malloc (size=72704),对齐后实际分配86016,为何分配72704字节内存?不好意思我也不知道,这是main之前的事情没研究过), 而且最后一个extent是闲置状态,这可以通过pa模块的内容来印证(看到edata_t 0x7ffff7617980没?):
顺便啰嗦一句malloc (size=72704)的调用栈:
Okay, 多出来一个extent/edata_t, 让我们修改下第一张图:
2. malloc
来到malloc(16)这一行,如果读者看过前面关于tcache的文章就能想到流程是这样的:
-》会优先从tcache.bin[1]中拿内存
-》但问题是它是空的,需要先填充100个(tcache_bin_info[1]>>cache_slow->lg_fill_div[1])
-》先分配一个4096大小的extent,分割成256片(4096/16=256)
-》取其中的100个给tcache.bin[1]填充弹药
-》取其中一个返回给用户
extent与edata_t总是成对出现的,edata_t是对extent的描述,所以需要给它两都分配内存空间,其中
1)、 sizeof(edata_t)=128较小,首先由pac.edata_cache(paring heap)取,如失败由base_alloc_edata分配,这正是我们例子对应的情况。
2)、而extent至少是PAGE大小,我们的例子中是4096字节,由pac.ecache_retained.eset中某个大的extent分裂而得(还记得上面有个free extent其大小为2M-32768-86016吗?就是它。)
我们直接运行过malloc(16)这一行,让我们一一验证上面的猜想:
先从用户得到的指针通过emap找到它所属的edata_t/extent,
由旧的extent分裂而来
我们再从tcache.bin[1]这个角度看看它是否有100颗弹药,弹药是不是指向上面的extent内的某片region:
3. gc
可以看到上面的tcache.bin[1]还剩99颗弹药(size=99*16), 而根据我们的例子这些空间不会被用到,因为malloc(16)后我们只malloc(1024),岂不白白浪费了99*16个字节?!
这就是gc存在的意义!
$ gcc test.c `jemalloc-config --libdir`/libjemalloc.a `jemalloc-config --libs` -g
$ MALLOC_CONF="tcache_gc_incr_bytes:4096" gdb ./a.out
我是这么运行程序的,tcache_gc_incr_bytes:4096就是为了尽快触发gc: 每隔4096个字节allocation就会触发一次tcache gc, 就会把low_bits_low_water到low_bits_empty之间的3/4 还给extent。
nflush=75, 意味着还75个回去。 走过tcache_gc_small后观察tcache.bin[1]:
还剩24发弹药。
看low_bits_empty/full的值依然是0xc88, 与gc前没变化,容易让人误以为是释放的stack_head开始向下的弹药,其实还是从 empty向上,只不过释放完下面的弹药后又把上面的弹药下移了。
下移代码:
看内存验证上面的代码:
stack_head位置变了,但还是指向原来的extent的第二片region(0x7ffff741d010).
最后
今天太多计算,希望屏幕前的你还没晕。gc没gc不要紧,保重龙体要紧!