內核記憶體分配器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: