1. 程式人生 > >Linux記憶體管理之SLAB記憶體分配器

Linux記憶體管理之SLAB記憶體分配器

一、前言

1、 為什麼需要SLAB記憶體分配器

slab記憶體分配器是linux核心中比較經典的記憶體分配器(目前已經被slub記憶體分配器取代了)。之所以提出slab分配器,是因為buddy system只能按page對齊來分配記憶體。然而大多數情況下,需要的記憶體size都不是按page對齊的,如果直接通過buddy system分配記憶體,就會出現很大的記憶體碎片,記憶體碎片即分配了卻沒有使用,也無法再被分配的記憶體。正是由於buddy system的這種限制,slab分配器應運而生。slab分配器的底層依託於buddy system,上層卻對使用者提供了更加靈活的記憶體分配服務。如下圖:

這裡寫圖片描述

2、 SLAB記憶體分配器作用

提供小記憶體塊不是slab分配器的唯一任務. 由於結構上的特點. 它也用作一個快取. 主要針對經常分配並釋放的物件. 通過建立slab快取, 核心能夠儲備一些物件, 供後續使用, 即使在初始化狀態, 也是如此。

舉例來說, 為管理與程序關聯的檔案系統資料, 核心必須經常生成struct fs_struct的新例項. 此型別例項佔據的記憶體塊同樣需要經常回收(在程序結束時). 換句話說, 核心趨向於非常有規律地分配並釋放大小為sizeof{fs_struct}的記憶體塊. slab分配器將釋放的記憶體塊儲存在一個內部列表中. 並不馬上返回給夥伴系統. 在請求為該類物件分配一個新例項時, 會使用最近釋放的記憶體塊. 這有兩個優點. 首先, 由於核心不必使用夥伴系統演算法, 處理時間會變短. 其次, 由於該記憶體塊仍然是”新”的,因此其仍然駐留在CPU快取記憶體的概率較高.

3、 SLAB記憶體分配器工作機制

  • 從buddy system分配pages,放入slab分配器記憶體池,也可以稱為cache
  • 使用者呼叫slab分配器提供的記憶體分配介面如kmalloc,從slab分配器記憶體池中分配記憶體,記憶體的size沒有按page size對齊的要求。

注:本文說明的cache快取指的並不是真正的快取,真正的快取指的是硬體快取,也就是我們通常所說的L1 cache、L2 cache、L3 cache,硬體快取是為了解決快速的CPU和速度較慢的記憶體之間速度不匹配的問題,CPU訪問cache的速度要快於記憶體,如果將常用的資料放到硬體快取中,使用時CPU直接訪問cache而不用再訪問記憶體,從而提升系統速度。下文中的快取實際上是用軟體在記憶體中預先開闢一塊空間,使用時直接從這一塊空間中去取,是SLAB分配器為了便於對小塊記憶體的管理而建立的。
 

4、 SLAB相關說明

     (1)SLAB與夥伴(Buddy)演算法

       夥伴系統Buddy System的相關介紹可以參加其他部落格。在夥伴系統中,根據使用者請求,夥伴系統演算法會為使用者分配2^order個頁框,order的大小從0到11。在上文中,提到SLAB分配器是建立在夥伴系統之上的。簡單來說,就是使用者程序或者系統程序向SLAB申請了專門存放某一類物件的記憶體空間,但此時SLAB中沒有足夠的空間來專門存放此類物件,於是SLAB就像夥伴系統申請2的冪次方個連續的物理頁框,SLAB的申請得到夥伴系統滿足之後,SLAB就對這一塊記憶體進行管理,用以存放多個上文中提到的某一類物件。

     (2)SLAB與物件

         物件實際上指的是某一種資料型別。一個SLAB只針對一種資料型別(物件)。為了提升對物件的訪問效率,SLAB可能會對物件進行對齊【slab著色區和著色補償區:著色區的大小使Slab中的每個物件的起始地址都按快取記憶體中的”快取行(cache line)”大小進行對齊】。

     (3)SLAB與per-CPU快取

           為了提升效率,SLAB分配器為每一個CPU都提供了對應各個CPU的資料結構struct array_cache,該結構指向被釋放的物件。當CPU需要使用申請某一個物件的記憶體空間時,會先檢查array_cache中是否有空閒的物件,如果有的話就直接使用。如果沒有空閒物件,就像SLAB分配器進行申請。
 

二、SLAB記憶體分配器結構分析

1、SLAB記憶體分配器高層組織結構

圖 1 給出了 slab 結構的高層組織結構。在最高層是 cache_chain,這是一個 slab 快取的連結列表。這對於 best-fit 演算法非常有用,可以用來查詢最適合所需要的分配大小的快取(遍歷列表)。

2、 kmem_cache定義

cache_chain 的每個元素都是一個 kmem_cache 結構的引用(稱為一個 cache)。每一個kmem_cache定義了一個要管理的給定大小的物件池

核心在初始化的時候根據kmalloc_sizes.h檔案中定義的obj大小(參見 kmalloc分配記憶體大小的限制和巨集的一種用法),初始化了管理這些obj的slab,以及相關kmem_cache。cache_chain上掛著系統中所有的kmem_cache。前面我們提到初始化的時候會根據kmalloc_sizes.h中的定義初始化好固定大小obj的slab以及對應的kmem_cache,這些kmem_cache稱之為通用快取,提供給kmalloc來使用的。kmalloc根據傳入size大小來選擇合適的kmem_cache,然後從他的array_cache中取出obj。在初始化之後,系統中kmem_cache的連結串列大致如下。

這裡寫圖片描述

3、kmem_cache中的slab型別

每個kmem_cache快取都包含了一個 slabs 列表,這是一段連續的記憶體塊(通常都是頁面)。存在 3 種 slab:

  • slabs_full:完全分配的 slab
  • slabs_partial:部分分配的 slab
  • slabs_free:空 slab,或者沒有物件被分配

注意 slabs_free 列表中的 slab 是進行回收(reaping)的主要備選物件。正是通過此過程,slab 所使用的記憶體被返回給作業系統供其他使用者使用。
slab 列表中的每個 slab 都是一個連續的記憶體塊(一個或多個連續頁),它們被劃分成一個個物件。這些物件是從特定快取中進行分配和釋放的基本元素。注意 slab 是 slab 分配器進行操作的最小分配單位,因此如果需要對 slab 進行擴充套件,這也就是所擴充套件的最小值。通常來說,每個 slab 被分配為多個物件。
由於物件是從 slab 中進行分配和釋放的,因此單個 slab 可以在 slab 列表之間進行移動。例如,當一個 slab 中的所有物件都被使用完時,就從 slabs_partial 列表中移動到 slabs_full 列表中。當一個 slab 完全被分配並且有物件被釋放後,就從 slabs_full 列表中移動到 slabs_partial 列表中。當所有物件都被釋放之後,就從 slabs_partial 列表移動到 slabs_free 列表中。

4、SLAB記憶體分配器結構詳解

(1)slab記憶體分配器結構圖解

注:SLAB分配器把物件分組放進快取記憶體。每個快取記憶體都是同種型別物件的一種“儲備”。包含快取記憶體的主記憶體區被劃分為多個SLAB,每個SLAB由一個或多個連續的頁框組成,這些頁框中既包含已分配的物件,也包含空閒的物件。如上圖所示。

(2)資料結構原始碼

1、kmem_cache資料結構描述:


struct kmem_cache{
/* 1) per-cpu data, touched during every alloc/free */
	struct array_cache *array[NR_CPUS];//array是一個指向陣列的指標,每個陣列項都對應於系統中的一個CPU。每個陣列項都包含了另一個指標,指向下文討論的array_cache結構的例項
/* 2) Cache tunables. Protected by cache_chain_mutex */
	unsigned int batchcount;//指定了在每CPU列表為空的情況下,從快取的slab中獲取物件的數目。它還表示在快取增長時分配的物件數目
	unsigned int limit;//limit指定了每CPU列表中儲存的物件的最大數目,如果超出該值,核心會將batchcount個物件返回到slab
	unsigned int shared;
 
	unsigned int buffer_size;//指定了快取中管理的物件的長度
	u32 reciprocal_buffer_size;//buffer_size的倒數值,為了克服出發運算對效能的影響
/* 3) touched by every alloc & free from the backend */
 
	unsigned int flags;//是一個標誌暫存器,定義快取的全域性性質,當前只有一個標誌位,用於標記slab頭得管理資料是在slab內還是外
	unsigned int num;//儲存了可以放入slab的物件的最大數目
 
/* 4) cache_grow/shrink */
	/* order of pgs per slab (2^n) */
	unsigned int gfporder;//指定了slab包含的頁數目以2為底得對數
 
	/* force GFP flags, e.g. GFP_DMA */
	gfp_t gfpflags;//與夥伴系統互動時所提供的分配標識
 
	size_t colour;//指定了顏色的最大數目
	unsigned int colour_off;//是基本偏移量乘以顏色值獲得的絕對偏移量
	struct kmem_cache *slabp_cache;//如果slab頭部的管理資料儲存在slab外部,則slabp_cache指向分配所需記憶體的一般性快取;如果slab頭部在slab上,則其為NULL
	unsigned int slab_size;//slab管理區的大小
	unsigned int dflags;//另一個標誌集合,描述slab的“動態性質”,但目前還沒有定義標誌
	/* constructor func */
	void (*ctor)(struct kmem_cache *, void *);//建立快取記憶體時的建構函式指標
/* 5) cache creation/removal */
	const char *name;//快取的名稱
	struct list_head next;//用於將kmem_cache的所有例項儲存在全域性連結串列cache_chain上
 
/* 6) statistics */
#if STATS//統計資料欄位
	unsigned long num_active;
	unsigned long num_allocations;
	unsigned long high_mark;
	unsigned long grown;
	unsigned long reaped;
	unsigned long errors;
	unsigned long max_freeable;
	unsigned long node_allocs;
	unsigned long node_frees;
	unsigned long node_overflow;
	atomic_t allochit;
	atomic_t allocmiss;
	atomic_t freehit;
	atomic_t freemiss;
#endif
#if DEBUG
	/*
	 * If debugging is enabled, then the allocator can add additional
	 * fields and/or padding to every object. buffer_size contains the total
	 * object size including these internal fields, the following two
	 * variables contain the offset to the user object and its size.
	 */
	int obj_offset;
	int obj_size;
#endif
	/*
	 * We put nodelists[] at the end of kmem_cache, because we want to size
	 * this array to nr_node_ids slots instead of MAX_NUMNODES
	 * (see kmem_cache_init())
	 * We still use [MAX_NUMNODES] and not [1] or [0] because cache_cache
	 * is statically defined, so we reserve the max number of nodes.
	 */
	struct kmem_list3 *nodelists[MAX_NUMNODES];//nodelists是一個數組,每個陣列對應於系統中一個可能的記憶體結點。每個陣列項都包含kmem_list3的一個例項
	/*
	 * Do not add fields after nodelists[]
	 */
}

2、array_cache資料結構描述:

struct array_cache {
	/*儲存了per-CPU快取中可使用物件的指標的個數*/
	unsigned int avail;
	/*儲存的物件的最大的數量*/
	unsigned int limit;
	/*如果per-CPU列表中儲存的最大物件的數目超過limit值,核心會將batchcount個物件返回到slab*/
	unsigned int batchcount;
	/*從per—CPU快取中移除一個物件時,此值將被設定為1,快取收縮時,此時被設定為0.這使得核心能夠確認在快取上一次
	收縮之後是否被訪問過,也是快取重要性的一個標誌*/
	unsigned int touched;
	/*一個偽指標陣列,指向per-CPU快取的物件*/
	void *entry[];	

};

3、kmem_list3資料結構描述:

struct kmem_list3 {
	struct list_head slabs_partial;//部分空閒的slab連結串列
	struct list_head slabs_full;//非空閒的slab連結串列
	struct list_head slabs_free;//完全空閒的slab連結串列
	unsigned long free_objects;//部分空閒的slab連結串列和完全空閒的slab連結串列中空閒物件的總數
	unsigned int free_limit;//指定了所有slab上容許未使用物件的最大數目
	unsigned int colour_next;//核心建立的下一個slab的顏色
	spinlock_t list_lock;
	struct array_cache *shared;//結點內共享
	struct array_cache **alien;//在其他結點上
	unsigned long next_reap;//定義了核心在兩次嘗試收縮快取之間,必須經過的時間間隔
	int free_touched;//表示快取是否是活動的

};

4、 slab資料結構描述:

struct slab {
	struct list_head list;
	unsigned long colouroff;//該Slab上著色區的大小
	void *s_mem;//指向物件區的起點
	unsigned int inuse;//Slab中所分配物件的個數
	kmem_bufctl_t free;//指明瞭空閒物件鏈中的第一個物件,kmem_bufctl_t其實是一個整數
	unsigned short nodeid;//結點標識號

};

注:與SLAB有關的比較重要的兩個屬性:s_mem和freelist,其中s_mem指向slab中的第一個物件的地址(或者已經被分配或者空閒)。freelist指向空閒物件連結串列

(3)slab結構

每個Slab的首部都有一個小小的區域是不用的,稱為“著色區(coloring area)”。著色區的大小使Slab中的每個物件的起始地址都按快取記憶體中的”快取行(cache line)”大小進行對齊(80386的一級快取記憶體行大小為16位元組,Pentium為32位元組)。因為Slab是由1個頁面或多個頁面(最多為32)組成,因此,每個Slab都是從一個頁面邊界開始的,它自然按快取記憶體的緩衝行對齊。但是,Slab中的物件大小不確定,設定著色區的目的就是將Slab中第一個物件的起始地址往後推到與緩衝行對齊的位置。因為一個緩衝區中有多個Slab,因此,應該把每個緩衝區中的各個Slab著色區的大小盡量安排成不同的大小,這樣可以使得在不同的Slab中,處於同一相對位置的物件,讓它們在快取記憶體中的起始地址相互錯開,這樣就可以改善快取記憶體的存取效率。

 每個Slab上最後一個物件以後也有個小小的廢料區是不用的,這是對著色區大小的補償,其大小取決於著色區的大小,以及Slab與其每個物件的相對大小。但該區域與著色區的總和對於同一種物件的各個Slab是個常數。

 每個物件的大小基本上是所需資料結構的大小。只有當資料結構的大小不與快取記憶體中的緩衝行對齊時,才增加若干位元組使其對齊。所以,一個Slab上的所有物件的起始地址都必然是按快取記憶體中的緩衝行對齊的
 

三、兩個重要的資料結構kmem_cache和array_cache

1、 kmem_cache和array_cache資料結構之間的連線關係

2、struct kmem_list3 

kmem_list3是per node型別資料,一個node對應一個kmem_list3結構,我們的系統是UMA,所以只有一個kmem_list3結構。 
kmem_list3中有三個連結串列slabs_partial、slabs_full、slabs_free,每個連結串列中掛著都是slab結構。其中,已經有部分obj被分配出去的slab均掛在slabs_partial下;全部obj都被分配出去的slab掛在slabs_full下;全部obj都未分配出去的slab掛在slabs_free下。 

3、struct array_cache 

array_cache是per cpu型別資料,每個core對應一個array_cache結構。 
當array_cache中的obj為空時,系統會以batchcount值為準,一次性從kmem_list3中搬運batchcount個obj到arry_cache中,存放在entry裡。 

3、 slab 

slab主要包含兩大部分:

  • 管理性資料:管理性資料包括struct slab和kmem_bufctl_t;
  • obj物件;

slab有兩種形式的結構,管理資料外掛式或內嵌式。如果obj比較小,那麼struct slab和kmem_bufctl_t可以和obj分配在同一個物理page中,可稱為內嵌式;如果obj比較大,那麼管理性資料需要單獨分配一塊記憶體來存放,稱之為外掛式。我們在上圖中所畫的slab結構為內嵌式。

4、kmem_bufctl_t物件描述符

管理性資料kmem_bufctl_t的型別是unsigned int,本質上是一個空閒obj連結串列,用於描述下一個可用obj序號。初始化時,當前slab中的obj均可用,所以圖中kmem_bufctl_t中的值依次就是下一個可用的obj序。 
kmem_bufctl_t中使用方法可參考下圖場景。 

系統初始化時slab->free執行0#slab,某時刻系統已將0#—3# obj分配出去,此時slab->free指向4# slab,而4#slab對應的kmem_bufctl_t中存放的值是5。表明slab中current可用obj是4#,next可用obj是5#。 
系統執行一段時間後,1# obj需回收。此時,1#obj對應的kmem_bufctl_t中填入slab->free值4,並將slab->free修正為1。這表明slab中current可用obj是1#,next可用的obj為4#。
 

四、SLAB分配流程以及管理邏輯

1、SLAB與分割槽頁框分配器

先介紹一下分割槽頁框分配器,分割槽頁框分配器用於處理對連續頁框組的記憶體分配請求。其中有一些函式和巨集請求頁框,一般情況下,它們都返回第一個所分配的頁的線性地址,如果分配失敗,則返回NULL。這些函式簡介如下:

// 請求2^order個連續的頁框,他返回第一個所分配頁框的描述符的地址。分配失敗則返回NULL。
alloc_pages(gfp_mask,order);
// 與函式alloc_pages(gfp_mask,orser)類似,但是此函式返回第一個所分配頁的線性地址。
__get_free_pages(gfp_mask,order);
// 該函式先檢查page指向的頁描述符,如果該頁框未被保留(PG_reserved標誌位為0),就把描述符的count欄位值減1。如果count欄位的值變為0,就假定從與page對應的頁框開始的2^order個連續的頁框不再被使用。在這種情況下該函式釋放頁框。
__free_pages(page,order);
// 類似於__free_pages(),但是它接收的引數為要釋放的第一個頁框的線性地址addr。
free_pages(addr,order);

在核心完成slab分配器的初始化之後(SLAB分配器的初始化參見連結 SLAB分配器初始化),後續當SLAB分配器建立新的SLAB時,需要依靠分割槽頁框分配器來獲得一組連續的空閒頁框。為了達到此目的,需要呼叫kmem_getpages()函式,函式定義如下:

static struct page *kmem_getpages(struct kmem_cache *cachep, gfp_t flags,int nodeid);

其中,引數cachep指向需要額外頁框的快取記憶體描述符(請求頁框的個數存放在cachep->gfporder欄位中的order決定),flags說明如何請求頁框,nodeid指明從哪個NUMA節點的記憶體中請求頁框。與kmem_getpages()函式相對的是kmem_freepages(),kmem_freepages()函式可以釋放分配給slab的頁框。kmem_freepages()函式定義如下:

static void kmem_freepages(struct kmem_cache *cachep, struct page *page);

2、SLAB與建立快取記憶體

建立新的slab快取需要呼叫函式kmem_cache_create()。該函式定義如下:

/*
 * kmem_cache_create - Create a cache.
 * @name: A string which is used in /proc/slabinfo to identify this cache.
 * @size: The size of objects to be created in this cache.
 * @align: The required alignment for the objects.
 * @flags: SLAB flags
 * @ctor: A constructor for the objects.
 *
 * Returns a ptr to the cache on success, NULL on failure.
 * Cannot be called within a interrupt, but can be interrupted.
 * The @ctor is run when new pages are allocated by the cache.
 *
 * The flags are
 *
 * %SLAB_POISON - Poison the slab with a known test pattern (a5a5a5a5)
 * to catch references to uninitialised memory.
 *
 * %SLAB_RED_ZONE - Insert `Red' zones around the allocated memory to check
 * for buffer overruns.
 *
 * %SLAB_HWCACHE_ALIGN - Align the objects in this cache to a hardware
 * cacheline.  This can be beneficial if you're counting cycles as closely
 * as davem.
 */
struct kmem_cache *kmem_cache_create(const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void *));

kmem_cache_create()函式在呼叫成功時返回一個指向所構造的快取記憶體的指標,失敗則返回NULL。

與kmem_cache_create()函式相對應的是銷燬快取記憶體的函式kmem_cache_destroy(),該函式定義如下:

void kmem_cache_destroy(struct kmem_cache *s)
{
	LIST_HEAD(release);
	bool need_rcu_barrier = false;
	int err;
 
	if (unlikely(!s))
		return;
 
	get_online_cpus();
	get_online_mems();
 
	kasan_cache_destroy(s);
	mutex_lock(&slab_mutex);
 
	s->refcount--;
	if (s->refcount)
		goto out_unlock;
 
	err = shutdown_memcg_caches(s, &release, &need_rcu_barrier);
	if (!err)
		err = shutdown_cache(s, &release, &need_rcu_barrier);
 
	if (err) {
		pr_err("kmem_cache_destroy %s: Slab cache still has objects\n",
		       s->name);
		dump_stack();
	}
out_unlock:
	mutex_unlock(&slab_mutex);
 
	put_online_mems();
	put_online_cpus();
 
	release_caches(&release, need_rcu_barrier);
}
EXPORT_SYMBOL(kmem_cache_destroy);

需要注意的是:kmem_cache_destroy()函式銷燬的快取記憶體中應該只包含未使用物件,如果一個快取記憶體中含有正在使用的物件時呼叫kmem_cache_destroy()函式將會失敗,從kmem_cache_destroy()函式的原始碼中我們很容易看出。

3、SLAB物件分配和釋放

(1)物件分配概述

obj分配優先考慮從array_cache的entry中取,取的規則遵循LIFO原則,先從enry的末尾取出obj,因為這個obj很有可能還在硬體cache中,是熱的。如果entry為空,則說明是第一次從array_cache中分配obj或者是array_cache中的所有obj都已分配,所以需要先從kmem_cache的kmem_list3中取出batchcount個obj,把這些obj全部填充到enty中,然後再分配。 

(2)物件釋放概述

obj釋放也是優先考慮釋放到array_cache中,而不是直接釋放到kmem_list3中。只有array_cache中的obj超過了limit上限,系統才會將enty中的頭batchcount個obj搬到kmem_cache所在的kmem_list3中,然後將entry中剩餘obj向前移動,然後再將準備釋放的obj放到entry的末尾。

(3)kmem_cache_alloc

建立完成某一種資料型別或者某一種物件的快取記憶體之後,我們可以從該物件的快取記憶體中分配與釋放物件。其中,kmem_cache_alloc()函式用於從特定的快取中獲取物件,該函式定義如下:

/**
 * kmem_cache_alloc - Allocate an object
 * @cachep: The cache to allocate from.
 * @flags: See kmalloc().
 *
 * Allocate an object from this cache.  The flags are only relevant
 * if the cache has no available objects.
 */
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
{
	void *ret = slab_alloc(cachep, flags, _RET_IP_);
 
	kasan_slab_alloc(cachep, ret, flags);
	trace_kmem_cache_alloc(_RET_IP_, ret,
			       cachep->object_size, cachep->size, flags);
	return ret;
}
EXPORT_SYMBOL(kmem_cache_alloc);

從函式的名稱、引數、返回值、註釋中,我們很容易知道kmem_cache_alloc()函式從給定的slab快取記憶體中獲取一個指向空閒物件的指標。實際上,進行獲取空閒物件的時候,會先從per-CPU快取中也就是array_cache中查詢空閒物件,如果沒有則會從kmem_cache_node中獲取空閒物件,如果也沒有則需要利用夥伴演算法分配新的連續頁框,然後從新的頁框中獲取空閒物件

[1]、kmem_cache_alloc大致的呼叫鏈如下:

kmem_cache_alloc()——>slab_alloc()——>__do_cache_alloc()——>____cache_alloc()——>cpu_cache_get()(這裡實際上是從array_cache中獲取空閒物件)——>cache_alloc_refill()(這裡會在array_cache中沒有空閒物件時執行)——>cpu_cache_get()(經過cache_alloc_refill()的執行基本保證array_cache中有空閒物件)——>返回可用空閒物件

[2]、cache_alloc_refill()函式執行步驟分解如下:

cache_alloc_refill()——>嘗試從被一個NUMA節點所有CPU共享的緩衝中獲取空閒物件(原始碼註釋中寫道:See if we can refill from the shared array),如果有則返回可用物件,refill結束——>從kmem_cache_node中的slab中獲取空閒物件,有則返回,沒有就執行下一步——>kmem_getpages()

(4)kmem_cache_free

kmem_cache_free()函式用於從特定的快取中釋放物件,函式定義如下:

/**
 * kmem_cache_free - Deallocate an object
 * @cachep: The cache the allocation was from.
 * @objp: The previously allocated object.
 *
 * Free an object which was previously allocated from this
 * cache.
 */
void kmem_cache_free(struct kmem_cache *cachep, void *objp)
{
	unsigned long flags;
	cachep = cache_from_obj(cachep, objp);
	if (!cachep)
		return;
 
	local_irq_save(flags);
	debug_check_no_locks_freed(objp, cachep->object_size);
	if (!(cachep->flags & SLAB_DEBUG_OBJECTS))
		debug_check_no_obj_freed(objp, cachep->object_size);
	__cache_free(cachep, objp, _RET_IP_);
	local_irq_restore(flags);
 
	trace_kmem_cache_free(_RET_IP_, objp);
}
EXPORT_SYMBOL(kmem_cache_free);

五、擴充套件

1、內部碎片和外部碎片

(1)外部碎片

外部碎片指的是還沒有被分配出去(不屬於任何程序)但由於太小而無法分配給申請記憶體空間的新程序的記憶體空閒區域。外部碎片是除了任何已分配區域或頁面外部的空閒儲存塊。這些儲存塊的總和可以滿足當前申請的長度要求,但是由於它們的地址不連續或其他原因,使得系統無法滿足當前申請。簡單示例如下圖:

如果某程序現在需要向作業系統申請地址連續的32K記憶體空間,注意是地址連續,實際上系統中當前共有10K+23K=33K空閒記憶體,但是這些空閒記憶體並不連續,所以不能滿足程序的請求。這就是所謂的外部碎片,造成外部碎片的原因主要是程序或者系統頻繁的申請和釋放不同大小的一組連續頁框。Linux作業系統中為了儘量避免外部碎片而採用了夥伴系統(Buddy System)演算法。

(2)內部碎片

內部碎片就是已經被分配出去(能明確指出屬於哪個程序)卻不能被利用的記憶體空間;內部碎片是處於區域內部或頁面內部的儲存塊,佔有這些區域或頁面的程序並不使用這個儲存塊,而在程序佔有這塊儲存塊時,系統無法利用它。直到程序釋放它,或程序結束時,系統才有可能利用這個儲存塊。簡單示例如下圖:

某程序向系統申請了3K記憶體空間,系統通過夥伴系統演算法可能分配給程序4K(一個標準頁面)記憶體空間,導致剩餘1K記憶體空間無法被系統利用,造成了浪費。這是由於程序請求記憶體大小與系統分配給它的記憶體大小不匹配造成的。由於夥伴演算法採用頁框(Page Frame)作為基本記憶體區,適合於大塊記憶體請求。在很多情況下,程序或者系統申請的記憶體都是小於4K(一個標準頁面)的,依然採用夥伴演算法必然會造成系統記憶體的極大浪費。為滿足程序或者系統對小片記憶體的請求,對記憶體管理粒度更小的SLAB分配器就產生了。(注:Linux中的SLAB演算法實際上是借鑑了Sun公司的Solaris作業系統中的SLAB模式)。

2、SLAB分配器初始化

slab分配器的初始化涉及到一個雞與蛋的問題。為了初始化slab資料結構,核心需要很多遠小於一頁的記憶體區,很顯然由kmalloc分配這種記憶體最合適,但是kmalloc只有在slab分配器初始化完才能使用。核心藉助一些技巧來解決該問題。
kmem_cache_init函式被核心用來初始化slab分配器。它在夥伴系統啟用後呼叫。在SMP系統中,啟動CPU正在執行,其它CPU還未初始化,它要在smp_init之前呼叫。slab採用多步逐步初始化slab分配器,其工作過程:

  • 建立第一個名為kmem_cache的slab快取,此時該快取的管理資料結構使用的是靜態分配的記憶體。在slab分配器初始化完成後,會將這裡使用的靜態資料結構替換為動態分配的記憶體。
  • 初始化其它的slab快取,由於已經初始化了第一個slab快取,因此這一步是可行。

將初始化過程由於“雞與蛋”的問題而使用的靜態資料結構替換為動態分配的

3、slab替代品:slob和slub

儘管slab分配器對許多可能的工作負荷都工作良好, 但也有一些情形, 它無法提供最優效能. 如果某些計算機處於當前硬體尺度的邊界上, 在此類計算機上使用slab分配會出現一些問題:微小的嵌入式系統, 配備有大量實體記憶體的大規模並行系統. 在第二種情況下, slab分配器所需的大量元資料可能成為一個問題:開發者稱在大型系統上僅slab的資料結構就需要很多位元組記憶體。對嵌入式系統來說, slab分配器程式碼量和複雜性都太高。

為處理此類情形, 在核心版本2.6開發期間, 增加了slab分配器的兩個替代品:

(1)slob分配器

slob分配器進行了特別優化, 以便減少程式碼量. 它圍繞一個簡單的記憶體塊連結串列展開(因此而得名). 在分配記憶體時, 使用了同樣簡單的最先適配演算法.slob分配器只有大約600行程式碼, 總的程式碼量很小. 事實上, 從速度來說, 它不是最高效的分配器, 也肯定不是為大型系統設計的.

(2)slub分配器

slub分配器通過將頁幀打包為組,並通過struct page中未使用的欄位來管理這些組,試圖最小化所需的記憶體開銷。讀者此前已經看到,這樣做不會簡化該結構的定義,但在大型計算機上slub比slab提供了更好的效能,說明了這樣做是正確的.

由於slab分配是大多數核心配置的預設選項,我不會詳細討論備選的分配器. 但有很重要的一點需要強調:核心的其餘部分無需關注底層選擇使用了哪個分配器,所有分配器的前端介面都是相同的

每個分配器都必須實現一組特定的函式, 用於記憶體分配和快取:

kmalloc、__kmalloc和kmalloc_node // 一般的(特定於結點)記憶體分配函式.

kmem_cache_alloc、kmem_cache_alloc_node  // 提供(特定於結點)特定型別的核心快取.

本文在討論slab分配器時,會講解這些函式的行為. 使用這些標準函式, 核心可以提供更方便的函式, 而不涉及記憶體在內部具體如何管理. 舉例來說, kcalloc為陣列分配記憶體,而kzalloc分配一個填充位元組0的記憶體區。

普通核心程式碼只需要包含slab.h,即可使用記憶體分配的所有標準核心函式。連編系統會保證使用編譯時選擇的分配器,來滿足程式的記憶體分配請求。
 

4、分割槽頁框
 

5、參閱:

“雞與蛋”的問題而使