Linux記憶體管理 Slab分配器(一:概述)
採用夥伴演算法分配記憶體時,每次至少分配一個頁面。但當請求分配的記憶體大小為幾十個位元組或幾百個位元組時應該如何處理?如何在一個頁面中分配小的記憶體區,小記憶體區的分配所產生的內碎片又如何解決?Linux採用Slab。
Linux 所使用的 slab 分配器的基礎是 Jeff Bonwick 為 SunOS 作業系統首次引入的一種演算法。Jeff 的分配器是圍繞物件快取進行的。在核心中,會為有限的物件集(例如檔案描述符和其他常見結構)分配大量記憶體。Jeff 發現對核心中普通物件進行初始化所需的時間超過了對其進行分配和釋放所需的時間。因此他的結論是不應該將記憶體釋放回一個全域性的記憶體池,而是將記憶體保持為針對特定目而初始化的狀態。例如,如果記憶體被分配給了一個互斥鎖,那麼只需在為互斥鎖首次分配記憶體時執行一次互斥鎖初始化函式(mutex_init)即可。後續的記憶體分配不需要執行這個初始化函式,因為從上次釋放和呼叫析構之後,它已經處於所需的狀態中了。
Linux slab 分配器使用了這種思想和其他一些思想來構建一個在空間和時間上都具有高效性的記憶體分配器。
圖 1 給出了 slab 結構的高層組織結構。在最高層是 cache_chain,這是一個 slab 快取的連結列表。這對於 best-fit 演算法非常有用,可以用來查詢最適合所需要的分配大小的快取(遍歷列表)。cache_chain 的每個元素都是一個 kmem_cache 結構的引用(稱為一個 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 列表中。
為了便於理解,在網上找了一個圖,如下:
用於描述快取的資料結構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[] */ }
array_cache資料結構描述:
struct array_cache {
unsigned int avail;//本地快取記憶體中可用的空閒物件數
unsigned int limit;//空閒物件的上限
unsigned int batchcount;//一次轉入和轉出的物件數量
unsigned int touched;//標識本地CPU最近是否被使用
spinlock_t lock;
void *entry[];//這是一個偽陣列,便於對後面用於跟蹤空閒物件的指標陣列的訪問
};
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;//表示快取是否是活動的
};
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機制的簡介表示如下圖所示:
slab內的結構如下圖所示:
每個Slab的首部都有一個小小的區域是不用的,稱為“著色區(coloring area)”。著色區的大小使Slab中的每個物件的起始地址都按快取記憶體中的”快取行(cache line)”大小進行對齊(80386的一級快取記憶體行大小為16位元組,Pentium為32位元組)。因為Slab是由1個頁面或多個頁面(最多為32)組成,因此,每個Slab都是從一個頁面邊界開始的,它自然按快取記憶體的緩衝行對齊。但是,Slab中的物件大小不確定,設定著色區的目的就是將Slab中第一個物件的起始地址往後推到與緩衝行對齊的位置。因為一個緩衝區中有多個Slab,因此,應該把每個緩衝區中的各個Slab著色區的大小盡量安排成不同的大小,這樣可以使得在不同的Slab中,處於同一相對位置的物件,讓它們在快取記憶體中的起始地址相互錯開,這樣就可以改善快取記憶體的存取效率。
每個Slab上最後一個物件以後也有個小小的廢料區是不用的,這是對著色區大小的補償,其大小取決於著色區的大小,以及Slab與其每個物件的相對大小。但該區域與著色區的總和對於同一種物件的各個Slab是個常數。
每個物件的大小基本上是所需資料結構的大小。只有當資料結構的大小不與快取記憶體中的緩衝行對齊時,才增加若干位元組使其對齊。所以,一個Slab上的所有物件的起始地址都必然是按快取記憶體中的緩衝行對齊的。
與傳統的記憶體管理模式相比, slab 快取分配器提供了很多優點。首先,核心通常依賴於對小物件的分配,它們會在系統生命週期內進行無數次分配。slab 快取分配器通過對類似大小的物件進行快取而提供這種功能,從而避免了常見的碎片問題。slab 分配器還支援通用物件的初始化,從而避免了為同一目而對一個物件重複進行初始化。最後,slab 分配器還可以支援硬體快取對齊和著色,這允許不同快取中的物件佔用相同的快取行,從而提高快取的利用率並獲得更好的效能。