全栈猎人
首发于全栈猎人
理解memcached源码 - Slab II

理解memcached源码 - Slab II

原文发表在:

https://holmeshe.me/understanding-memcached-source-code-II/holmeshe.me图标

Slab分配器是这个缓存系统的核心,并在很大程度上决定了核心资源 - 内存 - 的利用效率。其它的三个部分,

用来淘汰(超时)对象的LRU算法;和

基于lebevent的事件驱动;以及

用于分布数据的一致性哈希,

可以看作是围绕Slab来开发的。

这次我们继续看用于Slab的内存是如何分配的。

首先我们继续看 slabs_init 的两个实参。第一个是 settings.maxbytes - 控制这个memcached可以使用的总内存大小。在传入 slabs_init 之前,这个参数被赋值为全局变量 mem_limit

void slabs_init(const size_t limit, const double factor, const bool prealloc, const uint32_t *slab_sizes) {
...
    mem_limit = limit; // scr: here
...


static size_t mem_limit = 0;
...
  settings.maxbytes = 64 * 1024 * 1024; /* default is 64MB */
...
        case 'm':
            settings.maxbytes = ((size_t)atoi(optarg)) * 1024 * 1024;
            break;
...

另外一个怎是 preallocate 。它决定了是否为(各个)Slab组slab class预分配 内存。这个参数的值由 L 命令行参数来决定。

...
   bool preallocate = false;
...
        case 'L' :
            if (enable_large_pages() == 0) {
                preallocate = true;
            } else {
                fprintf(stderr, "Cannot enable large pages on this system\n"
                    "(There is no Linux support as of this version)\n");
                return 1;
            }
            break;
...

下面我们来看 slabs 的内存分配函数。

New slab

具体来说,这个函数用于给 Slab组 分配大小为1M的内存块(slab)。而 Slab组 由参数 id 指定。

static int do_slabs_newslab(const unsigned int id) {
    slabclass_t *p = &slabclass[id]; // scr: ----------------------------> 1)
    slabclass_t *g = &slabclass[SLAB_GLOBAL_PAGE_POOL]; // scr: ---------> *)
    int len = settings.slab_reassign ? settings.item_size_max // scr: ---> 2)
        : p->size * p->perslab;
    char *ptr;

    if ((mem_limit && mem_malloced + len > mem_limit && p->slabs > 0 // -> 3)
         && g->slabs == 0)) {
        mem_limit_reached = true;
        MEMCACHED_SLABS_SLABCLASS_ALLOCATE_FAILED(id);
        return 0;
    }

    if ((grow_slab_list(id) == 0) || // scr: ----------------------------> 4)
        (((ptr = get_page_from_global_pool()) == NULL) && // scr: -------> *)
        ((ptr = memory_allocate((size_t)len)) == 0))) { // scr: ---------> 5)

        MEMCACHED_SLABS_SLABCLASS_ALLOCATE_FAILED(id);
        return 0;
    }

    memset(ptr, 0, (size_t)len);
    split_slab_page_into_freelist(ptr, id); // scr: ---------------------> 6)

    p->slab_list[p->slabs++] = ptr; // scr: -----------------------------> 7)
    MEMCACHED_SLABS_SLABCLASS_ALLOCATE(id);

    return 1;
}

1)slabclass[id]Slab组 的数据结构。上篇讨论了这个数组的初始化。

2)settings.slab_reassign 决定是否启用 再平衡 策略。如果启用,未使用的 slabs 不会被立即释放,而是分配给其他 Slab组 使用,这就产生了一个问题,即所有 Slab组 都需要使用统一大小的 slab。所以这个设置同时也决定了是否使用 同种 slab (大小为 settings.item_size_max,或者上述的1M),还是 异种slabp->size * p->perslab)。除了用命令行参数“slab_reassign”以外,“modern”也会设置这个值,而本文也会用1M作为 slab 的大小。

3)检查内存使用是否超出上线。

4)grow_slab_list 检查是否增长 slabclass_t.slab_list,如果需要,则增长之。

static int grow_slab_list (const unsigned int id) {
    slabclass_t *p = &slabclass[id];
    if (p->slabs == p->list_size) {
        size_t new_size =  (p->list_size != 0) ? p->list_size * 2 : 16;
        void *new_list = realloc(p->slab_list, new_size * sizeof(void *));
        if (new_list == 0) return 0;
        p->list_size = new_size;
        p->slab_list = new_list;
    }
    return 1;
}

5)memory_allocate 是真正分配 slab 内存的函数。如上述,这里的 len 是1M。

6)split_slab_page_into_freelist 初始化 (或者是 free)刚刚分配的 slab 内存用作对象存储。这个函数会在下一节讨论。

7)将刚刚分配的 slab 加入到 slabclass_t.slab_list

下图总结了这个过程(我们想象 do_slabs_newslab(n) 被调用了两次)

接下来我们来看在第6)步中一块 slab 是如何被初始化的。

split_slab_page_into_freelist

static void split_slab_page_into_freelist(char *ptr, const unsigned int id) {
    slabclass_t *p = &slabclass[id];
    int x;
    for (x = 0; x < p->perslab; x++) {
        do_slabs_free(ptr, 0, id);
        ptr += p->size;
    }
}

这个函数会遍历 slab 里的所有 item 块(slabclass_t.size),然后调用 do_slabs_free 来初始化每个 item 块的元数据。换一个说法,就是 “拆分 slab待分配列表”-“split a slab into item free list”。你也许已经猜到了,这个 待分配列表 会被直接用于 对象分配 ,这个过程后面会详细讨论。

do_slabs_free

static void do_slabs_free(void *ptr, const size_t size, unsigned int id) {
    slabclass_t *p;
    item *it;
...
    p = &slabclass[id];

    it = (item *)ptr;
    it->it_flags = ITEM_SLABBED; // scr: ---------------> 1)
    it->slabs_clsid = 0;
    it->prev = 0; // scr: ------------------------------> 2)
    it->next = p->slots;
    if (it->next) it->next->prev = it;
    p->slots = it;

    p->sl_curr++; // scr: ------------------------------> 3)
    p->requested -= size;
    return;
}

技术上来说,这个函数处理的元数据存在于每个 item 块的开始。

1)初始化一些域。这里 item 是另一个核心数据结构,后续会讨论。

2)将 item 加入到上述的 待分配列表 ,并且更新链表表头,slabclass_t.slots

3)更新可分配项目数量,slabclass_t.sl_curr;并且更新 slabclass_t.requested 负责统计。注意这里并没有真正的释放对象,所以传入的size 是0。

Slab 预分配

下面我们来看 do_slabs_newslab 怎么使用。其中一个地方是之前看到过的 slabs_initpreallocate 设置为 true),

 void slabs_init(const size_t limit, const double factor, const bool prealloc, const uint32_t *slab_sizes) {
...
    if (prealloc) {
        slabs_preallocate(power_largest);
    }
}


static void slabs_preallocate (const unsigned int maxslabs) {
    int i;
    unsigned int prealloc = 0;

    /* pre-allocate a 1MB slab in every size class so people don't get
       confused by non-intuitive "SERVER_ERROR out of memory"
       messages.  this is the most common question on the mailing
       list.  if you really don't want this, you can rebuild without
       these three lines.  */

    for (i = POWER_SMALLEST /* scr: 1 */; i < MAX_NUMBER_OF_SLAB_CLASSES; i++) {
        if (++prealloc > maxslabs)
            return;
        if (do_slabs_newslab(i) == 0) {
            fprintf(stderr, "Error while preallocating slab memory!\n"
                "If using -L or other prealloc options, max memory must be "
                "at least %d megabytes.\n", power_largest);
            exit(1);
        }
    }

}

这个方法从POWER_SMALLEST (1)开始遍历所有的 slabclass ,然后给每个 Slab组预分配一个 slab。(下标为0的 Slab组 是一个特殊的组,存储空闲的 slab 用于上面提到的 再平衡 策略)。

References

上文一样。

编辑于 2018-12-10

文章被以下专栏收录