1. 程式人生 > >linux記憶體管理演算法 :夥伴演算法和slab

linux記憶體管理演算法 :夥伴演算法和slab

良好的作業系統效能部分依賴於作業系統有效管理資源的能力。在過去,堆記憶體管理器是實際的規範,但是其效能會受到記憶體碎片和記憶體回收需求的影響。現在,Linux® 核心使用了源自於 Solaris 的一種方法,但是這種方法在嵌入式系統中已經使用了很長時間了,它是將記憶體作為物件按照大小進行分配。本文將探索 slab 分配器背後所採用的思想,並介紹這種方法提供的介面和用法。

動態記憶體管理

記憶體管理的目標是提供一種方法,為實現各種目的而在各個使用者之間實現記憶體共享。記憶體管理方法應該實現以下兩個功能:

  • 最小化管理記憶體所需的時間
  • 最大化用於一般應用的可用記憶體(最小化管理開銷)

記憶體管理實際上是一種關於權衡的零和遊戲。您可以開發一種使用少量記憶體進行管理的演算法,但是要花費更多時間來管理可用記憶體。也可以開發一個演算法來有效地管理記憶體,但卻要使用更多的記憶體。最終,特定應用程式的需求將促使對這種權衡作出選擇。

每個記憶體管理器都使用了一種基於堆的分配策略。在這種方法中,大塊記憶體(稱為 )用來為使用者定義的目的提供記憶體。當用戶需要一塊記憶體時,就請求給自己分配一定大小的記憶體。堆管理器會檢視可用記憶體的情況(使用特定演算法)並返回一塊記憶體。搜尋過程中使用的一些演算法有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)即可。後續的記憶體分配不需要執行這個初始化函式,因為從上次釋放和呼叫析構之後,它已經處於所需的狀態中了。這裡想說的主要是我們得到一塊很原始的記憶體,必須要經過一定的初始化之後才能用於特定的目的。當我們用slab時,記憶體不會被釋放到全域性記憶體池中,所以還是處於特定的初始化狀態的。這樣就能加速我們的處理過程。

Linux slab 分配器使用了這種思想和其他一些思想來構建一個在空間和時間上都具有高效性的記憶體分配器。

圖 1 給出了 slab 結構的高層組織結構。在最高層是 cache_chain,這是一個 slab 快取的連結列表。這對於 best-fit 演算法非常有用,可以用來查詢最適合所需要的分配大小的快取(遍歷列表)。cache_chain 的每個元素都是一個 kmem_cache 結構的引用(稱為一個 cache)。它定義了一個要管理的給定大小的物件池。

圖 1. slab 分配器的主要結構
圖 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 快取函式將使用該引用進行建立、刪除、分配等操作。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 從高階記憶體中分配記憶體。

kmem_cache_zalloc

核心函式 kmem_cache_zalloc 與 kmem_cache_alloc 類似,只不過它對物件執行 memset 操作,用來在將物件返回呼叫者之前對其進行清除操作。

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 快取 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 快取的示例用法

下面的程式碼片斷展示了建立新 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;
}

slab 的 proc 介面

proc 檔案系統提供了一種簡單的方法來監視系統中所有活動的 slab 快取。這個檔案稱為 /proc/slabinfo,它除了提供一些可以從使用者空間訪問的可調整引數之外,還提供了有關所有 slab 快取的詳細資訊。當前版本的 slabinfo 提供了一個標題,這樣輸出結果就更具可讀性。對於系統中的每個 slab 快取來說,這個檔案提供了物件數量、活動物件數量以及物件大小的資訊(除了每個 slab 的物件和頁面之外)。另外還提供了一組可調整的引數和 slab 資料。

要調優特定的 slab 快取,可以簡單地向 /proc/slabinfo 檔案中以字串的形式迴轉 slab 快取名稱和 3 個可調整的引數。下面的例子展示瞭如何增加 limit 和 batchcount 的值,而保留 shared factor 不變(格式為 “cache name limit batchcount shared factor”):

# echo "my_cache 128 64 8" > /proc/slabinfo

limit 欄位表示每個 CPU 可以快取的物件的最大數量。 batchcount 欄位是當快取為空時轉換到每個 CPU 快取中全域性快取物件的最大數量。shared 引數說明了對稱多處理器(Symmetric MultiProcessing,SMP)系統的共享行為。

注意您必須具有超級使用者的特權才能在 proc 檔案系統中為 slab 快取調優引數。

SLOB 分配器

對於小型的嵌入式系統來說,存在一個 slab 模擬層,名為 SLOB。這個 slab 的替代品在小型嵌入式 Linux 系統中具有優勢,但是即使它儲存了 512KB 記憶體,依然存在碎片和難於擴充套件的問題。在禁用 CONFIG_SLAB 時,核心會回到這個 SLOB 分配器中。更多資訊請參看 參考資料 一節。

結束語

slab 快取分配器的原始碼實際上是 Linux 核心中可讀性較好的一部分。除了函式呼叫的間接性之外,原始碼也非常直觀,總的來說,具有很好的註釋。如果您希望瞭解更多有關 slab 快取分配器的內容,建議您從原始碼開始,因為它是有關這種機制的最新文件。 下面的 參考資料 一節提供了介紹 slab 快取分配器的參考資料,但是不幸的是就目前的 2.6 實現來說,這些文件都已經過時了。