Bootstrap

九浅一深Jemalloc5.3.0 -- ①深*一撸到底boot>malloc>gc

目前市面上有不少分析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函数走完了,还不是最终形态)。

bbff16db180547d2abda6d1faae658fd.png

今天我们不读代码,而是直接从内存(主要是emap)内容反推剩余的boot过程,并结合各个变量的值对原图修修补补。

OK,开始吧,我们先让程序运行到main,从arena_emap_global入手看看有哪些edata_t, extent(这些都是jemalloc里最重要的数据结构).

注意:到main后要s一下进入malloc内部,不然得不到变量arena_emap_global.

e17f7c482e7c4774857fa5a975f3526b.png

4319a4315738443a8b7d5dc04d129ac0.png

1201461a0cc3407ebe004a43216b91b6.png

0ec0cd5ec96d458a83f28320d9cecbd0.png

39b195b6fbd84f6dac178afa7c1a4573.png

ea5a5f80d98d43fe8af855164be38828.png

去掉重复的,有三个不同的edata_t, 看下他们对应的extent:

ab1ac9c31ade4aae9bec98c5a2dae99c.png

 三个extent是在同一块内存分割出来的(因为e_addr+size正好是下一个e_addr, size加起来正好是2M),实际上后两个是由第一张图里的trail分裂得来的(extent split,为了 malloc (size=72704),对齐后实际分配86016,为何分配72704字节内存?不好意思我也不知道,这是main之前的事情没研究过), 而且最后一个extent是闲置状态,这可以通过pa模块的内容来印证(看到edata_t 0x7ffff7617980没?):

b46cd363542c46bd8580f2b3c6fedd8c.png

 顺便啰嗦一句malloc (size=72704)的调用栈:

86bd1abb9eed4024bb65ae62cde7c953.png

Okay, 多出来一个extent/edata_t, 让我们修改下第一张图:

e4581ce6f248496b9f35620a41515c1e.png

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,

6ba785c93cf84d1393c13fb79b32b54d.png

由旧的extent分裂而来

c9f26421be07470587c3ad73899459da.png

我们再从tcache.bin[1]这个角度看看它是否有100颗弹药,弹药是不是指向上面的extent内的某片region: 

74a5e36288b041acb7dc8884dcbbec8c.png

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。

f2533cb135d8431bbcac5f9922205aad.png

nflush=75, 意味着还75个回去。 走过tcache_gc_small后观察tcache.bin[1]:

5d8cea5dea15435a995ee3d3b34c1870.png

还剩24发弹药。

看low_bits_empty/full的值依然是0xc88, 与gc前没变化,容易让人误以为是释放的stack_head开始向下的弹药,其实还是从 empty向上,只不过释放完下面的弹药后又把上面的弹药下移了。

下移代码:

0867590e4eda4f3cb292ad768c80f076.png

看内存验证上面的代码:

b8edcd6bf9c843dcb39c5077498a1256.png

 stack_head位置变了,但还是指向原来的extent的第二片region(0x7ffff741d010).

最后

今天太多计算,希望屏幕前的你还没晕。gc没gc不要紧,保重龙体要紧!

;