memcached源碼分析-----slab內存分配器
溫馨提示:本文用到了一些可以在啟動memcached設置的全局變量。關於這些全局變量的含義可以參考《memcached啟動參數詳解》。對於這些全局變量,處理方式就像《如何閱讀memcached源代碼》所說的那樣直接取其默認值。
slab內存池分配器:
slab簡介:
memcached使用了一個叫slab的內存分配方法,有關slab的介紹可以參考鏈接1和鏈接2。可以簡單地把它看作內存池。memcached內存池分配的內存塊大小是固定的。雖然是固定大小,但memcached的能分配的內存大小(尺寸)也是有很多種規格的。一般來說,是滿足需求的。
memcached聲明了一個slabclass_t結構體類型,並且定義了一個slabclass_t類型數組slabclass(是一個全局變量)。可以把數組的每一個元素稱為一個slab分配器。一個slab分配器能分配的內存大小是固定的,不同的slab分配的內存大小是不同的。下面借一幅經典的圖來說明:
從每個slab class(slab分配器)分配出去的內存塊都會用指針連接起來的(連起來才不會丟失啊)。如下圖所示:
上圖是一個邏輯圖。每一個item都不大,從幾B到1M。如果每一個item都是地動態調用malloc申請的,勢必會造成很多內存碎片。所以memcached的做法是,先申請一個比較大的一塊內存,然後把這塊內存劃分成一個個的item,並用兩個指針(prev和next)把這些item連接起來。所以實際的物理圖如下所示:
上圖中,每一個slabclass_t都有一個slab數組。同一個slabclass_t的多個slab分配的內存大小是相同的,不同的slabclass_t分配的內存大小是不同的。因為每一個slab分配器能分配出去的總內存都是有一個上限的,所以對於一個slabclass_t來說,要想分配很多內存就必須有多個slab分配器。
確定slab分配器的分配規格:
看完了圖,現在來看一下memcached是怎麽確定slab分配器的分配規格的。因為memcached使用了全局變量,先來看一下全局變量。
//slabs.c文件 typedef struct { unsigned int size;//slab分配器分配的item的大小 unsigned int perslab; //每一個slab分配器能分配多少個item void *slots; //指向空閑item鏈表 unsigned int sl_curr; //空閑item的個數 //這個是已經分配了內存的slabs個數。list_size是這個slabs數組(slab_list)的大小 unsigned int slabs; //本slabclass_t可用的slab分配器個數 //slab數組,數組的每一個元素就是一個slab分配器,這些分配器都分配相同尺寸的內存 void **slab_list; unsigned int list_size; //slab數組的大小, list_size >= slabs //用於reassign,指明slabclass_t中的哪個塊內存要被其他slabclass_t使用 unsigned int killing; size_t requested; //本slabclass_t分配出去的字節數 } slabclass_t; #define POWER_SMALLEST 1 #define POWER_LARGEST 200 #define CHUNK_ALIGN_BYTES 8 #define MAX_NUMBER_OF_SLAB_CLASSES (POWER_LARGEST + 1) //數組元素雖然有MAX_NUMBER_OF_SLAB_CLASSES個,但實際上並不是全部都使用的。 //實際使用的元素個數由power_largest指明 static slabclass_t slabclass[MAX_NUMBER_OF_SLAB_CLASSES];//201 static int power_largest;//slabclass數組中,已經使用了的元素個數.
可以看到,上面的代碼定義了一個全局slabclass數組。這個數組就是前面那些圖的slabclass_t數組。雖然slabclass數組有201個元素,但可能並不會所有元素都使用的。由全局變量power_largest指明使用了多少個元素.下面看一下slabs_init函數,該函數對這個數組進行一些初始化操作。該函數會在main函數中被調用。
//slabs.c文件 static size_t mem_limit = 0;//用戶設置的內存最大限制 static size_t mem_malloced = 0; //如果程序要求預先分配內存,而不是到了需要的時候才分配內存,那麽 //mem_base就指向那塊預先分配的內存. //mem_current指向還可以使用的內存的開始位置 //mem_avail指明還有多少內存是可以使用的 static void *mem_base = NULL; static void *mem_current = NULL; static size_t mem_avail = 0; //參數factor是擴容因子,默認值是1.25 void slabs_init(const size_t limit, const double factor, const bool prealloc) { int i = POWER_SMALLEST - 1; //settings.chunk_size默認值為48,可以在啟動memcached的時候通過-n選項設置 //size由兩部分組成: item結構體本身 和 這個item對應的數據 //這裏的數據也就是set、add命令中的那個數據.後面的循環可以看到這個size變量會 //根據擴容因子factor慢慢擴大,所以能存儲的數據長度也會變大的 unsigned int size = sizeof(item) + settings.chunk_size; mem_limit = limit;//用戶設置或者默認的內存最大限制 //用戶要求預分配一大塊的內存,以後需要內存,就向這塊內存申請。 if (prealloc) {//默認值為false mem_base = malloc(mem_limit); if (mem_base != NULL) { mem_current = mem_base; mem_avail = mem_limit; } else { fprintf(stderr, "Warning: Failed to allocate requested memory in" " one large chunk.\nWill allocate in smaller chunks\n"); } } //初始化數組,這個操作很重要,數組中所有元素的成員變量值都為0了 memset(slabclass, 0, sizeof(slabclass)); //slabclass數組中的第一個元素並不使用 //settings.item_size_max是memcached支持的最大item尺寸,默認為1M(也就是網上 //所說的memcached存儲的數據最大為1MB)。 while (++i < POWER_LARGEST && 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的slab分配器能分配的item大小 slabclass[i].size = size; //這個slabclass的slab分配器最多能分配多少個item(也決定了最多分配多少內存) slabclass[i].perslab = settings.item_size_max / slabclass[i].size; size *= factor;//擴容 } //最大的item power_largest = i; slabclass[power_largest].size = settings.item_size_max; slabclass[power_largest].perslab = 1; ... if (prealloc) {//預分配內存 slabs_preallocate(power_largest); } }
上面代碼中出現的item是用來存儲我們放在memcached的數據。代碼中的循環決定了slabclass數組中的每一個slabclass_t能分配的item大小,也就是slab分配器能分配的item大小,同時也確定了slab分配器能分配的item個數。
上面的代碼還可以看到,可以通過增大settings.item_size_max而使得memcached可以存儲更大的一條數據信息。當然是有限制的,最大也只能為128MB。巧的是,slab分配器能分配的最大內存也是受這個settings.item_size_max所限制。因為每一個slab分配器能分配的最大內存有上限,所以slabclass數組中的每一個slabclass_t都有多個slab分配器,其用一個數組管理這些slab分配器。而這個數組大小是不受限制的,所以對於某個特定的尺寸的item是可以有很多很多的。當然整個memcached能分配的總內存大小也是有限制的,可以在啟動memcached的時候通過-m選項設置,默認值為64MB。slabs_init函數中的limit參數就是memcached能分配的總內存。
預分配內存:
現在就假設用戶需要預先分配一些內存,而不是等到客戶端發送存儲數據命令的時候才分配內存。slabs_preallocate函數是為slabclass數組中每一個slabclass_t元素預先分配一些空閑的item。由於item可能比較小(上面的代碼也可以看到這一點),所以不能以item為單位申請內存(這樣很容易造成內存碎片)。於是在申請的使用就申請一個比較大的一塊內存,然後把這塊內存劃分成一個個的item,這樣就等於申請了多個item。本文將申請得到的這塊內存稱為內存頁,也就是申請了一個頁。如果全局變量settings.slab_reassign為真,那麽頁的大小為settings.item_size_max,否則等於slabclass_t.size * slabclass_t.perslab。settings.slab_reassign主要用於平衡各個slabclass_t的。後文將統一使用內存頁、頁大小稱呼這塊分配內存,不區分其大小。
現在就假設用戶需要預先分配內存,看一下slabs_preallocate函數。該函數的參數值為使用到的slabclass數組元素個數。slabs_preallocate函數的調用是分配slab內存塊和和設置item的。
//參數值為使用到的slabclass數組元素個數 //為slabclass數組的每一個元素(使用到的元素)分配內存 static void slabs_preallocate (const unsigned int maxslabs) { int i; unsigned int prealloc = 0; //遍歷slabclass數組 for (i = POWER_SMALLEST; i <= POWER_LARGEST; i++) { if (++prealloc > maxslabs)//當然是只遍歷使用了的數組元素 return; if (do_slabs_newslab(i) == 0) {//為每一個slabclass_t分配一個內存頁 //如果分配失敗,將退出程序.因為這個預分配的內存是後面程序運行的基礎 //如果這裏分配失敗了,後面的代碼無從執行。所以就直接退出程序。 exit(1); } } } //slabclass_t中slab的數目是慢慢增多的。該函數的作用就是為slabclass_t申請多一個slab //參數id指明是slabclass數組中的那個slabclass_t static int do_slabs_newslab(const unsigned int id) { slabclass_t *p = &slabclass[id]; //settings.slab_reassign的默認值為false,這裏就采用false。 int len = settings.slab_reassign ? settings.item_size_max : p->size * p->perslab;//其積 <= settings.item_size_max char *ptr; //mem_malloced的值通過環境變量設置,默認為0 if ((mem_limit && mem_malloced + len > mem_limit && p->slabs > 0) || (grow_slab_list(id) == 0) ||//增長slab_list(失敗返回0)。一般都會成功,除非無法分配內存 ((ptr = memory_allocate((size_t)len)) == 0)) {//分配len字節內存(也就是一個頁) return 0; } memset(ptr, 0, (size_t)len);//清零內存塊是必須的 //將這塊內存切成一個個的item,當然item的大小有id所控制 split_slab_page_into_freelist(ptr, id); //將分配得到的內存頁交由slab_list掌管 p->slab_list[p->slabs++] = ptr; mem_malloced += len; return 1; }
上面的do_slabs_newslab函數內部調用了三個函數。函數grow_slab_list的作用是增大slab數組的大小(如下圖所示的slab數組)。memory_allocate函數則是負責申請大小為len字節的內存。而函數split_slab_page_into_freelist則負責把申請到的內存切分成多個item,並且把這些item用指向連起來,形成雙向鏈表。如下圖所示:前面已經見過這圖了,看完代碼再來看一下吧。
下面看一下那三個函數的具體實現。
//增加slab_list成員指向的內存,也就是增大slab_list數組。使得可以有更多的slab分配器 //除非內存分配失敗,否則都是返回1,無論是否真正增大了 static int grow_slab_list (const unsigned int id) { slabclass_t *p = &slabclass[id]; if (p->slabs == p->list_size) {//用完了之前申請到的slab_list數組的所有元素 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; } //申請分配內存,如果程序是有預分配內存塊的,就向預分配內存塊申請內存 //否則調用malloc分配內存 static void *memory_allocate(size_t size) { void *ret; //如果程序要求預先分配內存,而不是到了需要的時候才分配內存,那麽 //mem_base就指向那塊預先分配的內存. //mem_current指向還可以使用的內存的開始位置 //mem_avail指明還有多少內存是可以使用的 if (mem_base == NULL) {//不是預分配內存 /* We are not using a preallocated large memory chunk */ ret = malloc(size); } else { ret = mem_current; //在字節對齊中,最後幾個用於對齊的字節本身就是沒有意義的(沒有被使用起來) //所以這裏是先計算size是否比可用的內存大,然後才計算對齊 if (size > mem_avail) {//沒有足夠的可用內存 return NULL; } //現在考慮對齊問題,如果對齊後size 比mem_avail大也是無所謂的 //因為最後幾個用於對齊的字節不會真正使用 /* mem_current pointer _must_ be aligned!!! */ if (size % CHUNK_ALIGN_BYTES) {//字節對齊.保證size是CHUNK_ALIGN_BYTES (8)的倍數 size += CHUNK_ALIGN_BYTES - (size % CHUNK_ALIGN_BYTES); } mem_current = ((char*)mem_current) + size; if (size < mem_avail) { mem_avail -= size; } else {//此時,size比mem_avail大也無所謂 mem_avail = 0; } } return ret; } //將ptr指向的內存頁劃分成一個個的item 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++) { //將ptr指向的內存劃分成一個個的item.一共劃成perslab個 //並將這些item前後連起來。 //do_slabs_free函數本來是worker線程向內存池歸還內存時調用的。但在這裏 //新申請的內存也可以當作是向內存池歸還內存。把內存註入內存池中 do_slabs_free(ptr, 0, id); ptr += p->size;//size是item的大小 } } static void do_slabs_free(void *ptr, const size_t size, unsigned int id) { slabclass_t *p; item *it; assert(((item *)ptr)->slabs_clsid == 0); assert(id >= POWER_SMALLEST && id <= power_largest); if (id < POWER_SMALLEST || id > power_largest) return; p = &slabclass[id]; it = (item *)ptr; //為item的it_flags添加ITEM_SLABBED屬性,標明這個item是在slab中沒有被分配出去 it->it_flags |= ITEM_SLABBED; //由split_slab_page_into_freelist調用時,下面4行的作用是 //讓這些item的prev和next相互指向,把這些item連起來. //當本函數是在worker線程向內存池歸還內存時調用,那麽下面4行的作用是, //使用鏈表頭插法把該item插入到空閑item鏈表中。 it->prev = 0; it->next = p->slots; if (it->next) it->next->prev = it; p->slots = it;//slot變量指向第一個空閑可以使用的item p->sl_curr++;//空閑可以使用的item數量 p->requested -= size;//減少這個slabclass_t分配出去的字節數 return; }
在do_slabs_free函數的註釋說到,在worker線程向內存池歸還內存時,該函數也是會被調用的。因為同一slab內存塊中的各個item歸還時間不同,所以memcached運行一段時間後,item鏈表就會變得很混亂,不會像上面那個圖那樣。有可能如下圖那樣:
雖然混亂,但肯定還是會有前面那張邏輯圖那樣的清晰鏈表圖,其中slots變量指向第一個空閑的item。
向內存池申請內存:
與do_slabs_free函數對應的是do_slabs_alloc函數。當worker線程向內存池申請內存時就會調用該函數。在調用之前就要根據所申請的內存大小,確定好要向slabclass數組的哪個元素申請內存了。函數slabs_clsid就是完成這個任務。
unsigned int slabs_clsid(const size_t size) {//返回slabclass索引下標值 int res = POWER_SMALLEST;//res的初始值為1 //返回0表示查找失敗,因為slabclass數組中,第一個元素是沒有使用的 if (size == 0) return 0; //因為slabclass數組中各個元素能分配的item大小是升序的 //所以從小到大直接判斷即可在數組找到最小但又能滿足的元素 while (size > slabclass[res].size) if (res++ == power_largest) /* won‘t fit in the biggest slab */ return 0; return res; }
在do_slabs_alloc函數中如果對應的slabclass_t有空閑的item,那麽就直接將之分配出去。否則就需要擴充slab得到一些空閑的item然後分配出去。代碼如下面所示:
//向slabclass申請一個item。在調用該函數之前,已經調用slabs_clsid函數確定 //本次申請是向哪個slabclass_t申請item了,參數id就是指明是向哪個slabclass_t //申請item。如果該slabclass_t是有空閑item,那麽就從空閑的item隊列中分配一個 //如果沒有空閑item,那麽就申請一個內存頁。再從新申請的頁中分配一個item //返回值為得到的item,如果沒有內存了,返回NULL 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) {//下標越界 MEMCACHED_SLABS_ALLOCATE_FAILED(size, 0); return NULL; } p = &slabclass[id]; assert(p->sl_curr == 0 || ((item *)p->slots)->slabs_clsid == 0); //如果p->sl_curr等於0,就說明該slabclass_t沒有空閑的item了。 //此時需要調用do_slabs_newslab申請一個內存頁 if (! (p->sl_curr != 0 || do_slabs_newslab(id) != 0)) { //當p->sl_curr等於0並且do_slabs_newslab的返回值等於0時,進入這裏 /* We don‘t have more memory available */ ret = NULL; } else if (p->sl_curr != 0) { //除非do_slabs_newslab調用失敗,否則都會來到這裏.無論一開始sl_curr是否為0。 //p->slots指向第一個空閑的item,此時要把第一個空閑的item分配出去 /* return off our freelist */ it = (item *)p->slots; p->slots = it->next;//slots指向下一個空閑的item if (it->next) it->next->prev = 0; p->sl_curr--;//空閑數目減一 ret = (void *)it; } if (ret) { p->requested += size;//增加本slabclass分配出去的字節數 } return ret; }
可以看到在do_slabs_alloc函數的內部也是通過調用do_slabs_newslab增加item的。
在本文前面的代碼中,都沒有看到鎖的。作為memcached這個用鎖大戶,有點不正常。其實前面的代碼中,有一些是要加鎖才能訪問的,比如do_slabs_alloc函數。之所以上面的代碼中沒有看到,是因為memcached使用了包裹函數(這個概念對應看過《UNIX網絡編程》的讀者來說很熟悉吧)。memcached在包裹函數中加鎖後,才訪問上面的那些函數的。下面就是兩個包裹函數。
static pthread_mutex_t slabs_lock = PTHREAD_MUTEX_INITIALIZER; void *slabs_alloc(size_t size, unsigned int id) { void *ret; pthread_mutex_lock(&slabs_lock); ret = do_slabs_alloc(size, id); pthread_mutex_unlock(&slabs_lock); return ret; } void slabs_free(void *ptr, size_t size, unsigned int id) { pthread_mutex_lock(&slabs_lock); do_slabs_free(ptr, size, id); pthread_mutex_unlock(&slabs_lock); }
memcached源碼分析-----slab內存分配器