之前說了管理區頁框分配器,這裏我們簡稱爲頁框分配器,在頁框分配器中主要是管理物理內存,將物理內存的頁框分配給申請者,而且我們知道也可頁框大小爲4K(也可設置爲4M),這時候就會有個問題,如果我只需要1KB大小的內存,頁框分配器也不得不分配一個4KB的頁框給申請者,這樣就會有3KB被白白浪費掉了。爲了應對這種情況,在頁框分配器上一層又做了一層SLAB層,SLAB分配器的作用就是從頁框分配器中拿出一些頁框,專門把這些頁框拆分成一小塊一小塊的小內存,當申請者申請的是小內存時,系統就會從SLAB中獲取一小塊分配給申請者。它們的整個關係如下圖:
可以看出,SLAB分配器和頁框分配器並沒有什麼直接的聯繫,對於頁框分配器來說,SLAB分配器也只是一個從它那裏申請頁框的申請者而已。
命令結果如下:
命令結果如下:如剛纔所有,我們看到有些SLAB的名字比較特別,如TCP,UDP,dquot這些,它們都是專用SLAB,專屬於它們自己的模塊。而後面這張圖,如kmalloc-8,kmalloc-16...還有dma-kmalloc-96,dma-kmalloc-192...這些都是普通SLAB,當需要爲一些小數據分配內存時(比如一個結構體),就會從這些普通SLAB中獲取內存。值得注意的是,對於kmalloc-8這些普通SLAB,都有一個對應的dma-kmalloc-8這種類型的普通SLAB,這種類型是專門使用了ZONE-DMA區域的內存,方便用於DMA模式申請內存。在SLAB中,可分配的內存塊稱之爲對象,在後面那張圖中,如kmalloc-8這個普通SLAB,裏面所有的對象都是8B大小,同理,kmalloc-16中的對象都是以16B爲大小。當你申請1B~8B的內存時,系統會從kmalloc-8中分配一個對象給你,當你申請8B~16B的內存時,系統會從kmalloc-16裏給你分配。雖然即使申請5B,分配了一個8B的對象,還有3B空閒,但這樣設計已經大大減小了內存碎片化了,保證了碎片內存不會超過50%(kmalloc-8除外)。需要注意,在kmalloc-8中申請到的對象,釋放時也會回到kmalloc-8中。
除了減小了內存碎片化,SLAB還有一個作用,提高了系統的效率,當對象擁有者釋放一個對象後,SLAB的處理是僅僅標記對象爲空閒,並不做多少處理,而又有申請者申請相應大小的對象時,SLAB會優先分配最近釋放的對象,這樣這個對象甚至有可能還在硬件高速緩存中,有點類似管理區頁框分配器中每CPU高速緩存的做法。
kmem_cache結構
雖然叫SLAB分配器,但是在SLAB分配器中,最頂層的數據結構卻不是SLAB,而是kmem_cache,我們暫且叫它SLAB緩存吧,每個SLAB緩存都有它自己的名字,就是上圖中的kmalloc-8,kmalloc-16等。總的來說,kmem_cache結構用於描述一種SLAB,並且管理着這種SLAB中所有的對象。所有的kmem_cache結構會保存在以slab_caches作爲頭的鏈表中。在內核模塊中可以通過kmem_cache_create自行創建一個kmem_cache用於管理屬於自己模塊的SLAB。
我們先看看kmem_cache結構:
/* slab分配器中的SLAB高速緩存 */ struct kmem_cache { /* 指向包含空閒對象的本地高速緩存,每個CPU有一個該結構,當有對象釋放時,優先放入本地CPU高速緩存中 */ struct array_cache __percpu *cpu_cache; /* 1) Cache tunables. Protected by slab_mutex */ /* 要轉移進本地高速緩存或從本地高速緩存中轉移出去的對象的數量 */ unsigned int batchcount; /* 本地高速緩存中空閒對象的最大數目 */ unsigned int limit; /* 是否存在CPU共享高速緩存,CPU共享高速緩存指針保存在kmem_cache_node結構中 */ unsigned int shared; /* 對象長度 + 填充字節 */ unsigned int size; /* size的倒數,加快計算 */ struct reciprocal_value reciprocal_buffer_size; /* 2) touched by every alloc & free from the backend */ /* 高速緩存永久屬性的標識,如果SLAB描述符放在外部(不放在SLAB中),則CFLAGS_OFF_SLAB置1 */ unsigned int flags; /* constant flags */ /* 每個SLAB中對象的個數(在同一個高速緩存中slab中對象個數相同) */ unsigned int num; /* # of objs per slab */ /* 3) cache_grow/shrink */ /* 一個單獨SLAB中包含的連續頁框數目的對數 */ unsigned int gfporder; /* 分配頁框時傳遞給夥伴系統的一組標識 */ gfp_t allocflags; /* SLAB使用的顏色個數 */ size_t colour; /* SLAB中基本對齊偏移,當新SLAB着色時,偏移量的值需要乘上這個基本對齊偏移量,理解就是1個偏移量等於多少個B大小的值 */ unsigned int colour_off; /* 空閒對象鏈表放在外部時使用,其指向的SLAB高速緩存來存儲空閒對象鏈表 */ struct kmem_cache *freelist_cache; /* 空閒對象鏈表的大小 */ unsigned int freelist_size; /* 構造函數,一般用於初始化這個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 */ /* 統計 */ #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 /* 結點鏈表,此高速緩存可能在不同NUMA的結點都有SLAB鏈表 */ struct kmem_cache_node *node[MAX_NUMNODES]; };從結構中可以看出,在這個kmem_cache中所有對象的大小是相同的(object_size),並且此kmem_cache中所有SLAB的大小也是相同的(gfporder、num)。
在這個結構中,最重要的可能就屬struct kmem_cache_node * node[Max_NUMNODES]這個指針數組了,指向的struct kmem_cache_node中保存着slab鏈表,在NUMA架構中每個node對應數組中的一個元素,因爲每個SLAB高速緩存都有可能在不同結點維護有自己的SLAB用於這個結點的分配。我們看看struct kmem_cache_node:
/* SLAB鏈表結構 */ struct kmem_cache_node { /* 鎖 */ spinlock_t list_lock; /* SLAB用 */ #ifdef CONFIG_SLAB /* 只使用了部分對象的SLAB描述符的雙向循環鏈表 */ struct list_head slabs_partial; /* partial list first, better asm code */ /* 不包含空閒對象的SLAB描述符的雙向循環鏈表 */ struct list_head slabs_full; /* 只包含空閒對象的SLAB描述符的雙向循環鏈表 */ struct list_head slabs_free; /* 高速緩存中空閒對象個數(包括slabs_partial鏈表中和slabs_free鏈表中所有的空閒對象) */ unsigned long free_objects; /* 高速緩存中空閒對象的上限 */ unsigned int free_limit; /* 下一個被分配的SLAB使用的顏色 */ unsigned int colour_next; /* Per-node cache coloring */ /* 指向這個結點上所有CPU共享的一個本地高速緩存 */ struct array_cache *shared; /* shared per node */ struct alien_cache **alien; /* on other nodes */ /* 兩次緩存收縮時的間隔,降低次數,提高性能 */ 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 };在SLAB描述符中,最重要的可能就是s_mem和freelist這兩個指針。s_mem用於指向這段連續頁框中第一個對象,freelist指向空閒對象鏈表。
空閒對象鏈表是一個由數組製成的簡單鏈表,它保存的地方有兩種情況:
- 保存在外部,會從SLAB中分配一個對象用於保存新的SLAB的空閒對象鏈表。
- 保存在內部,保存在這個SLAB所代表的連續頁框的頭部。
本地CPU空閒對象鏈表
現在說說本地CPU空閒對象鏈表。這個在kmem_cache結構中用cpu_cache表示,整個數據結構是struct array_cache,它的目的是將釋放的對象加入到這個鏈表中,我們可以先看看數據結構:
struct array_cache { /* 可用對象數目 */ unsigned int avail; /* 可擁有的最大對象數目,和kmem_cache中一樣 */ unsigned int limit; /* 同kmem_cache,要轉移進本地高速緩存或從本地高速緩存中轉移出去的對象的數量 */ unsigned int batchcount; /* 是否在收縮後被訪問過 */ unsigned int touched; /* 僞數組,初始沒有任何數據項,之後會增加並保存釋放的對象指針 */ void *entry[]; /* };
因爲每個CPU都有它們自己的硬件高速緩存,當此CPU上釋放對象時,可能這個對象很可能還在這個CPU的硬件高速緩存中,所以內核爲每個CPU維護一個這樣的鏈表,當需要新的對象時,會優先嚐試從當前CPU的本地CPU空閒對象鏈表獲取相應大小的對象。這個本地CPU空閒對象鏈表在系統初始化完成後是一個空的鏈表,只有釋放對象時纔會將對象加入這個鏈表。當然,鏈表對象個數也是有所限制,其最大值就是limit,鏈表數超過這個值時,會將batchcount個數的對象返回到所有CPU共享的空閒對象鏈表(也是這樣一個結構)中。
注意在array_cache中有一個entry數組,裏面保存的是指向空閒對象的首地址的指針,注意這個鏈表是在kmem_cache結構中的,也就是kmalloc-8有它自己的本地CPU高速緩存鏈表,dquot也有它自己的本地CPU高速緩存鏈表,每種類型kmem_cache都有它自己的本地CPU空閒對象鏈表。
所有CPU共享的空閒對象鏈表
原理和本地CPU空閒對象鏈表一樣,唯一的區別就是所有CPU都可以從這個鏈表中獲取對象,一個常規的對象申請流程是這樣的:系統首先會從本地CPU空閒對象鏈表中嘗試獲取一個對象用於分配;如果失敗,則嘗試來到所有CPU共享的空閒對象鏈表鏈表中嘗試獲取;如果還是失敗,就會從SLAB中分配一個;這時如果還失敗,kmem_cache會嘗試從頁框分配器中獲取一組連續的頁框建立一個新的SLAB,然後從新的SLAB中獲取一個對象。對象釋放過程也類似,首先會先將對象釋放到本地CPU空閒對象鏈表中,如果本地CPU空閒對象鏈表中對象過多,kmem_cache會將本地CPU空閒對象鏈表中的batchcount個對象移動到所有CPU共享的空閒對象鏈表鏈表中,如果所有CPU共享的空閒對象鏈表鏈表的對象也太多了,kmem_cache也會把所有CPU共享的空閒對象鏈表鏈表中batchcount個數的對象移回它們自己所屬的SLAB中,這時如果SLAB中空閒對象太多,kmem_cache會整理出一些空閒的SLAB,將這些SLAB所佔用的頁框釋放回頁框分配器中。
這個所有CPU共享的空閒對象鏈表也不是肯定會有的,kmem_cache中有個shared字段如果爲1,則這個kmem_cache有這個高速緩存,如果爲0則沒有。