1. 程式人生 > 其它 >核心記憶體分配器SLAB和SLUB

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