1. 程式人生 > 其它 >linux記憶體管理筆記(二十七)----slub分配器概述【轉】

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

【作者】張昺華 【出處】http://www.cnblogs.com/sky-heaven/ 【部落格園】 http://www.cnblogs.com/sky-heaven/ 【知乎】 http://www.zhihu.com/people/zhang-bing-hua 【我的作品---旋轉倒立擺】 http://v.youku.com/v_show/id_XODM5NDAzNjQw.html?spm=a2hzp.8253869.0.0&from=y1.7-2 【我的作品---自平衡自動循跡車】 http://v.youku.com/v_show/id_XODM5MzYyNTIw.html?spm=a2hzp.8253869.0.0&from=y1.7-2 【大餅教你學系列】https://edu.csdn.net/course/detail/10393 【新浪微博】 張昺華--sky 【twitter】 @sky2030_ 【微信公眾號】 張昺華 本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利.