動態記憶體管理
記憶體管理的目標是提供一種方法,為實現各種目的而在各個使用者之間實現記憶體共用。記憶體管理方法應該實現以下兩個功能:
- 最小化管理記憶體所需的時間
- 最大化用於一般應用的可用記憶體(最小化管理開銷)
記憶體管理實際上是一種關於權衡的零和遊戲。您可以開發一種使用少量記憶體進行管理的演算法,但是要花費更多時間來管理可用記憶體。也可以開發一個演算法來有效地管理記憶體,但卻要使用更多的記憶體。最終,特定應用程式的需求將促使對這種權衡作出選擇。
每個記憶體管理器都使用了一種基於堆的分配策略。在這種方法中,大塊記憶體(稱為 堆)用來為使用者定義的目的提供記憶體。當使用者需要一塊記憶體時,就請求給自己分配一定大小的記憶體。堆管理器會查看可用記憶體的情況(使用特定演算法)並返回一塊記憶體。搜尋過程中使用的一些演算法有 first-fit(在堆中搜尋到的第一個滿足請求的記憶體塊)和 best-fit(使用堆中滿足請求的最合適的記憶體塊)。當使用者使用完記憶體後,就將記憶體返回給堆。
這種基於堆的分配策略的根本問題是片段(fragmentation)。當記憶體塊被分配後,它們會以不同的順序在不同的時間返回。這樣會在堆中留下一些洞,需要花一些時間才能有效地管理空閑記憶體。這種演算法通常具有較高的記憶體使用量效率(分配需要的記憶體),但是卻需要花費更多時間來對堆進行管理。
另外一種方法稱為 buddy memory allocation,是一種更快的記憶體配置技術,它將記憶體劃分為 2 的冪次方個分區,並使用 best-fit 方法來分配記憶體請求。當使用者釋放記憶體時,就會檢查 buddy 塊,查看其相鄰的記憶體塊是否也已經被釋放。如果是的話,將合并記憶體塊以最小化記憶體片段。這個演算法的時間效率更高,但是由於使用 best-fit 方法的緣故,會產生記憶體浪費。
本文將著重介紹 Linux 核心的記憶體管理,尤其是 slab 分配提供的機制。
slab 緩衝
Linux 所使用的 slab 分配器的基礎是 Jeff Bonwick 為 SunOS 作業系統首次引入的一種演算法。Jeff 的分配器是圍繞對象緩衝進行的。在核心中,會為有限的對象集(例如檔案描述符和其他常見結構)分配大量記憶體。Jeff 發現對核心中普通對象進行初始化所需的時間超過了對其進行分配和釋放所需的時間。因此他的結論是不應該將記憶體釋放回一個全域的記憶體池,而是將記憶體保持為針對特定目而初始化的狀態。例如,如果記憶體被分配給了一個互斥鎖,那麼只需在為互斥鎖首次分配記憶體時執行一次互斥鎖初始化函數(mutex_init
)即可。後續的記憶體配置不需要執行這個初始化函數,因為從上次釋放和調用析構之後,它已經處於所需的狀態中了。
Linux slab 分配器使用了這種思想和其他一些思想來構建一個在空間和時間上都具有高效性的記憶體 Clerk。
圖 1 給出了 slab 結構的高層組織圖。在最高層是 cache_chain
,這是一個 slab 緩衝的連結清單。這對於 best-fit 演算法非常有用,可以用來尋找最適合所需要的分配大小的緩衝(遍曆列表)。cache_chain
的每個元素都是一個 kmem_cache
結構的引用(稱為一個 cache)。它定義了一個要管理的給定大小的對象池。
圖 1. slab 分配器的主要結構
每個緩衝都包含了一個 slabs 列表,這是一段連續的記憶體塊(通常都是頁面)。存在 3 種 slab:
-
slabs_full
-
完全分配的 slab
-
slabs_partial
-
部分分配的 slab
-
slabs_empty
-
空 slab,或者沒有對象被分配
注意 slabs_empty
列表中的 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_empty
列表中。
slab 背後的動機
與傳統的記憶體管理員模式相比, slab 緩衝分配器提供了很多優點。首先,核心通常依賴於對小對象的分配,它們會在系統生命週期內進行無數次分配。slab 緩衝分配器通過對類似大小的對象進行緩衝而提供這種功能,從而避免了常見的片段問題。slab 分配器還支援通用對象的初始化,從而避免了為同一目而對一個對象重複進行初始化。最後,slab 分配器還可以支援硬體緩衝對齊和著色,這允許不同緩衝中的對象佔用相同的緩衝行,從而提高緩衝的利用率並獲得更好的效能。
API 函數
現在來看一下能夠建立新 slab 緩衝、向緩衝中增加記憶體、銷毀緩衝的應用程式介面(API)以及 slab 中對對象進行分配和釋放操作的函數。
第一個步驟是建立 slab 緩衝結構,您可以將其靜態建立為:
struct struct kmem_cache *my_cachep;
slab 緩衝的 Linux 原始碼
您可以在 ./linux/mm/slab.c 中找到 slab 緩衝的原始碼。 kmem_cache
結構也是在 ./linux/mm/slab.c 中定義的。本文著重討論 2.6.21 Linux 核心中的當前實現。
然後其他 slab 緩衝函數將使用該引用進行建立、刪除、分配等操作。kmem_cache
結構包含了每個中央處理器單元(CPU)的資料、一組可調整的(可以通過 proc 檔案系統訪問)參數、統計資訊和管理 slab 緩衝所必須的元素。
kmem_cache_create
核心功能 kmem_cache_create
用來建立一個新緩衝。這通常是在核心初始化時執行的,或者在首次載入核心模組時執行。其原型定義如下:
struct kmem_cache *kmem_cache_create( const char *name, size_t size, size_t align, unsigned long flags; void (*ctor)(void*, struct kmem_cache *, unsigned long), void (*dtor)(void*, struct kmem_cache *, unsigned long));
name
參數定義了緩衝名稱,proc 檔案系統(在 /proc/slabinfo 中)使用它標識這個緩衝。 size
參數指定了為這個緩衝建立的對象的大小, align
參數定義了每個對象必需的對齊。 flags
參數指定了為緩衝啟用的選項。這些標誌如表 1 所示。
表 1. kmem_cache_create 的部分選項(在 flags 參數中指定)
選項 |
說明 |
SLAB_RED_ZONE |
在對象頭、尾插入標誌,用來支援對緩衝區溢位的檢查。 |
SLAB_POISON |
使用一種己知模式填充 slab,允許對緩衝中的對象進行監視(對象屬對象所有,不過可以在外部進行修改)。 |
SLAB_HWCACHE_ALIGN |
指定緩衝對象必須與硬體緩衝行對齊。 |
ctor
和 dtor
參數定義了一個可選的物件建構器和析構器。構造器和析構器是使用者提供的回呼函數。當從緩衝中分配新對象時,可以通過構造器進行初始化。
在建立緩衝之後, kmem_cache_create
函數會返回對它的引用。注意這個函數並沒有向緩衝分配任何記憶體。相反,在試圖從緩衝(最初為空白)指派至時,refill 操作將記憶體配置給它。當所有對象都被使用掉時,也可以通過相同的操作向緩衝添加記憶體。
kmem_cache_destroy
核心功能 kmem_cache_destroy
用來銷毀緩衝。這個調用是由核心模組在被卸載時執行的。在調用這個函數時,緩衝必須為空白。
void kmem_cache_destroy( struct kmem_cache *cachep );
kmem_cache_alloc
要從一個命名的緩衝中分配一個對象,可以使用 kmem_cache_alloc
函數。調用者提供了從中指派至的緩衝以及一組標誌:
void kmem_cache_alloc( struct kmem_cache *cachep, gfp_t flags );
這個函數從緩衝中返回一個對象。注意如果緩衝目前為空白,那麼這個函數就會調用 cache_alloc_refill
向緩衝中增加記憶體。 kmem_cache_alloc
的 flags 選項與 kmalloc
的 flags 選項相同。表 2 給出了標誌選項的部分列表。
表 2. kmem_cache_alloc 和 kmalloc 核心功能的標誌選項
標誌 |
說明 |
GFP_USER |
為使用者指派記憶體(這個調用可能會睡眠)。 |
GFP_KERNEL |
從核心 RAM 中分配記憶體(這個調用可能會睡眠)。 |
GFP_ATOMIC |
使該調用強制處於非睡眠狀態(對中斷處理常式非常有用)。 |
GFP_HIGHUSER |
從高端記憶體中分配記憶體。 |
NUMA 的 slab 分配
對於 NUMA(Non-Uniform Memory Access)架構來說,對某個特定節點的分配函數是 kmem_cache_alloc_node
。
kmem_cache_zalloc
核心功能 kmem_cache_zalloc
與 kmem_cache_alloc
類似,只不過它對對象執行 memset
操作,用來在將對象返回調用者之前對其進行清除操作。
NUMA 的 slab 分配
對於 NUMA(Non-Uniform Memory Access)架構來說,對某個特定節點的分配函數是 kmem_cache_alloc_node
。
kmem_cache_free
要將一個對象釋放回 slab,可以使用 kmem_cache_free
。調用者提供了緩衝引用和要釋放的對象。
void kmem_cache_free( struct kmem_cache *cachep, void *objp );
kmalloc 和 kfree
核心中最常用的記憶體管理函數是 kmalloc
和 kfree
函數。這兩個函數的原型如下:
void *kmalloc( size_t size, int flags );void kfree( const void *objp );
注意在 kmalloc
中,惟一兩個參數是要分配的對象的大小和一組標誌(請參看 表 2 中的部分列表)。但是 kmalloc
和 kfree
使用了類似於前面定義的函數的 slab 緩衝。kmalloc
沒有為要從中指派至的某個 slab 緩衝命名,而是迴圈遍曆可用緩衝來尋找可以滿足大小限制的緩衝。找到之後,就(使用 __kmem_cache_alloc
)分配一個對象。要使用 kfree
釋放對象,從中指派至的緩衝可以通過調用 virt_to_cache
確定。這個函數會返回一個緩衝引用,然後在 __cache_free
調用中使用該引用釋放對象。
通用對象分配
在 slab 原始碼中,提供了一個名為 kmem_find_general_cachep
的函數,可執行緩衝搜尋,即用來尋找最適合所需對象大小的 slab 緩衝。
其他函數
slab 緩衝 API 還提供了其他一些非常有用的函數。 kmem_cache_size
函數會返回這個緩衝所管理的對象的大小。您也可以通過調用 kmem_cache_name
來檢索給定緩衝的名稱(在建立緩衝時定義)。緩衝可以通過釋放其中的空閑 slab 進行收縮。這可以通過調用 kmem_cache_shrink
實現。注意這個操作(稱為回收)是由核心定期自動執行的(通過 kswapd
)。
unsigned int kmem_cache_size( struct kmem_cache *cachep );const char *kmem_cache_name( struct kmem_cache *cachep );int kmem_cache_shrink( struct kmem_cache *cachep );
通用對象分配
在 slab 原始碼中,提供了一個名為 kmem_find_general_cachep
的函數,可執行緩衝搜尋,即用來尋找最適合所需對象大小的 slab 緩衝。
slab 緩衝的樣本用法
下面的代碼片斷展示了建立新 slab 緩衝、從緩衝中分配和釋放對象然後銷毀緩衝的過程。首先,必須要定義一個 kmem_cache
對象,然後對其進行初始化(請參看清單 1)。這個特定的緩衝包含 32 位元組的對象,並且是硬體緩衝對齊的(由標誌參數 SLAB_HWCACHE_ALIGN
定義)。
清單 1. 建立新 slab 緩衝
static struct kmem_cache *my_cachep;static void init_my_cache( void ){ my_cachep = kmem_cache_create( "my_cache", /* Name */ 32, /* Object Size */ 0, /* Alignment */ SLAB_HWCACHE_ALIGN, /* Flags */ NULL, NULL ); /* Constructor/Deconstructor */ return;}
使用所分配的 slab 緩衝,您現在可以從中分配一個對象了。清單 2 給出了一個從緩衝中分配和釋放對象的例子。它還展示了兩個其他函數的用法。
清單 2. 分配和釋放對象
int slab_test( void ){ void *object; printk( "Cache name is %s/n", kmem_cache_name( my_cachep ) ); printk( "Cache object size is %d/n", kmem_cache_size( my_cachep ) ); object = kmem_cache_alloc( my_cachep, GFP_KERNEL ); if (object) { kmem_cache_free( my_cachep, object ); } return 0;}
最後,清單 3 示範了 slab 緩衝的銷毀。調用者必須確保在執行銷毀操作過程中,不要從緩衝中指派至。
清單 3. 銷毀 slab 緩衝
static void remove_my_cache( void ){ if (my_cachep) kmem_cache_destroy( my_cachep ); return;}
SLOB 分配器
對於小型的嵌入式系統來說,存在一個 slab 類比層,名為 SLOB。這個 slab 的替代品在小型嵌入式 Linux 系統中具有優勢,但是即使它儲存了 512KB 記憶體,依然存在片段和難於擴充的問題。在禁用 CONFIG_SLAB
時,核心會回到這個 SLOB 分配器中。
結束語
slab 緩衝分配器的原始碼實際上是 Linux 核心中可讀性較好的一部分。除了函數調用的間接性之外,原始碼也非常直觀,總的來說,具有很好的注釋。如果您希望瞭解更多有關 slab 緩衝分配器的內容,建議您從原始碼開始,因為它是有關這種機制的最新文檔。 下面的 參考資料 一節提供了介紹 slab 緩衝分配器的參考資料,但是不幸的是就目前的 2.6 實現來說,這些文檔都已經過時了。
原文出處(點擊此處)