SLAB分配器概述
管理區(qū)頁框分配器,這里我們簡稱為頁框分配器,在頁框分配器中主要是管理物理內(nèi)存,將物理內(nèi)存的頁框分配給申請者,而且我們知道也可頁框大小為4K(也可設(shè)置為4M),這時候就會有個問題,如果我只需要1KB大小的內(nèi)存,頁框分配器也不得不分配一個4KB的頁框給申請者,這樣就會有3KB被白白浪費掉了。為了應(yīng)對這種情況,在頁框分配器上一層又做了一層SLAB層,SLAB分配器的作用就是從頁框分配器中拿出一些頁框,專門把這些頁框拆分成一小塊一小塊的小內(nèi)存,當(dāng)申請者申請的是小內(nèi)存時,系統(tǒng)就會從SLAB中獲取一小塊分配給申請者。它們的整個關(guān)系如下圖:
?

?
?
可以看出,SLAB分配器和頁框分配器并沒有什么直接的聯(lián)系,對于頁框分配器來說,SLAB分配器也只是一個從它那里申請頁框的申請者而已。
在SLAB分配器中將SLAB分為兩大類:專用SLAB和普通SLAB。專用SLAB用于特定的場合(比如TCP有自己專用的SLAB,當(dāng)TCP模塊需要小內(nèi)存時,會從自己的SLAB中分配),而普通SLAB就是用于常規(guī)分配的時候。我們可以使用命令查看SLAB的狀態(tài)
cat /proc/slabinfo
命令結(jié)果如下:
?

?
?
?

?
?
如剛才所有,我們看到有些SLAB的名字比較特別,如TCP,UDP,dquot這些,它們都是專用SLAB,專屬于它們自己的模塊。而后面這張圖,如kmalloc-8,kmalloc-16...還有dma-kmalloc-96,dma-kmalloc-192...這些都是普通SLAB,當(dāng)需要為一些小數(shù)據(jù)分配內(nèi)存時(比如一個結(jié)構(gòu)體),就會從這些普通SLAB中獲取內(nèi)存。值得注意的是,對于kmalloc-8這些普通SLAB,都有一個對應(yīng)的dma-kmalloc-8這種類型的普通SLAB,這種類型是專門使用了ZONE-DMA區(qū)域的內(nèi)存,方便用于DMA模式申請內(nèi)存。
在SLAB中,可分配的內(nèi)存塊稱之為對象,在后面那張圖中,如kmalloc-8這個普通SLAB,里面所有的對象都是8B大小,同理,kmalloc-16中的對象都是以16B為大小。當(dāng)你申請1B~8B的內(nèi)存時,系統(tǒng)會從kmalloc-8中分配一個對象給你,當(dāng)你申請8B~16B的內(nèi)存時,系統(tǒng)會從kmalloc-16里給你分配。雖然即使申請5B,分配了一個8B的對象,還有3B空閑,但這樣設(shè)計已經(jīng)大大減小了內(nèi)存碎片化了,保證了碎片內(nèi)存不會超過50%(kmalloc-8除外)。需要注意,在kmalloc-8中申請到的對象,釋放時也會回到kmalloc-8中。
除了減小了內(nèi)存碎片化,SLAB還有一個作用,提高了系統(tǒng)的效率,當(dāng)對象擁有者釋放一個對象后,SLAB的處理是僅僅標(biāo)記對象為空閑,并不做多少處理,而又有申請者申請相應(yīng)大小的對象時,SLAB會優(yōu)先分配最近釋放的對象,這樣這個對象甚至有可能還在硬件高速緩存中,有點類似管理區(qū)頁框分配器中每CPU高速緩存的做法。
?
kmem_cache結(jié)構(gòu)
雖然叫SLAB分配器,但是在SLAB分配器中,最頂層的數(shù)據(jù)結(jié)構(gòu)卻不是SLAB,而是kmem_cache,我們暫且叫它SLAB緩存吧,每個SLAB緩存都有它自己的名字,就是上圖中的kmalloc-8,kmalloc-16等。總的來說,kmem_cache結(jié)構(gòu)用于描述一種SLAB,并且管理著這種SLAB中所有的對象。所有的kmem_cache結(jié)構(gòu)會保存在以slab_caches作為頭的鏈表中。在內(nèi)核模塊中可以通過kmem_cache_create自行創(chuàng)建一個kmem_cache用于管理屬于自己模塊的SLAB。
我們先看看kmem_cache結(jié)構(gòu):
/* slab分配器中的SLAB高速緩存 */
struct kmem_cache {
/* 指向包含空閑對象的本地高速緩存,每個CPU有一個該結(jié)構(gòu),當(dāng)有對象釋放時,優(yōu)先放入本地CPU高速緩存中 */
struct array_cache __percpu *cpu_cache;
/* 1) Cache tunables. Protected by slab_mutex */
/* 要轉(zhuǎn)移進本地高速緩存或從本地高速緩存中轉(zhuǎn)移出去的對象的數(shù)量 */
unsigned int batchcount;
/* 本地高速緩存中空閑對象的最大數(shù)目 */
unsigned int limit;
/* 是否存在CPU共享高速緩存,CPU共享高速緩存指針保存在kmem_cache_node結(jié)構(gòu)中 */
unsigned int shared;
/* 對象長度 + 填充字節(jié) */
unsigned int size;
/* size的倒數(shù),加快計算 */
struct reciprocal_value reciprocal_buffer_size;
/* 2) touched by every alloc & free from the backend */
/* 高速緩存永久屬性的標(biāo)識,如果SLAB描述符放在外部(不放在SLAB中),則CFLAGS_OFF_SLAB置1 */
unsigned int flags; /* constant flags */
/* 每個SLAB中對象的個數(shù)(在同一個高速緩存中slab中對象個數(shù)相同) */
unsigned int num; /* # of objs per slab */
/* 3) cache_grow/shrink */
/* 一個單獨SLAB中包含的連續(xù)頁框數(shù)目的對數(shù) */
unsigned int gfporder;
/* 分配頁框時傳遞給伙伴系統(tǒng)的一組標(biāo)識 */
gfp_t allocflags;
/* SLAB使用的顏色個數(shù) */
size_t colour;
/* SLAB中基本對齊偏移,當(dāng)新SLAB著色時,偏移量的值需要乘上這個基本對齊偏移量,理解就是1個偏移量等于多少個B大小的值 */
unsigned int colour_off;
/* 空閑對象鏈表放在外部時使用,其指向的SLAB高速緩存來存儲空閑對象鏈表 */
struct kmem_cache *freelist_cache;
/* 空閑對象鏈表的大小 */
unsigned int freelist_size;
/* 構(gòu)造函數(shù),一般用于初始化這個SLAB高速緩存中的對象 */
void (*ctor)(void *obj);
/* 4) cache creation/removal */
/* 存放高速緩存名字 */
const char *name;
/* 高速緩存描述符雙向鏈表指針 */
struct list_head list;
int refcount;
/* 高速緩存中對象的大小 */
int object_size;
int align;
/* 5) statistics */
/* 統(tǒng)計 */
#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;
#endif /* CONFIG_DEBUG_SLAB */
#ifdef CONFIG_MEMCG_KMEM
/* 用于分組資源限制 */
struct memcg_cache_params *memcg_params;
#endif
/* 結(jié)點鏈表,此高速緩存可能在不同NUMA的結(jié)點都有SLAB鏈表 */
struct kmem_cache_node *node[MAX_NUMNODES];
};
從結(jié)構(gòu)中可以看出,在這個kmem_cache中所有對象的大小是相同的(object_size),并且此kmem_cache中所有SLAB的大小也是相同的(gfporder、num)。
在這個結(jié)構(gòu)中,最重要的可能就屬struct kmem_cache_node * node[Max_NUMNODES]這個指針數(shù)組了,指向的struct kmem_cache_node中保存著slab鏈表,在NUMA架構(gòu)中每個node對應(yīng)數(shù)組中的一個元素,因為每個SLAB高速緩存都有可能在不同結(jié)點維護有自己的SLAB用于這個結(jié)點的分配。我們看看struct kmem_cache_node:
/* SLAB鏈表結(jié)構(gòu) */
struct kmem_cache_node {
/* 鎖 */
spinlock_t list_lock;
/* SLAB用 */
#ifdef CONFIG_SLAB
/* 只使用了部分對象的SLAB描述符的雙向循環(huán)鏈表 */
struct list_head slabs_partial; /* partial list first, better asm code */
/* 不包含空閑對象的SLAB描述符的雙向循環(huán)鏈表 */
struct list_head slabs_full;
/* 只包含空閑對象的SLAB描述符的雙向循環(huán)鏈表 */
struct list_head slabs_free;
/* 高速緩存中空閑對象個數(shù)(包括slabs_partial鏈表中和slabs_free鏈表中所有的空閑對象) */
unsigned long free_objects;
/* 高速緩存中空閑對象的上限 */
unsigned int free_limit;
/* 下一個被分配的SLAB使用的顏色 */
unsigned int colour_next; /* Per-node cache coloring */
/* 指向這個結(jié)點上所有CPU共享的一個本地高速緩存 */
struct array_cache *shared; /* shared per node */
struct alien_cache **alien; /* on other nodes */
/* 兩次緩存收縮時的間隔,降低次數(shù),提高性能 */
unsigned long next_reap;
/* 0:收縮 1:獲取一個對象 */
int free_touched; /* updated without locking */
#endif
/* SLUB用 */
#ifdef CONFIG_SLUB
unsigned long nr_partial;
struct list_head partial;
#ifdef CONFIG_SLUB_DEBUG
atomic_long_t nr_slabs;
atomic_long_t total_objects;
struct list_head full;
#endif
#endif
};
在這個結(jié)構(gòu)中,最重要的就是slabs_partial、slabs_full、slabs_free這三個鏈表頭。
slabs_partial:維護部分對象被使用了的SLAB鏈表,保存的是SLAB描述符。
slabs_full:維護所有對象都被使用了的SLAB鏈表,保存的是SLAB描述符。
slabs_free:維護所有對象都沒被使用的SLAB鏈表,保存的是SLAB描述符。
可能到這里大家會比較郁悶,怎么又有SLAB鏈表,SLAB到底是什么東西?SLAB就是一組連續(xù)的頁框,它的描述符結(jié)合在頁描述符中,也就是頁描述符描述SLAB的時候,就是SLAB描述符。這三個鏈表保存的是這組頁框的首頁框的SLAB描述符。鏈表的組織形式與伙伴系統(tǒng)的組織頁框的形式一樣。
剛開始創(chuàng)建kmem_cache完成后,這三個鏈表都為空,只有在申請對象時發(fā)現(xiàn)沒有可用的slab時才會創(chuàng)建一個新的SLAB,并加入到這三個鏈表中的一個中。也就是說kmem_cache中的SLAB數(shù)量是動態(tài)變化的,當(dāng)SLAB數(shù)量太多時,kmem_cache會將一些SLAB釋放回頁框分配器中。
我們看看SLAB描述符中相關(guān)字段:
struct page {
/* First double word block */
/* 用于頁描述符,一組標(biāo)志(如PG_locked、PG_error),也對頁框所在的管理區(qū)和node進行編號 */
unsigned long flags; /
union {
/* 用于頁描述符,當(dāng)頁被插入頁高速緩存中時使用,或者當(dāng)頁屬于匿名區(qū)時使用 */
struct address_space *mapping;
/* 用于SLAB描述符,指向第一個對象的地址 */
void *s_mem; /* slab first object */
};
/* Second double word */
struct {
union {
/* 作為不同的含義被幾種內(nèi)核成分使用。例如,它在頁磁盤映像或匿名區(qū)中標(biāo)識存放在頁框中的數(shù)據(jù)的位置,或者它存放一個換出頁標(biāo)識符 */
pgoff_t index; /* Our offset within mapping. */
/* 用于SLAB描述符,指向空閑對象鏈表 */
void *freelist;
/* 當(dāng)管理區(qū)頁框分配器壓力過大時,設(shè)置這個標(biāo)志就確保這個頁框?qū)iT用于釋放其他頁框時使用 */
bool pfmemalloc;
};
union {
#if defined(CONFIG_HAVE_CMPXCHG_DOUBLE) && \
defined(CONFIG_HAVE_ALIGNED_STRUCT_PAGE)
/* Used for cmpxchg_double in slub */
/* SLUB使用 */
unsigned long counters;
#else
/* SLUB使用 */
unsigned counters;
#endif
struct {
union {
/* 頁框中的頁表項計數(shù),如果沒有為-1,如果為PAGE_BUDDY_MAPCOUNT_VALUE(-128),說明此頁及其后的一共2的private次方個數(shù)頁框處于伙伴系統(tǒng)中,正在使用時應(yīng)該是0 */
atomic_t _mapcount;
struct { /* SLUB使用 */
unsigned inuse:16;
unsigned objects:15;
unsigned frozen:1;
};
int units; /* SLOB */
};
/* 頁框的引用計數(shù),如果為-1,則此頁框空閑,并可分配給任一進程或內(nèi)核;如果大于或等于0,則說明頁框被分配給了一個或多個進程,或用于存放內(nèi)核數(shù)據(jù)。page_count()返回_count加1的值,也就是該頁的使用者數(shù)目 */
atomic_t _count; /* Usage count, see below. */
};
/* 用于SLAB時描述當(dāng)前SLAB已經(jīng)使用的對象 */
unsigned int active; /* SLAB */
};
};
/* Third double word block */
union {
/* 包含到頁的最近最少使用(LRU)雙向鏈表的指針,用于插入伙伴系統(tǒng)的空閑鏈表中,只有塊中頭頁框要被插入。也用于SLAB,加入到kmem_cache中的SLAB鏈表中 */
struct list_head lru;
/* SLAB使用 */
struct { /* slub per cpu partial pages */
struct page *next; /* Next partial slab */
#ifdef CONFIG_64BIT
int pages; /* Nr of partial slabs left */
int pobjects; /* Approximate # of objects */
#else
short int pages;
short int pobjects;
#endif
};
/* SLAB使用 */
struct slab *slab_page; /* slab fields */
struct rcu_head rcu_head; /* Used by SLAB
* when destroying via RCU
*/
#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && USE_SPLIT_PMD_PTLOCKS
pgtable_t pmd_huge_pte; /* protected by page->ptl */
#endif
};
/* Remainder is not double word aligned */
union {
/* 可用于正在使用頁的內(nèi)核成分(例如: 在緩沖頁的情況下它是一個緩沖器頭指針,如果頁是空閑的,則該字段由伙伴系統(tǒng)使用,在給伙伴系統(tǒng)使用時,表明的是塊的2的次方數(shù),只有塊的第一個頁框會使用) */
unsigned long private;
#if USE_SPLIT_PTE_PTLOCKS
#if ALLOC_SPLIT_PTLOCKS
spinlock_t *ptl;
#else
spinlock_t ptl;
#endif
#endif
/* SLAB描述符使用,指向SLAB的高速緩存 */
struct kmem_cache *slab_cache; /* SL[AU]B: Pointer to slab */
struct page *first_page; /* Compound tail pages */
};
#if defined(WANT_PAGE_VIRTUAL)
/* 線性地址,如果是沒有映射的高端內(nèi)存的頁框,則為空 */
void *virtual; /* Kernel virtual address (NULL if
not kmapped, ie. highmem) */
#endif /* WANT_PAGE_VIRTUAL */
#ifdef CONFIG_WANT_PAGE_DEBUG_FLAGS
unsigned long debug_flags; /* Use atomic bitops on this */
#endif
#ifdef CONFIG_KMEMCHECK
void *shadow;
#endif
#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS
int _last_cpupid;
#endif
}
在SLAB描述符中,最重要的可能就是s_mem和freelist這兩個指針。s_mem用于指向這段連續(xù)頁框中第一個對象,freelist指向空閑對象鏈表。
空閑對象鏈表是一個由數(shù)組制成的簡單鏈表,它保存的地方有兩種情況:
保存在外部,會從SLAB中分配一個對象用于保存新的SLAB的空閑對象鏈表。
保存在內(nèi)部,保存在這個SLAB所代表的連續(xù)頁框的頭部。
不過一般沒有什么其他情況空閑對象鏈表都是保存在內(nèi)部居多,這里我們只討論將空閑對象鏈表保存在內(nèi)部的情況,這種情況下,這個SLAB所代表的連續(xù)頁框的頭部首先放的就是空閑對象鏈表,后面接著放的是對象描述符數(shù)組(1,2個字節(jié)大小),之后緊接著就是對象所代表的內(nèi)存了,如下圖:
?

?
?
?
我們看看freelist數(shù)組是怎么形成一個鏈表的,之前我們也說了分配時會優(yōu)先分配最近釋放的對象,整個freelist跟struct page中的active有很大聯(lián)系,可以說active決定了下個分配的對象是誰,在freelist數(shù)組制作成的鏈表中,active作為下標(biāo),保存目標(biāo)空閑對象的對象號,在活動過程中,動態(tài)修改這個數(shù)組中的值。我們用一幅圖可以很清楚看出freelist是如何實現(xiàn):
?

?
?
SLAB中的連續(xù)頁框個數(shù)與kmem_cache結(jié)構(gòu)中的gfporder有關(guān),而這個gfporder在初始化時通過對象數(shù)量、大小、freelist大小、對象描述符數(shù)組大小和著色區(qū)計算出來的。而對于對象的大小,也并不是你創(chuàng)建時打算使用的大小,比如,我打算創(chuàng)建一個kmem_cache的對象大小是10字節(jié),而在創(chuàng)建過程中,系統(tǒng)會幫你優(yōu)化和初始化這些對象,包括將你的對象保存地址放在內(nèi)存對其標(biāo)志,在對象的兩邊放入一些填充區(qū)域(RED_ZONE)進行防止越界等工作。
關(guān)于SLAB著色
看名字很難理解,其實又很好理解,我們知道內(nèi)存需要處理時要先放入CPU硬件高速緩存中,而CPU硬件高速緩存與內(nèi)存的映射方式有多種。在同一個kmem_cache中所有SLAB都是相同大小,都是相同連續(xù)長度的頁框組成,這樣的話在不同SLAB中相同對象號對于頁框的首地址的偏移量也相同,這樣有很可能導(dǎo)致不同SLAB中相同對象號的對象放入CPU硬件高速緩存時會處于同一行,當(dāng)我們交替操作這兩個對象時,CPU的cache就會交替換入換出,效率就非常差。SLAB著色就是在同一個kmem_cache中對不同的SLAB添加一個偏移量,就讓相同對象號的對象不會對齊,也就不會放入硬件高速緩存的同一行中,提高了效率,如下圖:
?

?
?
著色空間就是前端的空閑區(qū)域,這個區(qū)有大小都是在分配新的SLAB時計算好的,計算方法很簡單,node結(jié)點對應(yīng)的kmem_cache_node中的colour_next乘上kmem_cache中的colour_off就得到了偏移量,然后colour_next++,當(dāng)colour_next等于kmem_cache中的colour時,colour_next回歸到0。
?偏移量 = kmem_cache.colour_off * kmem_cache.node[NODE_ID].colour_next; ? ?kmem_cache.node[NODE_ID].colour_next++; ? ?if (kmem_cache.node[NODE_ID].colour_next == kmem_cache.colour) ? ? ? ?kmem_cache.node[NODE_ID].colour_next = 0;
本地CPU空閑對象鏈表
現(xiàn)在說說本地CPU空閑對象鏈表。這個在kmem_cache結(jié)構(gòu)中用cpu_cache表示,整個數(shù)據(jù)結(jié)構(gòu)是struct array_cache,它的目的是將釋放的對象加入到這個鏈表中,我們可以先看看數(shù)據(jù)結(jié)構(gòu):
struct array_cache {
/* 可用對象數(shù)目 */
unsigned int avail;
/* 可擁有的最大對象數(shù)目,和kmem_cache中一樣 */
unsigned int limit;
/* 同kmem_cache,要轉(zhuǎn)移進本地高速緩存或從本地高速緩存中轉(zhuǎn)移出去的對象的數(shù)量 */
unsigned int batchcount;
/* 是否在收縮后被訪問過 */
unsigned int touched;
/* 偽數(shù)組,初始沒有任何數(shù)據(jù)項,之后會增加并保存釋放的對象指針 */
void *entry[]; /*
};
這個本地CPU空閑對象鏈表的存在與伙伴系統(tǒng)中的每CPU頁框分配器的存在原因一樣,都有兩點:
每個CPU都有它們自己的硬件高速緩存,當(dāng)此CPU上釋放對象時,可能這個對象很可能還在這個CPU的硬件高速緩存中,所以內(nèi)核為每個CPU維護一個這樣的鏈表,當(dāng)需要新的對象時,會優(yōu)先嘗試從當(dāng)前CPU的本地CPU空閑對象鏈表獲取相應(yīng)大小的對象。
減少鎖的競爭,試想一下,假設(shè)多個CPU同時申請一個大小的slab,這時候如果沒有本地CPU空閑對象鏈表,就會導(dǎo)致分配流程是互斥的,需要上鎖,就導(dǎo)致分配效率低。
這個本地CPU空閑對象鏈表在系統(tǒng)初始化完成后是一個空的鏈表,只有釋放對象時才會將對象加入這個鏈表。當(dāng)然,鏈表對象個數(shù)也是有所限制,其最大值就是limit,鏈表數(shù)超過這個值時,會將batchcount個數(shù)的對象返回到所有CPU共享的空閑對象鏈表(也是這樣一個結(jié)構(gòu))中。
注意在array_cache中有一個entry數(shù)組,里面保存的是指向空閑對象的首地址的指針,注意這個鏈表是在kmem_cache結(jié)構(gòu)中的,也就是kmalloc-8有它自己的本地CPU高速緩存鏈表,dquot也有它自己的本地CPU高速緩存鏈表,每種類型kmem_cache都有它自己的本地CPU空閑對象鏈表。
所有CPU共享的空閑對象鏈表
原理和本地CPU空閑對象鏈表一樣,唯一的區(qū)別就是所有CPU都可以從這個鏈表中獲取對象,一個常規(guī)的對象申請流程是這樣的:系統(tǒng)首先會從本地CPU空閑對象鏈表中嘗試獲取一個對象用于分配;如果失敗,則嘗試來到所有CPU共享的空閑對象鏈表鏈表中嘗試獲??;如果還是失敗,就會從SLAB中分配一個;這時如果還失敗,kmem_cache會嘗試從頁框分配器中獲取一組連續(xù)的頁框建立一個新的SLAB,然后從新的SLAB中獲取一個對象。對象釋放過程也類似,首先會先將對象釋放到本地CPU空閑對象鏈表中,如果本地CPU空閑對象鏈表中對象過多,kmem_cache會將本地CPU空閑對象鏈表中的batchcount個對象移動到所有CPU共享的空閑對象鏈表鏈表中,如果所有CPU共享的空閑對象鏈表鏈表的對象也太多了,kmem_cache也會把所有CPU共享的空閑對象鏈表鏈表中batchcount個數(shù)的對象移回它們自己所屬的SLAB中,這時如果SLAB中空閑對象太多,kmem_cache會整理出一些空閑的SLAB,將這些SLAB所占用的頁框釋放回頁框分配器中。
這個所有CPU共享的空閑對象鏈表也不是肯定會有的,kmem_cache中有個shared字段如果為1,則這個kmem_cache有這個高速緩存,如果為0則沒有。
總結(jié)
整個框架已經(jīng)說明結(jié)束了,我們用一幅圖進行整理:
?

?
?
?
評論