1.內部碎片和外部碎片
外部碎片
什么是外部碎片呢?我們通過一個圖來解釋:
假設這是一段連續的頁框,陰影部分表示已經被使用的頁框,現在需要申請一個連續的5個頁框。這個時候,在這段內存上不能找到連續的5個空閑的頁框,就會去另一段內存上去尋找5個連續的頁框,這樣子,久而久之就形成了頁框的浪費。稱為外部碎片。
內核中使用伙伴算法的遷移機制很好的解決了這種外部碎片。
內部碎片
當我們申請幾十個字節的時候,內核也是給我們分配一個頁,這樣在每個頁中就形成了很大的浪費。稱之為內部碎片。
內核中引入了slab機制去盡力的減少這種內部碎片。
2.slab分配機制
slab分配器是基於對象進行管理的,所謂的對象就是內核中的數據結構(例如:task_struct,file_struct 等)。相同類型的對象歸為一類,每當要申請這樣一個對象時,slab分配器就從一個slab列表中分配一個這樣大小的單元出去,而當要釋放時,將其重新保存在該列表中,而不是直接返回給伙伴系統,從而避免內部碎片。slab分配器並不丟棄已經分配的對象,而是釋放並把它們保存在內存中。slab分配對象時,會使用最近釋放的對象的內存塊,因此其駐留在cpu高速緩存中的概率會大大提高。
3.內核中slab的主要數據結構
簡要分析下這個圖:kmem_cache是一個cache_chain的鏈表,描述了一個高速緩存,每個高速緩存包含了一個slabs的列表,這通常是一段連續的內存塊。存在3種slab:slabs_full(完全分配的slab),slabs_partial(部分分配的slab),slabs_empty(空slab,或者沒有對象被分配)。slab是slab分配器的最小單位,在實現上一個slab有一個貨多個連續的物理頁組成(通常只有一頁)。單個slab可以在slab鏈表之間移動,例如如果一個半滿slab被分配了對象后變滿了,就要從slabs_partial中被刪除,同時插入到slabs_full中去。
舉例說明:如果有一個名叫inode_cachep的struct kmem_cache節點,它存放了一些inode對象。當內核請求分配一個新的inode對象時,slab分配器就開始工作了:
- 首先要查看inode_cachep的slabs_partial鏈表,如果slabs_partial非空,就從中選中一個slab,返回一個指向已分配但未使用的inode結構的指針。完事之后,如果這個slab滿了,就把它從slabs_partial中刪除,插入到slabs_full中去,結束;
- 如果slabs_partial為空,也就是沒有半滿的slab,就會到slabs_empty中尋找。如果slabs_empty非空,就選中一個slab,返回一個指向已分配但未使用的inode結構的指針,然后將這個slab從slabs_empty中刪除,插入到slabs_partial(或者slab_full)中去,結束;
- 如果slabs_empty也為空,那么沒辦法,cache內存已經不足,只能新創建一個slab了。
接下來我們來分析下slab在內核中數據結構的組織,首先要從kmem_cache這個結構體說起了
struct kmem_cache {
struct array_cache *array[NR_CPUS];//per_cpu數據,記錄了本地高速緩存的信息,也是用於跟蹤最近釋放的對象,每次分配和釋放都要直接訪問它。
unsigned int batchcount;//本地高速緩存轉入和轉出的大批數據數量
unsigned int limit;//本地高速緩存中空閑對象的最大數目
unsigned int shared;
unsigned int buffer_size;/*buffer的大小,就是對象的大小*/
u32 reciprocal_buffer_size;
unsigned int flags; /* constant flags */
unsigned int num; /* # of objs per slab *//*slab中有多少個對象*/
/* order of pgs per slab (2^n) */
unsigned int gfporder;/*每個slab中有多少個頁*/
gfp_t gfpflags; /*與伙伴系統交互時所提供的分配標識*/
size_t colour; /* cache colouring range *//*slab中的着色*/
unsigned int colour_off; /* colour offset */着色的偏移量
struct kmem_cache *slabp_cache;
unsigned int slab_size; //slab管理區的大小
unsigned int dflags; /* dynamic flags */
/* constructor func */
void (*ctor)(void *obj); /*構造函數*/
/* 5) cache creation/removal */
const char *name;/*slab上的名字*/
struct list_head next; //用於將高速緩存連入cache chain
/* 6) statistics */ //一些用於調試用的變量
#ifdef CONFIG_DEBUG_SLAB
unsigned long num_active;
unsigned long num_allocations;
unsigned long high_mark;
unsigned long grown;
unsigned long reaped;
unsigned long errors;
unsigned long max_freeable;
unsigned long node_allocs;
unsigned long node_frees;
unsigned long node_overflow;
atomic_t allochit;
atomic_t allocmiss;
atomic_t freehit;
atomic_t freemiss;
int obj_offset;
int obj_size;
#endif /* CONFIG_DEBUG_SLAB */
//用於組織該高速緩存中的slab
struct kmem_list3 *nodelists[MAX_NUMNODES];/*最大的內存節點*/
};
/* Size description struct for general caches. */
struct cache_sizes {
size_t cs_size;
struct kmem_cache *cs_cachep;
#ifdef CONFIG_ZONE_DMA
struct kmem_cache *cs_dmacachep;
#endif
};
由上面的總圖可知,一個核心的數據結構就是kmem_list3,它描述了slab描述符的狀態。
struct kmem_list3 {
/*三個鏈表中存的是一個高速緩存slab*/
/*在這三個鏈表中存放的是cache*/
struct list_head slabs_partial; //包含空閑對象和已經分配對象的slab描述符
struct list_head slabs_full;//只包含非空閑的slab描述符
struct list_head slabs_free;//只包含空閑的slab描述符
unsigned long free_objects; /*高速緩存中空閑對象的個數*/
unsigned int free_limit; //空閑對象的上限
unsigned int colour_next; /* Per-node cache coloring *//*即將要着色的下一個*/
spinlock_t list_lock;
struct array_cache *shared; /* shared per node */
struct array_cache **alien; /* on other nodes */
unsigned long next_reap; /* updated without locking *//**/
int free_touched; /* updated without locking */
};
接下來介紹描述單個slab的結構struct slab
struct slab {
struct list_head list; //用於將slab連入keme_list3的鏈表
unsigned long colouroff; //該slab的着色偏移
void *s_mem; /* 指向slab中的第一個對象*/
unsigned int inuse; /* num of objs active in slab */已經分配出去的對象
kmem_bufctl_t free; //下一個空閑對象的下標
unsigned short nodeid; //節點標識符
};
在kmem_cache中還有一個重要的數據結構struct array_cache.這是一個指針數組,數組的元素是系統的cpu的個數。該結構用來描述每個cpu的高速緩存,它的主要作用是減少smp系統中對於自旋鎖的競爭。
- 實際上,每次分配內存都是直接與本地cpu高速緩存進行交互,只有當其空閑內存不足時,才會從keme_list中的slab中引入一部分對象到本地高速緩存中,而keme_list中的空閑對象也不足時,那么就要從伙伴系統中引入新的頁來建立新的slab了。
struct array_cache {
unsigned int avail;/*當前cpu上有多少個可用的對象*/
unsigned int limit;/*per_cpu里面最大的對象的個數,當超過這個值時,將對象返回給伙伴系統*/
unsigned int batchcount;/*一次轉入和轉出的對象數量*/
unsigned int touched;/*標示本地cpu最近是否被使用*/
spinlock_t lock;/*自旋鎖*/
void *entry[]; /*
* Must have this definition in here for the proper
* alignment of array_cache. Also simplifies accessing
* the entries.
*/
};
對上面提到的各個數據結構做一個總結,用下圖來描述:
4.關於slab分配器的API
下面看一下slab分配器的接口——看看slab緩存是如何創建、撤銷以及如何從緩存中分配一個對象的。一個新的kmem_cache通過kmem_cache_create()函數來創建:
struct kmem_cache *
kmem_cache_create( const char *name, size_t size, size_t align,
unsigned long flags, void (*ctor)(void*));
*name是一個字符串,存放kmem_cache緩存的名字;size是緩存所存放的對象的大小;align是slab內第一個對象的偏移;flag是可選的配置項,用來控制緩存的行為。最后一個參數ctor是對象的構造函數,一般是不需要的,以NULL來代替。kmem_cache_create()成功執行之后會返回一個指向所創建的緩存的指針,否則返回NULL。kmem_cache_create()可能會引起阻塞(睡眠),因此不能在中斷上下文中使用。
撤銷一個kmem_cache則是通過kmem_cache_destroy()函數:
int kmem_cache_destroy( struct kmem_cache *cachep);
該函數成功則返回0,失敗返回非零值。調用kmem_cache_destroy()之前應該滿足下面幾個條件:首先,cachep所指向的緩存中所有slab都為空閑,否則的話是不可以撤銷的;其次在調用kmem_cache_destroy()過程中以及調用之后,調用者需要確保不會再訪問這個緩存;最后,該函數也可能會引起阻塞,因此不能在中斷上下文中使用。
可以通過下面函數來從kmem_cache中分配一個對象:
void* kmem_cache_alloc(struct kmem_cache* cachep, gfp_t flags);
這個函數從cachep指定的緩存中返回一個指向對象的指針。如果緩存中所有slab都是滿的,那么slab分配器會通過調用kmem_getpages()創建一個新的slab。
釋放一個對象的函數如下:
void kmem_cache_free(struct kmem_cache* cachep, void* objp);
這個函數是將被釋放的對象返還給先前的slab,其實就是將cachep中的對象objp標記為空閑而已
5.使用以上的API寫內核模塊,生成自己的slab高速緩存。
其實到了這里,應該去分析以上函數的源碼,但是幾次奮起分析,都被打趴在地。所以就寫個內核模塊,鼓勵下自己吧。
#include <linux/autoconf.h>
#include <linux/module.h>
#include <linux/slab.h>
MODULE_AUTHOR("wangzhangjun");
MODULE_DESCRIPTION("slab test module");
static struct kmem_cache *test_cachep = NULL;
struct slab_test
{
int val;
};
void fun_ctor(struct slab_test *object , struct kmem_cache *cachep , unsigned long flags )
{
printk(KERN_INFO "ctor fuction ...\n");
object->val = 1;
}
static int __init slab_init(void)
{
struct slab_test *object = NULL;//slab的一個對象
printk(KERN_INFO "slab_init\n");
test_cachep = kmem_cache_create("test_cachep",sizeof(struct slab_test)*3,0,SLAB_HWCACHE_ALIGN,fun_ctor);
if(NULL == test_cachep)
return -ENOMEM ;
printk(KERN_INFO "Cache name is %s\n",kmem_cache_name(test_cachep));//獲取高速緩存的名稱
printk(KERN_INFO "Cache object size is %d\n",kmem_cache_size(test_cachep));//獲取高速緩存的大小
object = kmem_cache_alloc(test_cachep,GFP_KERNEL);//從高速緩存中分配一個對象
if(object)
{
printk(KERN_INFO "alloc one val = %d\n",object->val);
kmem_cache_free( test_cachep, object );//歸還對象到高速緩存
//這句話的意思是雖然對象歸還到了高速緩存中,但是高速緩存中的值沒有做修改
//只是修改了一些它的狀態。
printk(KERN_INFO "alloc three val = %d\n",object->val);
object = NULL;
}else
return -ENOMEM;
return 0;
}
static void __exit slab_clean(void)
{
printk(KERN_INFO "slab_clean\n");
if(test_cachep)
kmem_cache_destroy(test_cachep);//調用這個函數時test_cachep所指向的緩存中所有的slab都要為空
}
module_init(slab_init);
module_exit(slab_clean);
MODULE_LICENSE("GPL");
我們結合結果來分析下這個內核模塊:
這是dmesg的結果,可以發現我們自己創建的高速緩存的名字test_cachep,還有每個對象的大小。
還有構造函數修改了對象里面的值,至於為什么構造函數會出現這么多次,可能是因為,這個函數被注冊了之后,系統的其他地方也會調用這個函數。在這里可以分析源碼,當調用keme_cache_create()的時候是沒有調用對象的構造函數的,調用kmem_cache_create()並沒有分配slab,而是在創建對象的時候發現沒有空閑對象,在分配對象的時候,會調用構造函數初始化對象。
另外結合上面的代碼可以發現,alloc three val是在kmem_cache_free之后打印的,但是它的值依然可以被打印出來,這充分說明了,slab這種機制是在將某個對象使用完之后,就其緩存起來,它還是切切實實的存在於內存中。
再結合/proc/slabinfo的信息看我們自己創建的slab高速緩存
可以發現名字為test_cachep的高速緩存,每個對象的大小(objsize)是16,和上面dmesg看到的值相同,objperslab(每個slab中的對象時202),pagesperslab(每個slab中包含的頁數),可以知道objsize * objperslab < pagesperslab
。
6.總結
目前只是對slab機制的原理有了一個感性的認識,對於這部分相關的源碼涉及到着色以及內存對齊等細節。看的不是很清楚,后面還需要仔細研究。