memcache(三)內存管理

memcached(三)內存管理

memcached使用預申請的方式來管理內存的分配,從而避免內存碎片化的問題。若是採用mallo和free來動態的申請和銷燬內存,必然會產生大量的內存碎片。算法

基本知識

slab:內存塊是memcached一次申請內存的最小單元,在memcached中一個slab的默認大小爲1M;數組

slabclass:特定大小的chunk的組。緩存

chunk:緩存的內存空間,一個slab被劃分爲若干個chunk;less

item:存儲數據的最小單元,每個chunk都會包含一個item;memcached

factor:增加因子,默認爲1.25,相鄰slab中的item大小與factor成比例關係;函數

基本原理

memcached使用預分配方法,避免頻繁的調用malloc和free;源碼分析

memcached經過不一樣的slab來管理不一樣chunk大小的內存塊,從而知足存儲不一樣大小的數據。ui

slab的申請是經過在使用item時申請slab大小的內存空間,而後再把內存切割爲大小相同的item,掛在到slab的未使用鏈表上。this

過時和被刪除item並不會被free掉,memcached並不會刪除已經分配的內存;spa

Memcached會優先使用已超時的記錄空間,經過LRU算法;

memcached使用lazy expiration來判斷元素是否過時,因此過時監視上不會佔用cpu時間。

源碼分析

下面主要分析memcached的內存申請和存儲相關代碼。

item

item是key/value的存儲單元。

typedef struct _stritem {
    struct _stritem *next;      /* 先後指針用於在鏈表slab->slots中鏈接先後數據 */ struct _stritem *prev;
    struct _stritem *h_next;    /* hash chain next */
    rel_time_t      time;       /* 最後一次訪問時間 */
    rel_time_t      exptime;    /* 過時時間 */
    int             nbytes;     /* 數據大小 */
    unsigned short  refcount;   /* 引用次數 */
    uint8_t         nsuffix;    /* suffix長度 */
    uint8_t         it_flags;   /* ITEM_* above */
    uint8_t         slabs_clsid;/* 全部slab的id */
    uint8_t         nkey;       /* key長度 */
    /* this odd type prevents type-punning issues when we do
     * the little shuffle to save space when not using CAS. */
    union {
        uint64_t cas;
        char end;
    } data[]; /* cas|key|suffix|value */
} item;

slab初始化

void slabs_init(const size_t limit, const double factor, const bool prealloc) {
    int i = POWER_SMALLEST - 1;
    unsigned int size = sizeof(item) + settings.chunk_size; /* 獲得每個item的大小 */

    mem_limit = limit;

    if (prealloc) { /* 預分配一塊內存 */
        ...
    }

    memset(slabclass, 0, sizeof(slabclass)); /* 把slabclass置爲0,slabclass是一個slab數組,存儲全部slab的信息 */

    while (++i < POWER_LARGEST && size <= settings.item_size_max / factor) {  /* 循環初始化每個slab的內容,保證slab中item的size小於max_size/factor */
        /* Make sure items are always n-byte aligned */
        if (size % CHUNK_ALIGN_BYTES)  /* 用於內存對齊 */
            size += CHUNK_ALIGN_BYTES - (size % CHUNK_ALIGN_BYTES);

        slabclass[i].size = size; /* 初始化slabclass中item的大小 */
        slabclass[i].perslab = settings.item_size_max / slabclass[i].size; /* 初始化每一個slab中item的數量 */
        size *= factor;  /* item的大小隨factor逐漸增大 */
        ...
    }
    /* 初始化最後一個slab,大小爲最大的max_size,只有一個item */
    power_largest = i;
    slabclass[power_largest].size = settings.item_size_max;
    slabclass[power_largest].perslab = 1;
    ...
}

從源碼中,能夠看出來同一個slab中全部的item的大小都是固定的,

申請slab內存

static void *do_slabs_alloc(const size_t size, unsigned int id) {
    slabclass_t *p;
    void *ret = NULL;
    item *it = NULL;

    if (id < POWER_SMALLEST || id > power_largest) { /* 判斷id是否合法 */
        MEMCACHED_SLABS_ALLOCATE_FAILED(size, 0);
        return NULL;
    }

    p = &slabclass[id]; /* 獲取slab */
    assert(p->sl_curr == 0 || ((item *)p->slots)->slabs_clsid == 0);

    /* fail unless we have space at the end of a recently allocated page,
       we have something on our freelist, or we could allocate a new page */
    if (! (p->sl_curr != 0 || do_slabs_newslab(id) != 0)) { /*若是sl_curr爲0,沒有剩餘的item,那麼就執行do_slabs_newslab申請內存空間*/
        /* We don't have more memory available */
        ret = NULL;
    } else if (p->sl_curr != 0) { /* 若是有未使用的空間,則獲取該item,並從slots鏈表中刪除該item */
        /* return off our freelist */
        it = (item *)p->slots;
        p->slots = it->next;
        if (it->next) it->next->prev = 0;
        p->sl_curr--;
        ret = (void *)it;
    }
    ...
    return ret;
}

sl_curr來判斷是否存在未使用的內容空間,若是不存在須要調用do_slabs_newslab來申請slab空間。

static int do_slabs_newslab(const unsigned int id) {
    slabclass_t *p = &slabclass[id];
    int len = settings.slab_reassign ? settings.item_size_max
        : p->size * p->perslab;
    char *ptr;
    /* 1. 判斷是否超過內存限制
         2. 判斷是否申請過內存空間
         3. 若是沒有申請過,則申請slab->size*slab->perslab大小的整塊內存
         4.若是申請過,調用grow_slab_list來擴大slab大小 */
    if ((mem_limit && mem_malloced + len > mem_limit && p->slabs > 0) || (grow_slab_list(id) == 0) || ((ptr = memory_allocate((size_t)len)) == 0)) { MEMCACHED_SLABS_SLABCLASS_ALLOCATE_FAILED(id); return 0; } memset(ptr, 0, (size_t)len); split_slab_page_into_freelist(ptr, id); /* 把申請的內存分配到slots鏈表中 */

    p->slab_list[p->slabs++] = ptr;
    mem_malloced += len;
    MEMCACHED_SLABS_SLABCLASS_ALLOCATE(id);

    return 1;
}

申請空間後,須要經過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;
    }
}

static void do_slabs_free(void *ptr, const size_t size, unsigned int id) {
    slabclass_t *p;
    item *it;
    ...
    p = &slabclass[id];
    /* 獲取內存指針,把item塊掛在到slots鏈表中,增長sl_curr */
    it = (item *)ptr;
    it->it_flags |= ITEM_SLABBED;
    it->prev = 0;
    it->next = p->slots;
    if (it->next) it->next->prev = it;
    p->slots = it;

    p->sl_curr++;
    p->requested -= size;
    return;
}

獲取適當大小的item

在do_item_alloc中,調用了slabs_clsid來獲取適合存儲當前元素的slab id。

unsigned int slabs_clsid(const size_t size) {
    int res = POWER_SMALLEST;

    if (size == 0)
        return 0;
    while (size > slabclass[res].size)    /* 遍歷slabclass來找到適合size的item */
        if (res++ == power_largest)     /* won't fit in the biggest slab */
            return 0;
    return res;
}

優缺點

內存預分配能夠避免內存碎片以及避免動態分配形成的開銷。

內存分配是由冗餘的,當一個slab不能被它所擁有的chunk大小整除時,slab尾部剩餘的空間就會被丟棄。

因爲分配的是特定長度的內存,所以沒法有效地利用全部分配的內存,例如若是將100字節的數據存儲在128字節的chunk中,會形成28字節的浪費。

相關文章
相關標籤/搜索