Linux 所使用的 slab 分配器的基礎是 Jeff Bonwick 爲 SunOS 操作系統首次引入的一種算法。Jeff 的分配器是圍繞對象緩存進行的。在內核中,會爲有限的對象集(例如文件描述符和其他常見結構)分配大量內存。Jeff
發現對內核中普通對象進行初始化所需的時間超過了對其進行分配和釋放所需的時間。因此他的結論是不應該將內存釋放回一個全局的內存池,而是將內存保持爲針對特定目而初始化的狀態。例如,如果內存被分配給了一個互斥鎖,那麼只需在爲互斥鎖首次分配內存時執行一次互斥鎖初始化函數(mutex_init
)即可。後續的內存分配不需要執行這個初始化函數,因爲從上次釋放和調用析構之後,它已經處於所需的狀態中了。
Memecached 中的slab 分配器也正是使用這一思想來構建一個在空間和時間上都具有高效性的內存分配器。下圖 給出了 slab 結構的高層組織結構。在最高層是cache_chain
,這是一個 slab 緩存的鏈接列表。這對於
best-fit 算法非常有用,可以用來查找最適合所需要的分配大小的緩存(遍歷列表)。cache_chain
的每個元素都是一個kmem_cache
結構的引用(稱爲一個
cache),它定義了一個要管理的給定大小的對象池。在需要某個特定大小的內存對象時,首先從cache_chian中找到最佳大小的一個kmem_cahce,然後再在對應的kem_cahe中按某種算法(如首先利用空閒對象,沒有則按LRU機制釋放已用或過期對象)最終獲得所需的大小的空間。具體結構如下圖所示:
從該圖可看出,這與前面所分析的Memcached的Item的存儲結構圖正是一致的。此處的cache_chain對應前面的slabclass數組(管理各種大小的slab集合),而kmem_cahe對應slabclass中的某個元素(slab_list鏈表)(管理某個特定大小的slab鏈表)。在刪除Item時,也不會將所對應的內存還給操作系統,而只是從對應的已分配中鏈表中去掉,轉而加到對應的空閒鏈表slots中以供後續循環利用。
memcached中內存分配機制主要理念:
1. 先爲分配相應的大塊內存,再在上面進行無縫小對象填充
2. 懶惰檢測機制,Memcached不花過多的時間在檢測各個item對象是否超時,當get獲取數據時,才檢查item對象是否應該刪除,你不訪問,我就不處理。
3. 懶惰刪除機制,在memecached中刪除一個item對象的時候,並不是從內存中釋放,而是單單的進行標記處理,再將其指針放入slot回收插糟,下次分配的時候直接使用。
Memcached首次默認分配64M的內存,之後所有的數據都是在這64M空間進行存儲,在Memcached啓動之後,不會對這些內存執行釋放操作,這些內存只有到Memcached進程退出之後會被系統回收。下面分析Memcached的內存主要操作函數,按逐級調用順序給出。
<span style="font-family:FangSong_GB2312;font-size:18px;">/*//內存初始化,settings.maxbytes是Memcached初始啓動參數指定的內存值大小,settings.factor是內存增長因子
slabs_init(settings.maxbytes, settings.factor, preallocate);
#define POWER_SMALLEST 1 //最小slab編號
#define POWER_LARGEST 200 //首次初始化200個slab
//實現內存池管理相關的靜態全局變量
static size_t mem_limit = 0;//總的內存大小
static size_t mem_malloced = 0;//初始化內存的大小,這個貌似沒什麼用
static void *mem_base = NULL;//指向總的內存的首地址
static void *mem_current = NULL;//當前分配到的內存地址
static size_t mem_avail = 0;//當前可用的內存大小
static slabclass_t slabclass[MAX_NUMBER_OF_SLAB_CLASSES];//定義slab結合,總共200個 */</span>
<span style="font-family:FangSong_GB2312;font-size:18px;">/**
* Determines the chunk sizes and initializes the slab class descriptors
* accordingly.
初始化整個slabcalss數組
limit:Memcached的總的內存的大小。
factor:chunk大小增長因子
*/
void slabs_init(const size_t limit, const double factor, const bool prealloc) {
int i = POWER_SMALLEST - 1;
//size表示申請空間的大小,其值由配置的chunk_size(指item中的數據部分大小)和單個item的大小來指定
unsigned int size = sizeof(item) + settings.chunk_size;
mem_limit = limit;
if (prealloc) {//支持預分配
/* Allocate everything in a big chunk with malloc */
mem_base = malloc(mem_limit);//分配限定的空間,mem_base爲總內存起始地址
if (mem_base != NULL) {
mem_current = mem_base;//mem_current爲當前分配空間地址
mem_avail = mem_limit;//可用(總分配空間中還未分配給Item的部分)
} else {
fprintf(stderr, "Warning: Failed to allocate requested memory in"
" one large chunk.\nWill allocate in smaller chunks\n");
}
}
//置空slabclass數組
memset(slabclass, 0, sizeof(slabclass)); //sizeof(slabclass)爲整個數組大小,而非指針大小
//開始分配,i<200 && 單個chunk的size<=單個item最大大小/內存增長因子
while (++i < POWER_LARGEST && size <= settings.item_size_max / factor) {
/* Make sure items are always n-byte aligned */
//確保item總是8byte對齊
if (size % CHUNK_ALIGN_BYTES)
size += CHUNK_ALIGN_BYTES - (size % CHUNK_ALIGN_BYTES);//沒對齊,則補齊
slabclass[i].size = size;//slab中chunk的大小設爲補齊的大小
slabclass[i].perslab = settings.item_size_max / slabclass[i].size;//每個slab中的chunk數量
size *= factor;//下一個slab中的chunk擴大factor倍
if (settings.verbose > 1) {//如果有打開調試信息,則輸出調試信息
fprintf(stderr, "slab class %3d: chunk size %9u perslab %7u\n",
i, slabclass[i].size, slabclass[i].perslab);
}
}
//slab數組中的最後一個slab,此時chunk大小增加爲1M,因此只有一個chunk
power_largest = i;
slabclass[power_largest].size = settings.item_size_max;
slabclass[power_largest].perslab = 1;
if (settings.verbose > 1) {
fprintf(stderr, "slab class %3d: chunk size %9u perslab %7u\n",
i, slabclass[i].size, slabclass[i].perslab);
}
/* for the test suite: faking of how much we've already malloc'd */
{
char *t_initial_malloc = getenv("T_MEMD_INITIAL_MALLOC");
if (t_initial_malloc) {
mem_malloced = (size_t)atol(t_initial_malloc);
}
}
if (prealloc) {
//真正分配空間:分配每個slab的內存空間,傳入最大已經初始化的最大slab編號
slabs_preallocate(power_largest);
}
}</span>
<span style="font-family:FangSong_GB2312;font-size:18px;">//分配每個slabclass數組元素的內存空間
static void slabs_preallocate (const unsigned int maxslabs) {
int i;
unsigned int prealloc = 0;
for (i = POWER_SMALLEST; i <= POWER_LARGEST; i++) {
if (++prealloc > maxslabs)
return;
//執行分配操作,對第i個slabclass執行分配操作
if (do_slabs_newslab(i) == 0) {
fprintf(stderr, "Error while preallocating slab memory!\n"
"If using -L or other prealloc options, max memory must be "
"at least %d megabytes.\n", power_largest);
exit(1);
}
}
} </span>
<span style="font-family:FangSong_GB2312;font-size:18px;">//爲第id個slabclass執行分配操作
static int do_slabs_newslab(const unsigned int id) {
slabclass_t *p = &slabclass[id];//p指向第i個slabclass
int len = settings.slab_reassign ? settings.item_size_max:p->size*p->perslab; //len爲一個slabclass的大小
char *ptr;
//grow_slab_list初始化slabclass的slab_list,而slab_list中的指針指向每個slab
//memory_allocate從內存池申請1M的空間
if ((mem_limit && mem_malloced + len > mem_limit && p->slabs > 0) ||
(grow_slab_list(id) == 0) ||
((ptr = memory_allocate((size_t)len)) == 0)) { //優先從Memchahed內存池中分配,如果內存池爲空則從系統分配給定大小內存
MEMCACHED_SLABS_SLABCLASS_ALLOCATE_FAILED(id);
return 0;
}
memset(ptr, 0, (size_t)len);
//將申請的1M空間按slabclass的size進行切分
split_slab_page_into_freelist(ptr, id);
p->slab_list[p->slabs++] = ptr;
mem_malloced += len;//增加已經分配出去的內存數
MEMCACHED_SLABS_SLABCLASS_ALLOCATE(id);
return 1;
}
</span>
<span style="font-family:FangSong_GB2312;font-size:18px;">//將同級別slab的空間且分爲該大小的chunk
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);//創建空閒item
ptr += p->size;//指針前移item的大小
}
}</span>
<span style="font-family:FangSong_GB2312;font-size:18px;">//創建空閒item ,掛載到對應slabclass的空閒鏈表中
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);//判斷id有效性
if (id < POWER_SMALLEST || id > power_largest)
return;
MEMCACHED_SLABS_FREE(size, id, ptr);
p = &slabclass[id];
it = (item *)ptr;
it->it_flags |= ITEM_SLABBED;
it->prev = 0;
it->next = p->slots;//掛載到slabclass的空閒鏈表中
if (it->next) it->next->prev = it;
p->slots = it;
p->sl_curr++;//空閒item個數+1
p->requested -= size;//已經申請到的空間數量更新
return;
} </span>
至此,從創建slabclass數組,到最底層的創建空閒item並掛載到對應的slabclass的空閒鏈表slots的頭部, 的操作完成。
Memcached的內存池由起始地址指針、當前地址指針、剩餘可用空間等變量維護,每次內存池操作只需要相應的改變這些變量即可。
以下爲內存池分配操作函數:
<span style="font-family:FangSong_GB2312;font-size:18px;">//優先從Memcached的內存池分配size大小的空間
static void *memory_allocate(size_t size) {
void *ret;
if (mem_base == NULL) {//如果內存池沒創建,則從系統分配
ret = malloc(size);
} else {
ret = mem_current;
//size大於剩餘的空間
if (size > mem_avail) {
return NULL;
}
//按8字節對齊
if (size % CHUNK_ALIGN_BYTES) {
size += CHUNK_ALIGN_BYTES - (size % CHUNK_ALIGN_BYTES);
}
//扣除size個空間
mem_current = ((char*)mem_current) + size;
if (size < mem_avail) {
mem_avail -= size;//更新剩餘空間大小
} else {
mem_avail = 0;
}
}
return ret;
} </span>
由此可見,內存池的實現其實是很簡單的。當然這裏只給出了內存池中內存分配的操作,而內存釋放操作也類似。總之,內存池的操作,需要維護內存池的幾個指針變量和空間指示變量即可。