original link: https://www.ibm.com/developerworks/cn/linux/l-linux-slab-allocator/
Linux slab 分配器剖析
了解 Linux 內(nèi)存管理的方式
M. Tim Jones 是一名嵌入式軟件工程師,他是
GNU/Linux Application Programming、
AI Application Programming 以及
BSD Sockets Programming from a Multilanguage Perspective 等書的作者。他的工程背景非常廣泛,從同步宇宙飛船的內(nèi)核開發(fā)到嵌入式架構(gòu)設(shè)計(jì),再到網(wǎng)絡(luò)協(xié)議的開發(fā)。Tim 是位于科羅拉多州 Longmont 的 Emulex Corp. 的一名顧問工程師。
簡介: 良好的操作系統(tǒng)性能部分依賴于操作系統(tǒng)有效管理資源的能力。在過去,堆內(nèi)存管理器是實(shí)際的規(guī)范,但是其
性能會(huì)受到內(nèi)存碎片和內(nèi)存回收需求的影響。現(xiàn)在,Linux® 內(nèi)核使用了源自于 Solaris
的一種方法,但是這種方法在嵌入式系統(tǒng)中已經(jīng)使用了很長時(shí)間了,它是將內(nèi)存作為對(duì)象按照大小進(jìn)行分配。本文將探索 slab
分配器背后所采用的思想,并介紹這種方法提供的接口和用法。
發(fā)布日期: 2010 年 9 月 20 日
級(jí)別: 中級(jí)
建議: 0 (添加評(píng)論)
動(dòng)態(tài)內(nèi)存管理
內(nèi)存管理的目標(biāo)是提供一種方法,為實(shí)現(xiàn)各種目的而在各個(gè)用戶之間實(shí)現(xiàn)內(nèi)存共享。內(nèi)存管理方法應(yīng)該實(shí)現(xiàn)以下兩個(gè)功能:
- 最小化管理內(nèi)存所需的時(shí)間
- 最大化用于一般應(yīng)用的可用內(nèi)存(最小化管理開銷)
內(nèi)存管理實(shí)際上是一種關(guān)于權(quán)衡的零和游戲。您可以開發(fā)一種使用少量內(nèi)存進(jìn)行管理的算法,但是要花費(fèi)更多時(shí)間來管理可用內(nèi)存。也可以開發(fā)一個(gè)算法來有效地管理內(nèi)存,但卻要使用更多的內(nèi)存。最終,特定應(yīng)用程序的需求將促使對(duì)這種權(quán)衡作出選擇。
每個(gè)內(nèi)存管理器都使用了一種基于堆的分配策略。在這種方法中,大塊內(nèi)存(稱為 堆)用來為用戶定義的目的提供內(nèi)存。當(dāng)用戶需要一塊內(nèi)存時(shí),就請(qǐng)求給自己分配一定大小的內(nèi)存。堆管理器會(huì)查看可用內(nèi)存的情況(使用特定算法)并返回一塊內(nèi)存。搜索過程中使用的一些算法有 first-fit(在堆中搜索到的第一個(gè)滿足請(qǐng)求的內(nèi)存塊
)和 best-fit(使用堆中滿足請(qǐng)求的最合適的內(nèi)存塊)。當(dāng)用戶使用完內(nèi)存后,就將內(nèi)存返回給堆。
這種基于堆的分配策略的根本問題是碎片(fragmentation)。當(dāng)內(nèi)存塊被分配后,它們會(huì)以不同的順序在不同的時(shí)間返回。這樣會(huì)在堆中留下一些洞,需要花一些時(shí)間才能有效地管理空閑內(nèi)存。這種算法通常具有較高的內(nèi)存使用效率(分配需要的內(nèi)存),但是卻需要花費(fèi)更多時(shí)間來對(duì)堆進(jìn)行管理。
另外一種方法稱為 buddy memory allocation,
是一種更快的內(nèi)存分配技術(shù),它將內(nèi)存劃分為 2 的冪次方個(gè)分區(qū),并使用 best-fit 方法來分配內(nèi)存請(qǐng)求。當(dāng)用戶釋放內(nèi)存時(shí),就會(huì)檢查
buddy 塊,查看其相鄰的內(nèi)存塊是否也已經(jīng)被釋放。如果是的話,將合并內(nèi)存塊以最小化內(nèi)存碎片。這個(gè)算法的時(shí)間效率更高,但是由于使用
best-fit 方法的緣故,會(huì)產(chǎn)生內(nèi)存浪費(fèi)。
本文將著重介紹 Linux 內(nèi)核的內(nèi)存管理,尤其是 slab 分配提供的機(jī)制。
slab 緩存
Linux
所使用的 slab 分配器的基礎(chǔ)是 Jeff Bonwick 為 SunOS 操作系統(tǒng)首次引入的一種算法。Jeff
的分配器是圍繞對(duì)象緩存進(jìn)行的。在內(nèi)核中,會(huì)為有限的對(duì)象集(例如文件描述符和其他常見結(jié)構(gòu))分配大量內(nèi)存。Jeff
發(fā)現(xiàn)對(duì)內(nèi)核中普通對(duì)象進(jìn)行初始化所需的時(shí)間超過了對(duì)其進(jìn)行分配和釋放所需的時(shí)間。因此他的結(jié)論是不應(yīng)該將內(nèi)存釋放回一個(gè)全局的內(nèi)存池,而是將內(nèi)存保持為針
對(duì)特定目而初始化的狀態(tài)。例如,如果內(nèi)存被分配給了一個(gè)互斥鎖,那么只需在為互斥鎖首次分配內(nèi)存時(shí)執(zhí)行一次互斥鎖初始化函數(shù)(mutex_init
)即可。后續(xù)的內(nèi)存分配不需要執(zhí)行這個(gè)初始化函數(shù),因?yàn)閺纳洗吾尫藕驼{(diào)用析構(gòu)之后,它已經(jīng)處于所需的狀態(tài)中了。
Linux slab 分配器使用了這種思想和其他一些思想來構(gòu)建一個(gè)在空間和時(shí)間上都具有高效性的內(nèi)存分配器。
圖 1 給出了 slab 結(jié)構(gòu)的高層組織結(jié)構(gòu)。在最高層是 cache_chain
,這是一個(gè) slab 緩存的鏈接列表。這對(duì)于 best-fit 算法非常有用,可以用來查找最適合所需要的分配大小的緩存(遍歷列表)。cache_chain
的每個(gè)元素都是一個(gè)
kmem_cache
結(jié)構(gòu)的引用(稱為一個(gè) cache)。它定義了一個(gè)要管理的給定大小的對(duì)象池。
圖 1. slab 分配器的主要結(jié)構(gòu)
每個(gè)緩存都包含了一個(gè) slabs 列表,這是一段連續(xù)的內(nèi)存塊(通常都是頁面)。存在 3 種 slab:
-
slabs_full
- 完全分配的 slab
-
slabs_partial
- 部分分配的 slab
-
slabs_empty
- 空 slab,或者沒有對(duì)象被分配
注意 slabs_empty
列表中的 slab 是進(jìn)行回收(reaping)的主要備選對(duì)象。正是通過此過程,slab 所使用的內(nèi)存被返回給操作系統(tǒng)供其他用戶使用。
slab
列表中的每個(gè) slab 都是一個(gè)連續(xù)的內(nèi)存塊(一個(gè)或多個(gè)連續(xù)頁),它們被劃分成一個(gè)個(gè)對(duì)象。這些對(duì)象是從特定緩存中進(jìn)行分配和釋放的基本元素。注意
slab 是 slab 分配器進(jìn)行操作的最小分配單位,因此如果需要對(duì) slab 進(jìn)行擴(kuò)展,這也就是所擴(kuò)展的最小值。通常來說,每個(gè) slab
被分配為多個(gè)對(duì)象。
由于對(duì)象是從 slab 中進(jìn)行分配和釋放的,因此單個(gè) slab 可以在 slab 列表之間進(jìn)行移動(dòng)。例如,當(dāng)一個(gè) slab 中的所有對(duì)象都被使用完時(shí),就從 slabs_partial
列表中移動(dòng)到
slabs_full
列表中。當(dāng)一個(gè) slab 完全被分配并且有對(duì)象被釋放后,就從 slabs_full
列表中移動(dòng)到
slabs_partial
列表中。當(dāng)所有對(duì)象都被釋放之后,就從 slabs_partial
列表移動(dòng)到
slabs_empty
列表中。
slab 背后的動(dòng)機(jī)
與
傳統(tǒng)的內(nèi)存管理模式相比, slab
緩存分配器提供了很多優(yōu)點(diǎn)。首先,內(nèi)核通常依賴于對(duì)小對(duì)象的分配,它們會(huì)在系統(tǒng)生命周期內(nèi)進(jìn)行無數(shù)次分配。slab
緩存分配器通過對(duì)類似大小的對(duì)象進(jìn)行緩存而提供這種功能,從而避免了常見的碎片問題。slab
分配器還支持通用對(duì)象的初始化,從而避免了為同一目而對(duì)一個(gè)對(duì)象重復(fù)進(jìn)行初始化。最后,slab
分配器還可以支持硬件緩存對(duì)齊和著色,這允許不同緩存中的對(duì)象占用相同的緩存行,從而提高緩存的利用率并獲得更好的性能。
回頁首
API 函數(shù)
現(xiàn)在來看一下能夠創(chuàng)建新 slab 緩存、向緩存中增加內(nèi)存、銷毀緩存的應(yīng)用程序接口(API)以及 slab 中對(duì)對(duì)象進(jìn)行分配和釋放操作的函數(shù)。
第一個(gè)步驟是創(chuàng)建 slab 緩存結(jié)構(gòu),您可以將其靜態(tài)創(chuàng)建為:
struct struct kmem_cache *my_cachep;
|
然后其他 slab 緩存函數(shù)將使用該引用進(jìn)行創(chuàng)建、刪除、分配等操作。kmem_cache
結(jié)構(gòu)包含了每個(gè)中央處理器單元(CPU)的數(shù)據(jù)、一組可調(diào)整的(可以通過 proc 文件系統(tǒng)訪問)參數(shù)、統(tǒng)計(jì)信息和管理 slab 緩存所必須的元素。
kmem_cache_create
內(nèi)核函數(shù) kmem_cache_create
用來創(chuàng)建一個(gè)新緩存。這通常是在內(nèi)核初始化時(shí)執(zhí)行的,或者在首次加載內(nèi)核模塊時(shí)執(zhí)行。其原型定義如下:
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
參數(shù)定義了緩存名稱,proc 文件系統(tǒng)(在 /proc/slabinfo 中)使用它標(biāo)識(shí)這個(gè)緩存。
size
參數(shù)指定了為這個(gè)緩存創(chuàng)建的對(duì)象的大小, align
參數(shù)定義了每個(gè)對(duì)象必需的對(duì)齊。
flags
參數(shù)指定了為緩存啟用的選項(xiàng)。這些標(biāo)志如表 1 所示。
表 1.
kmem_cache_create 的部分選項(xiàng)(在 flags 參數(shù)中指定)
選項(xiàng) | 說明 |
SLAB_RED_ZONE |
在對(duì)象頭、尾插入標(biāo)志,用來支持對(duì)緩沖區(qū)溢出的檢查。 |
SLAB_POISON |
使用一種己知模式填充 slab,允許對(duì)緩存中的對(duì)象進(jìn)行監(jiān)視(對(duì)象屬對(duì)象所有,不過可以在外部進(jìn)行修改)。 |
SLAB_HWCACHE_ALIGN |
指定緩存對(duì)象必須與硬件緩存行對(duì)齊。 |
ctor
和 dtor
參數(shù)定義了一個(gè)可選的對(duì)象構(gòu)造器和析構(gòu)器。構(gòu)造器和析構(gòu)器是用戶提供的回調(diào)函數(shù)。當(dāng)從緩存中分配新對(duì)象時(shí),可以通過構(gòu)造器進(jìn)行初始化。
在創(chuàng)建緩存之后,
kmem_cache_create
函數(shù)會(huì)返回對(duì)它的引用。注意這個(gè)函數(shù)并沒有向緩存分配任何內(nèi)存。相反,在試圖從緩存(最初為空)分配對(duì)象時(shí),refill 操作將內(nèi)存分配給它。當(dāng)所有對(duì)象都被使用掉時(shí),也可以通過相同的操作向緩存添加內(nèi)存。
kmem_cache_destroy
內(nèi)核函數(shù) kmem_cache_destroy
用來銷毀緩存。這個(gè)調(diào)用是由內(nèi)核模塊在被卸載時(shí)執(zhí)行的。在調(diào)用這個(gè)函數(shù)時(shí),緩存必須為空。
void kmem_cache_destroy( struct kmem_cache *cachep );
|
kmem_cache_alloc
要從一個(gè)命名的緩存中分配一個(gè)對(duì)象,可以使用
kmem_cache_alloc
函數(shù)。調(diào)用者提供了從中分配對(duì)象的緩存以及一組標(biāo)志:
void kmem_cache_alloc( struct kmem_cache *cachep, gfp_t flags );
|
這個(gè)函數(shù)從緩存中返回一個(gè)對(duì)象。注意如果緩存目前為空,那么這個(gè)函數(shù)就會(huì)調(diào)用
cache_alloc_refill
向緩存中增加內(nèi)存。 kmem_cache_alloc
的 flags 選項(xiàng)與
kmalloc
的 flags 選項(xiàng)相同。表 2 給出了標(biāo)志選項(xiàng)的部分列表。
表 2.
kmem_cache_alloc 和 kmalloc 內(nèi)核函數(shù)的標(biāo)志選項(xiàng)
標(biāo)志 | 說明 |
GFP_USER |
為用戶分配內(nèi)存(這個(gè)調(diào)用可能會(huì)睡眠)。 |
GFP_KERNEL |
從內(nèi)核 RAM 中分配內(nèi)存(這個(gè)調(diào)用可能會(huì)睡眠)。 |
GFP_ATOMIC |
使該調(diào)用強(qiáng)制處于非睡眠狀態(tài)(對(duì)中斷處理程序非常有用)。 |
GFP_HIGHUSER |
從高端內(nèi)存中分配內(nèi)存。 |
kmem_cache_zalloc
內(nèi)核函數(shù) kmem_cache_zalloc
與
kmem_cache_alloc
類似,只不過它對(duì)對(duì)象執(zhí)行
memset
操作,用來在將對(duì)象返回調(diào)用者之前對(duì)其進(jìn)行清除操作。
kmem_cache_free
要將一個(gè)對(duì)象釋放回 slab,可以使用
kmem_cache_free
。調(diào)用者提供了緩存引用和要釋放的對(duì)象。
void kmem_cache_free( struct kmem_cache *cachep, void *objp );
|
kmalloc 和 kfree
內(nèi)核中最常用的內(nèi)存管理函數(shù)是
kmalloc
和 kfree
函數(shù)。這兩個(gè)函數(shù)的原型如下:
void *kmalloc( size_t size, int flags );
void kfree( const void *objp );
|
注意在 kmalloc
中,惟一兩個(gè)參數(shù)是要分配的對(duì)象的大小和一組標(biāo)志(請(qǐng)參看 表 2 中的部分列表)。但是 kmalloc
和
kfree
使用了類似于前面定義的函數(shù)的 slab 緩存。kmalloc
沒有為要從中分配對(duì)象的某個(gè) slab 緩存命名,而是循環(huán)遍歷可用緩存來查找可以滿足大小限制的緩存。找到之后,就(使用
__kmem_cache_alloc
)分配一個(gè)對(duì)象。要使用
kfree
釋放對(duì)象,從中分配對(duì)象的緩存可以通過調(diào)用 virt_to_cache
確定。這個(gè)函數(shù)會(huì)返回一個(gè)緩存引用,然后在
__cache_free
調(diào)用中使用該引用釋放對(duì)象。
其他函數(shù)
slab 緩存 API 還提供了其他一些非常有用的函數(shù)。
kmem_cache_size
函數(shù)會(huì)返回這個(gè)緩存所管理的對(duì)象的大小。您也可以通過調(diào)用 kmem_cache_name
來檢索給定緩存的名稱(在創(chuàng)建緩存時(shí)定義)。緩存可以通過釋放其中的空閑 slab 進(jìn)行收縮。這可以通過調(diào)用
kmem_cache_shrink
實(shí)現(xiàn)。注意這個(gè)操作(稱為回收)是由內(nèi)核定期自動(dòng)執(zhí)行的(通過
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 緩存的示例用法
下面的代碼片斷展示了創(chuàng)建新 slab 緩存、從緩存中分配和釋放對(duì)象然后銷毀緩存的過程。首先,必須要定義一個(gè) kmem_cache
對(duì)象,然后對(duì)其進(jìn)行初始化(請(qǐng)參看清單 1)。這個(gè)特定的緩存包含 32 字節(jié)的對(duì)象,并且是硬件緩存對(duì)齊的(由標(biāo)志參數(shù) SLAB_HWCACHE_ALIGN
定義)。
清單 1. 創(chuàng)建新 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 緩存,您現(xiàn)在可以從中分配一個(gè)對(duì)象了。清單 2 給出了一個(gè)從緩存中分配和釋放對(duì)象的例子。它還展示了兩個(gè)其他函數(shù)的用法。
清單 2. 分配和釋放對(duì)象
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 緩存的銷毀。調(diào)用者必須確保在執(zhí)行銷毀操作過程中,不要從緩存中分配對(duì)象。
清單 3. 銷毀 slab 緩存
static void remove_my_cache( void )
{
if (my_cachep) kmem_cache_destroy( my_cachep );
return;
}
|
回頁首
slab 的 proc 接口
proc
文件系統(tǒng)提供了一種簡單的方法來監(jiān)視系統(tǒng)中所有活動(dòng)的 slab 緩存。這個(gè)文件稱為
/proc/slabinfo,它除了提供一些可以從用戶空間訪問的可調(diào)整參數(shù)之外,還提供了有關(guān)所有 slab 緩存的詳細(xì)信息。當(dāng)前版本的
slabinfo 提供了一個(gè)標(biāo)題,這樣輸出結(jié)果就更具可讀性。對(duì)于系統(tǒng)中的每個(gè) slab
緩存來說,這個(gè)文件提供了對(duì)象數(shù)量、活動(dòng)對(duì)象數(shù)量以及對(duì)象大小的信息(除了每個(gè) slab 的對(duì)象和頁面之外)。另外還提供了一組可調(diào)整的參數(shù)和
slab 數(shù)據(jù)。
要調(diào)優(yōu)特定的 slab 緩存,可以簡單地向 /proc/slabinfo 文件中以字符串的形式回轉(zhuǎn) slab 緩存名稱和 3 個(gè)可調(diào)整的參數(shù)。下面的例子展示了如何增加 limit 和 batchcount 的值,而保留 shared
factor 不變(格式為 “cache name limit batchcount shared factor”):
# echo "my_cache 128 64 8" > /proc/slabinfo
|
limit
字段表示每個(gè) CPU 可以緩存的對(duì)象的最大數(shù)量。
batchcount
字段是當(dāng)緩存為空時(shí)轉(zhuǎn)換到每個(gè) CPU 緩存中全局緩存對(duì)象的最大數(shù)量。
shared
參數(shù)說明了對(duì)稱多處理器(Symmetric MultiProcessing,SMP)系統(tǒng)的共享行為。
注意您必須具有超級(jí)用戶的特權(quán)才能在 proc 文件系統(tǒng)中為 slab 緩存調(diào)優(yōu)參數(shù)。
回頁首
SLOB 分配器
對(duì)于小型的嵌入式系統(tǒng)來說,存在一個(gè) slab 模擬層,名為 SLOB。這個(gè) slab 的替代品在小型嵌入式 Linux 系統(tǒng)中具有優(yōu)勢,但是即使它保存了 512KB 內(nèi)存,依然存在碎片和難于擴(kuò)展的問題。在禁用 CONFIG_SLAB
時(shí),內(nèi)核會(huì)回到這個(gè) SLOB 分配器中。更多信息請(qǐng)參看 參考資料 一節(jié)。
回頁首
結(jié)束語
slab 緩存分配器的源代碼實(shí)際上是 Linux 內(nèi)核中可讀性較好的一部分。除了函數(shù)調(diào)用的間接性之外,源代碼也非常直觀,總的來說,具有很好的注釋。如果您希望了解更多有關(guān) slab 緩存分配器的內(nèi)容,建議您從源代碼開始,因?yàn)樗怯嘘P(guān)這種機(jī)制的最新文檔。
下面的 參考資料 一節(jié)提供了介紹 slab 緩存分配器的參考資料,但是不幸的是就目前的 2.6 實(shí)現(xiàn)來說,這些文檔都已經(jīng)過時(shí)了。
參考資料
學(xué)習(xí)
獲得產(chǎn)品和技術(shù)
-
獲取免費(fèi)的 SEK for Linux,共包含兩張 DVD,其中有用于 Linux 的最新 IBM 試用版軟件,包括 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere®。
- 利用可直接從 developerWorks 的 IBM 軟件下載資源中心 下載 IBM 試用版軟件,在 Linux 上構(gòu)建您的下一個(gè)開發(fā)項(xiàng)目。
討論
關(guān)于作者
M. Tim Jones 是一名嵌入式軟件工程師,他是 GNU/Linux Application Programming、AI Application Programming 以及 BSD Sockets Programming from a Multilanguage Perspective 等書的作者。他的工程背景非常廣泛,從同步宇宙飛船的內(nèi)核開發(fā)到嵌入式架構(gòu)設(shè)計(jì),再到網(wǎng)絡(luò)協(xié)議的開發(fā)。Tim 是位于科羅拉多州 Longmont 的 Emulex Corp. 的一名顧問工程師。