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);
}