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