1. 程式人生 > >memcached原始碼分析-----slab記憶體分配器

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