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是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;
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的大小都是固定的,
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; }
在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字節的浪費。