Memcache-內存模型-源碼分析

memcached-version-1.4.25

介紹

memcache 使用了 Slab Allocator 的內存分配機制, 按照預先規定的大小, 將待分配的內存劃分不同的區域并分割成特定長度的塊,每個區域塊只存放相對應大小的數據,以達到解決內存碎片問題, 因為不斷的 malloc() 不同大小的內存塊會產生大量的內存碎片,所以 memcache 實現了自己的內存管理機制,下面就讓我們看一下 memcache 內部是如何實現內存管理并劃分不同長度的塊.

數據結構

存放 key-value 數據的結構體 item

typedef struct _stritem {
    struct _stritem *next;    /* next item */
    struct _stritem *prev;      /* prev item */
    struct _stritem *h_next;    /* hash chain next */
    rel_time_t      time;       /* least recent access */
    rel_time_t      exptime;    /* expire time */
    int             nbytes;     /* size of data */
    unsigned short  refcount;   /* 引用計數,只要有線程操作該item就會++1 */
    uint8_t         nsuffix;    /* length of flags-and-length string */
    uint8_t         it_flags;   /* ITEM_* above */
    uint8_t         slabs_clsid;/* which slab class we're in */
    uint8_t         nkey;       /* key length, w/terminating null and padding */
    /* 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[];
    /* if it_flags & ITEM_CAS we have 8 bytes CAS */
    /* then null-terminated key */
    /* then " flags length\r\n" (no terminating null) */
    /* then data with terminating \r\n (no terminating null; it's binary!) */
} item;

slabclass 是什么?

memcache 內存模型會對初始化申請的 (內存區域) 進行切分,會切分成不同大小的item區域,比如切分成三塊區域 item-24Byte -> item-48Byte -> item-96Byte 這樣在每個切分的區域,只保存對應大小的item、而slabclass數組就是記錄每個item區域的使用情況即詳情.

item 在對應大小的區域又是如何保存?

現在已經有對應大小的item區域了, 然后在該區域里面又會以 chunk 進行劃分,默認每個chunk為1M,就是先有 slabclass 然后在每個 slabclass 指向區域劃分chunk , 然后在chunk區域進行劃分item
例如:
slabclass[1] -> chunk_1 -> [item-24Byte、item-24Byte、item-24Byte] chunk_2 -> [item-24Byte、item-24Byte、item-24Byte]

記錄每個item區域使用情況的結構體 slabclass

#define MAX_NUMBER_OF_SLAB_CLASSES (63 + 1)   slabclass 數組大小 , 最多不超過 64 
static slabclass_t slabclass[MAX_NUMBER_OF_SLAB_CLASSES]; 

typedef struct {
    unsigned int size;      /* item區域大小 */
    
    unsigned int perslab;   /* 每個chunk下可以保存item數量 */

    void *slots;            /* 空閑的item */
    unsigned int sl_curr;   /* 空閑的item數量 */

    unsigned int slabs;     /* chunk指針數組數量 */
    
    void **slab_list;       /* chunk指針數組 */
    
    unsigned int list_size; /* 預申請chunk指針數組的數量 */

    size_t requested; /* The number of requested bytes */
} slabclass_t;

memcache 內存模型

memcache 內存模型

三個主要的配置參數:

  • settings.maxbytes 存放數據內存大小默認64M
  • settings.factor 增長因子 1.25
  • preallocate 是否預申請內存

增長因子factor是什么?

因為 memcache 會對內存進行劃分不同區域大小的塊,但是會默認一個最小存放數據區域塊大小 size = 80/Byte 而增長因子就是以最小區域塊為基礎,每次遞增的倍數,但是最大遞增不能超過 62 個且 size*factor < 1M,下面代碼會有說明,就是保證我們最多有 62 個不同大小的內存區域塊,每個區域塊都是 factor 倍數,且最后一個區域塊一定是 1M , 所以我們可以根據實際使用情況來調節增長因子大小

例:
按照默認 1.25 進行增長,一共初始化 43 個區域,且每個區域之間都是 1.25 倍數,倒數第二個區域乘于 1.25 一定小于 1M , 因為最后一個區域等于 1M,這也說明Memcache存放數據的最大為1M.

增長因子初始化內存區域大小

源碼實現

(一) slabs_init 初始化內存

void slabs_init(const size_t limit, const double factor, const bool prealloc) {
    int i = POWER_SMALLEST - 1; //#define POWER_SMALLEST 1
    
    //最小數據塊size
    //sizeof(item) 存放數據的結構體 = 32 
    //settings.chunk_size 默認存放物理數據大小 = 48
    //size = 48 + 32 = 80/Byte 
    unsigned int size = sizeof(item) + settings.chunk_size;
    
    //申請的內存總大小默認64M
    mem_limit = limit;
    
    //是否預申請一塊內存區域,并直接指向該內存區域
    if (prealloc) {
        /* Allocate everything in a big chunk with malloc */
        mem_base = malloc(mem_limit);
        if (mem_base != NULL) {
            mem_current = mem_base;
            mem_avail = mem_limit;
        } else {
            //.......
        }
    }
    
    //slabclass數組置空
    memset(slabclass, 0, sizeof(slabclass));
    
    //按照 size * factor 填充 slabclass 數組 
    //不能超過 MAX_NUMBER_OF_SLAB_CLASSES - 1 &&  保證 size * factor 不能大于 settings.item_size_max
    while (++i < MAX_NUMBER_OF_SLAB_CLASSES-1 && size <= settings.item_size_max / factor) {
        /* Make sure items are always n-byte aligned */
        if (size % CHUNK_ALIGN_BYTES) //8字節對其
            size += CHUNK_ALIGN_BYTES - (size % CHUNK_ALIGN_BYTES);
        
        //每個slabclass組可存放item的大小
        slabclass[i].size = size;
        //每個chunk下可以保存item數量
        slabclass[i].perslab = settings.item_size_max / slabclass[i].size;
        //乘與增長因子繼續填充
        size *= factor;
        //.....
    }

    //保存最后一個元素的索引位置
    power_largest = i;
    //保證slab組最后一個可存放的item大小為settings.item_size_max 也就是1M
    slabclass[power_largest].size = settings.item_size_max;
    slabclass[power_largest].perslab = 1;
    //.....
    
    //為測試提供的,模擬先占用多少內存
    /* for the test suite:  faking of how much we've already malloc'd */
    {
        char *t_initial_malloc = getenv("T_MEMD_INITIAL_MALLOC");
        if (t_initial_malloc) {
            mem_malloced = (size_t)atol(t_initial_malloc);
        }
    }
    
    //如果是預申請則按照每個 slabclass[i].size 區域大小去劃分
    //chunk_1 -> [item-24Byte、item-24Byte、item-24Byte]
    //chunk_1 -> [item-48Byte、item-48Byte、item-48Byte]
    if (prealloc) {
        slabs_preallocate(power_largest);
    }
}

(二) slabs_preallocate 對預申請的內存進行劃分

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

    //循環執行
    for (i = POWER_SMALLEST; i < MAX_NUMBER_OF_SLAB_CLASSES; i++) {
        // 判斷是否超出當前slabclass最大索引
        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);
        }
    }
}

(三) do_slabs_newslab 根據每個slabclass區域大小進行劃分

static int do_slabs_newslab(const unsigned int id) {
    
    slabclass_t *p = &slabclass[id]; //根據索引取出slabclass
    slabclass_t *g = &slabclass[SLAB_GLOBAL_PAGE_POOL];
    
    // 獲取待申請chunk大小,理論上每個 chunk <= 1M(1048576/Byte)
    // 但是有些情況 size * perslab 不會正好等于 1M 而是小于 1M
    // 那么我們按照1M申請就會有一些字節浪費掉.
    // 比如第一個slabclass的區域是 80/Byte 如果按每個chunk為1M 那么 perslab = 1M/80 = 13107/item 
    // 就是一個chunk里面會有13107個item , 但是 13107 * 80 = 1048560/Byte 小于 1M(1048576/Byte)
    // 所以這里的判斷就是按照什么方式去申請這chunk空間,如果不想有字節浪費掉就   p->size * p->perslab
    
    int len = settings.slab_reassign ? settings.item_size_max
        : p->size * p->perslab;
        
    char *ptr;
    
    // 判斷內存使用是否超過最大設定
    if ((mem_limit && mem_malloced + len > mem_limit && p->slabs > 0
         && g->slabs == 0)) {
        mem_limit_reached = true;
       MEMCACHED_SLABS_SLABCLASS_ALLOCATE_FAILED(id);
        return 0;
    }
    
    // grow_slab_list 獲取chunk指針數組,就是 void **slab_list 、 list_size
    // get_page_from_global_pool 忽略.
    // memory_allocate 申請一塊 chunk 區域,并更新內存使用量
    if ((grow_slab_list(id) == 0) ||
        (((ptr = get_page_from_global_pool()) == NULL) &&
        ((ptr = memory_allocate((size_t)len)) == 0))) {

        MEMCACHED_SLABS_SLABCLASS_ALLOCATE_FAILED(id);
        return 0;
    }
    
    // chunk指針初始化置空
    memset(ptr, 0, (size_t)len);
    
    // chunk區域有了,就在chunk中進行劃分item
    split_slab_page_into_freelist(ptr, id);
    
    // 保存當前chunk的指針, 并更新 p->slabs++ 
    p->slab_list[p->slabs++] = ptr;
    MEMCACHED_SLABS_SLABCLASS_ALLOCATE(id);

    return 1;
}

(四) grow_slab_list 獲取chunk指針數組,不存在則創建,存在且空間不夠則擴容

static int grow_slab_list (const unsigned int id) {
    slabclass_t *p = &slabclass[id];
    // 判斷當前 chunk指針數組索引 是否等于 list_size 如果等于就會進行擴容
    // 初始化情況會等于
    if (p->slabs == p->list_size) {
        // 默認 slab_list 數組大小 16 
        // 之后在擴充每次2的倍數進行擴容
        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;
        // 預申請 chunk 指針數組的數量
        p->list_size = new_size;
        // 指向該數組
        p->slab_list = new_list;
    }
   return 1;
}

(五) memory_allocate 申請一塊 chunk 區域 , 并更新內存使用量

static void *memory_allocate(size_t size) {
    void *ret;
    
    // 判斷是否為預申請模式,如果不是則每次 malloc 申請 1M
    if (mem_base == NULL) {
        /* We are not using a preallocated large memory chunk */
       ret = malloc(size);
    } else {

        //當前內存使用位置
        ret = mem_current;
        
        // size 不能大于最大的mem_avail內存塊
        if (size > mem_avail) {
            return NULL;
        }

        /* mem_current pointer _must_ be aligned!!! */
        if (size % CHUNK_ALIGN_BYTES) {
            size += CHUNK_ALIGN_BYTES - (size % CHUNK_ALIGN_BYTES);
        }
        
        // 獲取一塊size大小內存,并更新內存使用位置
        mem_current = ((char*)mem_current) + size;
        
        // 更新一下mem_avail,就是還剩多少內存
        if (size < mem_avail) {
            mem_avail -= size;
        } else {
           mem_avail = 0;
       }
    }
    
    //更新一下內存使用量, 就是已使用了多少內存 
    mem_malloced += size;
    
    // 返回當前申請的內存,也就是 chunk 區域 
    return ret;
}

(六) split_slab_page_into_freelist 根據給定的 chunk區域指針 進行劃分item

static void split_slab_page_into_freelist(char *ptr, const unsigned int id) {
    slabclass_t *p = &slabclass[id];
    int x;
    // 當前chunk區域共有多少 perslab 就是 item
    for (x = 0; x < p->perslab; x++) {
        // 一個一個進行劃分
        do_slabs_free(ptr, 0, id);
        ptr += p->size;
    }
}

(七) do_slabs_free 劃分item

static void do_slabs_free(void *ptr, const size_t size, unsigned int id) {
    slabclass_t *p;
    item *it;

    assert(id >= POWER_SMALLEST && id <= power_largest);
    if (id < POWER_SMALLEST || id > power_largest)
        return;

    MEMCACHED_SLABS_FREE(size, id, ptr);
    p = &slabclass[id];

    it = (item *)ptr; //強制轉換成item結構體指針
    it->it_flags = ITEM_SLABBED; 
    it->slabs_clsid = 0;
    // 每一個item都已雙向鏈表形式連接
    it->prev = 0;
    it->next = p->slots;
    if (it->next) it->next->prev = it;
    
    // slots 一直指向這個空閑item鏈表
    p->slots = it;
    
    // 更新一下當前可使用item數量
    p->sl_curr++;
    p->requested -= size;
    return;
}

結束

上面介紹的函數就是Memcache啟動的時候,初始化內存所涉及到的所有核心函數實現

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯系作者。

推薦閱讀更多精彩內容