目前市面上有不少分析Jemalloc老版本的博文,但最新版本5.3.0却少之又少。而且5.3.0的架构与5之前的版本有较大不同,本着“与时俱进”、“由浅入深”的宗旨,我将逐步分析最新release版本Jemalloc5.3.0的实现。
另外,单讲实现代码是极其枯燥的,我将尽量每个原理知识点都用一个简简单单的小程序引出,这样便于大家测试和上手调试。另外,我还会用GDB打印数据结构、变量的值,方便理解当时的状态或算法。
记得有句名言,大概意思是“在软件工程中,没有一个中间层解决不了的问题”,jemalloc 5.3.0深彻贯彻了这一思想,当你执行malloc向jemalloc索要内存时,后台可能经历了无数次“托人找关系”:首先问tcache能不能办成,tcache可能办不成但他不说自己办不成,悄悄问arena bin能不能办成,之后又拖到了extent, pa, OS, 一路下来OS作为最后一层一般情况下(除非内存耗尽)都能办成。这基本就是jemalloc5.3.0的架构了。
上一篇我们讲到了如何从tcache.bin中拿数据(假设它有数据---16个字节的内存, jemalloc叫它region。比如4096字节大小的slab/extent里就有256个16字节大小的region,有128个32字节大小的region)。
这一篇浅挖一挖,看看tcache.bin是如何被填充的(fill)? 也就是上图绿色部分。
为了避免涉及到后续的中间层比如pa/OS, 先设计一个简单的小例子:
//gcc fill_tcache_bin.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[])
{
for(int i=0;i<100;i++)
malloc(10);
void* p100=malloc(10);
return 0;
}
说明:之所以先分配100次10个字节(实际分配16字节)就是要耗尽tcache.bin[1]中缓存的100颗子弹,至于为什么以及怎样缓存了100颗,这正是今天的课题。
于第11行 p100=malloc(10)处下断点。
之后b tcache_alloc_small,
因为子弹耗尽所以52行ret=0, tcache分配失败,要走 tcache_alloc_small_hard
(注意其命名方式,hard表明要向下层要数据了)
第一个问题是 fill多少个子弹?下面这块代码会告诉你。
tcache_slow->lg_fill_div[binind]的值会随着系统繁忙与否变化,不过初始值为1. lg可能是logarithm的缩写,表示以2为底的对数。
第二个问题是具体怎么fill的?
笼统来说也简单,arena.bin[1] 有两个重要数据成员:slabcur(edata_t*类型), slabs_nonfull(edata_heap_t类型,是一个pairing heap),
① 如果slabcur有100个空闲子弹(16bytes内存)则把它们的地址一一存入到tcache.bin[1](上面高亮行中的参数cache_bin,所以tcache.bin[1]至少要能容纳100个指针,实际它能容纳200个即tcache_bin_info[1].ncached_max个指针)。
② 如果slabcur不够100个则继续从slabs_nonfull中找空闲块。
源代码中也有注释:
看下面的图也许更直观一些。
注意:
- 红色箭头,这是填充后的示意。表示填充过程的话,应该把方向倒过来。大家明白意思即可。
- 在我们的例子中是用不到slabs_nonfull的,因为slabcur还有156个空闲内存块可用(初始值256-malloc循环100次用掉100个, 256=4096/16)
- slabs_nonfull为The Pairing Heap: A New Form of Self-Adjusting Heap, 文档地址 https://www.cs.cmu.edu/~sleator/papers/pairing-heaps.pdf
如果此后再free几个,stack_head应该会回退到0-100之间。这也是为何只fill一半的原因,为free留了一些空间。36个small size class对应的tcache.bin都是只fill一半: