內核記憶體分配器SLAB和SLUB
內核分配器的功能
在作業系統管理的虛擬記憶體中,用於記憶體管理的最小單位是頁,大多數傳統的架構是4KB。由於進程每次申請分配4KB是不現實的,比如分配幾個位元組或幾十個位元組,這時需要中間機制來管理頁面的微型記憶體。
為此,內核實現了一個分配器來管理頁中碎片記憶體的分配和回收。可以把分配器理解為一個零售供應商:它收購大量的庫存(4KB大小的頁),然後在模組需要時分成小塊出售。這種分配的基本版就是SLAB。
SLAB
當內核子系統為對象請求、釋放數據時,主要的開銷在於初始化、銷毀過程,而不是為對象分配的記憶體。如果有一組經常使用的內核對象,那麼可以將它們保存在一個可快速使用的地方,使整個過程更有效率。
這就是SLAB的原理:分配器跟蹤這些塊,稱為快取,當收到為某種類型的數據對象分配記憶體的請求時,它可以立即使用已經分配過的來滿足請求。這種情況下,SLAB是記憶體中包含預先分配的記憶體塊的一個或多個連續頁面。
SLAB可能存在以下狀態之一:
- empty:SLAB中所有對象都是空閑的
- partial:SLAB中包含被分配的對象和空閑對象
- full:SLAB中所有對象都被分配
分配器的目的是儘可能快的處理請求,因此跟蹤過程至關重要,這個過程通過快取完成,而每種對象類型都有一個快取。
SLUB
SLUB是SLAB的變體,旨在實現更好的調試、更少的碎片和更好的性能。它沿用SLAB的基本功能,優化了SLAB中多處理器的設計缺陷。自從2008年Linux 2.6.23以來SLUB被設置為默認分配器。
接下來會觀察SLUB的實現細節,並通過常用的場景給出示例。
SLAB中的對象通過鏈表互相連接,這樣分配器總是可以找到下一個空閑對象,而不需要關心已經使用的數據:
SLUB和SLAB不同:指向下一個空閑對象的指針直接存儲在對象本身內部的結構體中,並不需要額外的記憶體空間進行存儲,且保證SLAB功能100%的利用效率。在某些特殊情況,指針存儲在對象結構體中的一個偏移量裡面,這根據不同平台的CPU而定。
上圖中objsize
表示對象自身的大小,offset
是next指針之前的空間大小,size
是總大小。
所有的這些資訊,以及更多的資訊,都存儲在一個kmem_cache
結構體中,它的結構體定義如下:
/*
* Slab cache management.
*/
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab;
/* Used for retrieving partial slabs, etc. */
slab_flags_t flags;
unsigned long min_partial;
unsigned int size; /* The size of an object including metadata */
unsigned int object_size;/* The size of an object without metadata */
unsigned int offset; /* Free pointer offset */
......
struct kmem_cache_node *node[MAX_NUMNODES];
}
每個對象有且只有一個kmem_cache
,並且該對象的所有slab都由相同的kmem_cache
管理,這些結構體通過雙向鏈表互相鏈接,可以通過導出的slab_caches
變數從內核中的任何位置訪問。slab_caches
定義如下:
extern struct list_head slab_caches; // list_head用於管理雙向鏈表
在kmem_cache
結構體中,存儲了兩種指針以跟蹤對象:一個kmem_cache_node
數組,是結構體最後一個成員struct kmem_cache_node *node[MAX_NUMNODES]
;另一個是指向kmem_cache_cpu
的指針,結構體第一個成員struct kmem_cache_cpu __percpu *cpu_slab
。
-
kmem_cache_node
跟蹤不活動的partial和full的對象,在空閑的情況被訪問,或者當活動的slab被填滿時用另一個partial替換它:/* * The slab lists for all objects. */ struct kmem_cache_node { spinlock_t list_lock; #ifdef CONFIG_SLAB struct list_head slabs_partial; /* partial list first, better asm code */ struct list_head slabs_full; struct list_head slabs_free; unsigned long total_slabs; /* length of all slab lists */ unsigned long free_slabs; /* length of free slab list only */ unsigned long free_objects; unsigned int free_limit; unsigned int colour_next; /* Per-node cache coloring */ struct array_cache *shared; /* shared per node */ struct alien_cache **alien; /* on other nodes */ unsigned long next_reap; /* updated without locking */ int free_touched; /* updated without locking */ #endif #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 };
-
kmem_cache_cpu
管理活動的slab,它只有一個,並且與當前的CPU相關(不同的處理器有不同的快取)。下一次申請始終由freelist
欄位指向的slab返回:struct kmem_cache_cpu { void **freelist; /* Pointer to next available object */ unsigned long tid; /* Globally unique transaction id */ struct page *page; /* The slab from which we are allocating */ #ifdef CONFIG_SLUB_CPU_PARTIAL struct page *partial; /* Partially allocated frozen slabs */ #endif #ifdef CONFIG_SLUB_STATS unsigned stat[NR_SLUB_STAT_ITEMS]; #endif };
以下是kmem_cache
結構體成員的關係:
例子
-
普通分配
分配器從
kmem_cache
找到kmem_cache_cpu
訪問freelist
找到第一個空閑對象,返回該對象(藍色部分)。相應的更新指針將其從鏈表中刪除,並將freelist
指向下一個空閑對象:
-
分配即將滿的對象
返回最後一個對象後,已填滿的頁面將移動到full list中,將另一個partial list置為活動的slab:
-
申請即將滿的partial list裡面的對象,並且沒有其他partial list時
返回最後一個活動的對象之後,已填滿的頁會放入full list,然後系統分配一個全新的slab成為活動slab:
-
普通釋放
當釋放一個屬於partial list(或活動)的對象時,SLUB只是將其標記為空閑並更新指針:
-
釋放即將為empty list的對象
當釋放屬於partial list的最後一個對象時,slab被釋放,交給記憶體管理單元:
-
在full list裡面釋放
當釋放full list裡面的對象時,釋放後它不再是full list,將其放入partial list: