linux內存管理筆記(二十七)----slub分配器概述【轉】


轉自:https://blog.csdn.net/u012489236/article/details/107966849

在linux的內核運行需要動態分配內存的時候,其中有兩種分配方案:

第一種是以頁為單位分配內存,即一次分配內存的大小必須是頁的整數倍
第二種是按需分配,一次分配的內存大小是隨機的
​ 對於第一種方案是通過伙伴系統實現,以頁為單位管理和分配內存,但是這個單位確實也太大了。對於第二種方案,在現實的需求中,如果我們要為一個10個字符的字符串分配空間,如果按照伙伴系統采用分配一個4KB或者更多空間的完整頁面,不僅浪費而且完全不可接受。顯然的解決方案是需要將頁拆分為更小的單位,可以容納大量的小對象。同時新的解決方案月不能給內核帶來更大的開銷,不能對系統性能產生影響,同時也必須保證內存的利用率和效率。基於此,slab的分配器就應運而生,該機制是並沒有脫離伙伴系統,是基於伙伴系統分配的大內存進一步細化分成小內存分配,SLAB 就是為了解決這個小粒度內存分配的問題的。

slab分配器對許多可能的工作負荷都能很好工作,但是有一些場景,也無法提供最優化的性能。如果某些計算機處於當前硬件尺度的邊界上,slab分配器就會出現一些問題。同時對於嵌入式系統來說slab分配器代碼量和復雜度都太高,所以內核增加了兩個替代品,所以目前有三種實現算法,分別是slab、slub、slob,並且,依據它們各自的分配算法,在適用性方面會有一定的側重。

Slab是最基礎的,最早基於Bonwick的開創性論文並且可用 自Linux內核版本2.2起。
slob是被改進的slab,針對嵌入式系統進行了特別優化,以便減小代碼量。圍繞一個簡單的內存塊鏈表展開,在分配內存時,使用同樣簡單的最新適配算法。slob分配器只有大約600行代碼,總的代碼量很小。從速度來說,它不是最高效的分配器,頁肯定不是為大型系統設計的。
slub是在slab上進行的改進簡化,在大型機上表現出色,並且能更好的使用NUMA系統,slub相對於slab有5%-10%的性能提升和減小50%的內存占用
文章代碼分析基於以linux-4.9.88,以NXP的IMX系列硬件,分析slub的工作原理。

1. slub數據結構
要想理解slub分配器,首先需要了解slub分配器的核心結構體,kmem_cache的結構體定義如下

struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab;
/* Used for retriving partial slabs etc */
unsigned long flags;
unsigned long min_partial;
int size; /* The size of an object including meta data */
int object_size; /* The size of an object without meta data */
int offset; /* Free pointer offset. */
int cpu_partial; /* Number of per cpu partial objects to keep around */
struct kmem_cache_order_objects oo;

/* Allocation and freeing of slabs */
struct kmem_cache_order_objects max;
struct kmem_cache_order_objects min;
gfp_t allocflags; /* gfp flags to use on each alloc */
int refcount; /* Refcount for slab cache destroy */
void (*ctor)(void *);
int inuse; /* Offset to metadata */
int align; /* Alignment */
int reserved; /* Reserved bytes at the end of slabs */
const char *name; /* Name (only for display!) */
struct list_head list; /* List of slab caches */
int red_left_pad; /* Left redzone padding size */
struct kmem_cache_node *node[MAX_NUMNODES];
}

結構體成員變量 含義
cpu_slab 一個per cpu變量,對於每個cpu來說,相當於一個本地內存緩存池。當分配內存的時候優先從本地cpu分配內存以保證cache的命中率。
flags object分配掩碼
min_partial 限制struct kmem_cache_node中的partial鏈表slab的數量。如果大於這個mini_partial的值,那么多余的slab就會被釋放。
size 分配的object size
object_size 實際的object size
offset offset就是存儲下個object地址數據相對於這個object首地址的偏移。
cpu_partial per cpu partial中所有slab的free object的數量的最大值,超過這個值就會將所有的slab轉移到kmem_cache_node的partial鏈表
oo oo用來存放分配給slub的頁框的階數(高16位)和 slub中的對象數量(低16位)
min 當按照oo大小分配內存的時候出現內存不足就會考慮min大小方式分配。min只需要可以容納一個object即可。
allocflags 從伙伴系統分配內存掩碼。
list 有一個slab_caches鏈表,所有的slab都會掛入此鏈表。
node slab節點。在NUMA系統中,每個node都有一個struct kmem_cache_node數據結構
在該結構體中,有一個變量struct list_head list,可以想象下,對於操作系統來講,要創建和管理的緩存不至於task_struct,對於mm_struct,fs_struct都需要這個結構體,所有的緩存最后都會放到這個鏈表中,也就是LIST_HEAD(slab_caches)。對於緩存中哪些對象被分配,哪些是空着,什么情況下整個大內存塊都被分配完了,需要向伙伴系統申請幾個頁形成新的大內存塊?這些信息該由誰來維護呢??就引出了兩個成員變量kmem_cache_cpu和kmem_cache_node。

在分配緩存的時候,需要分兩種路徑,快速通道(kmem_cache_cpu)和普通通道(kmem_cache_node),每次分配的時候,要先從kmem_cache_cpu分配;如果kmem_cache_cpu里面沒有空閑塊,那就從kmem_cache_node中進行分配;如果還是沒有空閑塊,最后從伙伴系統中分配新的頁。

cpu_cache對於每個CPU來說,相當於一個本地內存緩沖池,當分配內存的時候,優先從本地CPU分配內存以及保證cache的命中率,struct kmem_cache_cpu用於管理slub緩存

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 */
struct page *partial; /* Partially allocated frozen slabs */
#ifdef CONFIG_SLUB_STATS
unsigned stat[NR_SLUB_STAT_ITEMS];
#endif
};

結構體成員變量 含義
freelist 指向本地CPU的第一個空閑對象,這一項會有指針指向下一個空閑的項,最終所有空閑的項會形成一個鏈表
tid 主要用來同步
page 指向大內存塊的第一個頁,緩存塊就是從里面分配的
partial 大內存塊的第一個頁,之所以名字叫 partial(部分),就是因為它里面部分被分配出去了,部分是空的。這是一個備用列表,當 page 滿了,就會從這里找
struct kmem_cache_node:用於管理每個Node的slub頁面,由於每個Node的訪問速度不一致,slub頁面由Node來管理;

struct kmem_cache_node {
spinlock_t list_lock;
#ifdef CONFIG_SLUB
unsigned long nr_partial; /* partial slab鏈表中slab的數量 */
struct list_head partial; /* partial slab鏈表表頭 */
#ifdef CONFIG_SLUB_DEBUG
atomic_long_t nr_slabs; /* 節點中的slab數 */
atomic_long_t total_objects; /* 節點中的對象數 */
struct list_head full; /* full slab鏈表表頭 */
#endif
#endi
}

結構體成員變量 含義
list_lock 自旋鎖,保護數據
nr_partial partial slab鏈表中slab的數量
partial 這個鏈表里存放的是部分空閑的大內存塊。這是 kmem_cache_cpu 里面的 partial 的備用列表,如果那里沒有,就到這里來找。
其結構圖如下圖所示

 

 


2. 初始化
為了初始化slub的數據結構,內核需要若干遠小於一整頁的內存塊,這些最適合使用kmalloc來分配。但是此時只有slub系統已經完成初始化后,才能使用kmalloc。換而言之,kmalloc只能在kmalloc已經初始化之后初始化,這個是不可能,所以內核使用kmem_cache_init函數用於初始化slub分配器。
分配器的初始化工作主要是初始化用於kmalloc的gerneral cache,slub分配器的gerneral cache定義如下:

extern struct kmem_cache *kmalloc_caches[KMALLOC_SHIFT_HIGH + 1];
#define KMALLOC_SHIFT_HIGH (PAGE_SHIFT + 1)
#define PAGE_SHIFT 12
//(各個架構下的定義都有些差異,如果是arm64,那么是通過CONFIG_ARM64_PAGE_SHIFT來指定的,這個配置項在arch/arm64/Kconfig文件中定義,默認為12,也就是默認頁面大小為4KiB,筆者以arm64為例)

那么KMALLOC_SHIFT_HIGH=PAGE_SHIFT + 1 = 12 + 1 = 13,KMALLOC_SHIFT_HIGH+1=13+ 1= 14說明kmalloc_caches數組中有14個元素,每個元素是kmem_cache這個結構體

它在內核初始化階段(start_kernel)、伙伴系統啟用之后調用,它首先執行緩存初始化過程,如下圖所示

 

 


從緩存中分配kmem_cache對象,並復制並使用臨時kmem_cache
從緩存中分配kmem_cache_node對象,然后復制並使用臨時使用的kmem_cache_node
kmalloc boot cache
起初並沒有boot cache,因此定義了兩個靜態變量(boot_kmem_cache,boot_kmem_cache_node)用於臨時使用。這里的核心是boot cache創建函數:create_boot_cache()

 

 


當調用create_boot_cache創建完kmem_cache和kmem_cache_node兩個Cache后,就需要調用bootstrap從Cache中為kmem_cache和kmem_cache_node分配內存空間然后將靜態變量boot_kmem_cache和boot_kmem_cache_node中的內容復制到分配的內存空間中,這相當於完成了一次對自身的重建。

static struct kmem_cache * __init bootstrap(struct kmem_cache *static_cache)
{
int node;
struct kmem_cache *s = kmem_cache_zalloc(kmem_cache, GFP_NOWAIT); --------------------(1)
struct kmem_cache_node *n;

memcpy(s, static_cache, kmem_cache->object_size);

/*
* This runs very early, and only the boot processor is supposed to be
* up. Even if it weren't true, IRQs are not up so we couldn't fire
* IPIs around.
*/
__flush_cpu_slab(s, smp_processor_id());
for_each_kmem_cache_node(s, node, n) { --------------------(2)
struct page *p;

list_for_each_entry(p, &n->partial, lru)
p->slab_cache = s;

#ifdef CONFIG_SLUB_DEBUG
list_for_each_entry(p, &n->full, lru)
p->slab_cache = s;
#endif
}
slab_init_memcg_params(s);
list_add(&s->list, &slab_caches);
return s;
}

1.首先將會通過kmem_cache_zalloc()申請kmem_cache空間,值得注意的是該函數申請調用kmem_cache_zalloc()->kmem_cache_alloc()->slab_alloc(),其最終將會通過前面create_boot_cache()初始化創建的kmem_cache來申請slub空間來使用。臨時使用的kmem_cache結構形式接收的參數static_cache的內容復制到新分配的緩存中,其大小與object_size相同。早期引導過程中,因此無法對其他CPU進行IPI調用,因此僅刷新本地CPU。

2.通過for_each_node_state()遍歷各個內存管理節點node,在通過get_node()獲取對應節點的slab,如果slab不為空這回遍歷部分滿slab鏈,修正每個slab指向kmem_cache的指針,如果開啟CONFIG_SLUB_DEBUG,則會遍歷滿slab鏈,設置每個slab指向kmem_cache的指針;最后將kmem_cache添加到全局slab_caches鏈表中。

接下來是創建kmalloc boot cache - create_kmalloc_caches(),來初始化kmalloc_caches表,其最終創建的kmalloc_caches是以{0,96,192,8,16,32,64,128,256,512,1024,2046,4096,8196}為大小的slab表;創建完之后,將設置slab_state為UP,然后將kmem_cache的name成員進行初始化;最后如果配置了CONFIG_ZONE_DMA,將會初始化創建kmalloc_dma_caches表。可以得到size_index與kmalloc_caches的對應關系

 

 

 

我們以通常情況下KMALLOC_MIN_SIZE等於8為例進行說明。size_index[0-23]數組根據對象大小映射到不同的kmalloc_caches[0-13]保存的cache。觀察kmalloc_caches[0-13]數組,可見索引即該cache slab塊的order。由於最小對象為8(23)字節,kmalloc_caches[0-2]這三個數組元素沒有用到,slub使用kmalloc_caches[1]保存96字節大小的對象,kmalloc_caches[2] 保存192字節大小的對象,相當於細分了kmalloc的粒度,有利於減少空間的浪費。kmalloc_caches[0]未使用。

3. 總結
slab分配器中用到了對象這個概念,就是內核中的數據結構以及對該數據結構進行創建和撤銷的操作。其核心思想如下

將內核中經常使用的對象放到高速緩存中,並且由系統保持為初始的可利用狀態,比如進程描述符,內核中會頻繁對此數據進行申請和釋放
當一個新進場創建時,內核就會直接從slab分配器的高速緩存中獲取一個已經初始化的對象
當進程結束時,該結構所占的頁框並不被釋放,而是重新返回slab分配器中,如果沒有基於對象的slab分配器,內核將花費更多的時間去分配、初始化、已經釋放對象。

 

 


上圖顯示了slab、cache及object 三者之間的關系。該圖顯示了2個大小為3KB 的內核對象和3個大小為7KB的對象,它們位於各自的cache中。slab分配算法采用 cache來存儲內核對象。在創建 cache 時,若干起初標記為free的對象被分配到 cache。cache內的對象數量取決於相關slab的大小。例如,12KB slab(由3個連續的4KB頁面組成)可以存儲6個2KB對象。最初,cache內的所有對象都標記為空閑。當需要內核數據結構的新對象時,分配器可以從cache上分配任何空閑對象以便滿足請求。從cache上分配的對象標記為used(使用)。

讓我們考慮一個場景,這里內核為表示進程描述符的對象從slab分配器請求內存。在 Linux 系統中,進程描述符屬於 struct task_struct 類型,它需要大約1.7KB的內存。當Linux內核創建一個新任務時,它從cache中請求 struct task_struct對象的必要內存。cache 利用已經在slab中分配的並且標記為 free (空閑)的 struct task_struct對象來滿足請求。
在Linux中,slab可以處於三種可能狀態之一:

滿的:slab的所有對象標記為使用。
空的:slab上的所有對象標記為空閑。
部分:slab上的對象有的標記為使用,有的標記為空閑。
slab分配器首先嘗試在部分為空的slab中用空閑對象來滿足請求。如果不存在,則從空的slab 中分配空閑對象。如果沒有空的slab可用,則從連續物理頁面分配新的slab,並將其分配給cache;從這個slab上,再分配對象內存。slab分配器提供兩個主要優點:

減小伙伴算法在分配小塊連續內存時所產生的內部碎片問題,因為每個內核數據結構都有關聯的cache,每個 cache都由一個或多個slab組成,而slab按所表示對象的大小來分塊。因此,當內核請求對象內存時,slab 分配器可以返回剛好表示對象的所需內存。
將頻繁使用的對象緩存起來,減小分配、初始化和釋放的時間開銷 ,當對象頻繁地被分配和釋放時,如來自內核請求的情況,slab 分配方案在管理內存時特別有效。分配和釋放內存的動作可能是一個耗時過程。然而,由於對象已預先創建,因此可以從cache 中快速分配。再者,當內核用完對象並釋放它時,它被標記為空閑並返回到cache,從而立即可用於后續的內核請求。
對於伙伴系統和slab分配器,就好比“批發商”和“零售商”,“批發商”,是指按頁面管理並分配內存的機制;而“零售商”,則是從“批發商”那里批發獲取資源,並以字節為單位,管理和分配內存的機制。作為零售商的slab,那么就需要解決兩個問題

該如何從批發商buddy system批發內存
如何管理批發的內存並把這些內存“散賣“出去,如何使這些散內存由更高的使用效率
4. 參考文檔
趣談Linux操作系統
Linux內存管理:slub分配器
操作系統內存分配算法_操作系統基礎45-伙伴系統和slab內存分配
————————————————
版權聲明:本文為CSDN博主「奇小葩」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/u012489236/article/details/107966849


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM