1. 程式人生 > >memcached源碼分析-----slab內存分配器

memcached源碼分析-----slab內存分配器

details 初始化 allocator 內存碎片 時間 ng- 遍歷 thread 失敗

溫馨提示:本文用到了一些可以在啟動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內存分配器