解密Slab分配器:內存管理的高效武器
在 Linux 操作系統(tǒng)的神秘世界里,內存管理如同一場精密的棋局,每一步都關乎著系統(tǒng)的性能和穩(wěn)定性。而在這場棋局中,Slab 分配器宛如一顆璀璨的明珠,是內存管理的高效武器。它以其獨特的設計和強大的功能,為 Linux 系統(tǒng)的內存分配與釋放提供了卓越的解決方案。現(xiàn)在,讓我們一同揭開 Slab 分配器的神秘面紗,探索它在內存管理中的神奇之處。
Linux內核版本:5.0
架構:ARM64
Linux 5.0內核源碼注釋倉庫地址:
zhangzihengya/LinuxSourceCode_v5.0_study (http://github.com)
一、Slab分配器概述
Slab 分配器是 Linux 內核中一種極為重要的內存管理機制,主要用于高效管理小塊內存的分配。它針對頻繁分配和釋放的小對象進行了專門優(yōu)化,能夠有效減少內存碎片,顯著提高系統(tǒng)性能。在 Linux 內核的眾多子系統(tǒng)中,Slab 分配器得到了廣泛應用,比如網(wǎng)絡緩沖區(qū)、文件系統(tǒng)緩存以及進程控制塊等領域。
Slab 分配器的核心概念包括 Cache(緩存)、Slab 和 Object。其中,Cache 是為每種類型的對象創(chuàng)建的緩存,每個緩存存儲相同大小的對象集合。Slab 則是一塊連續(xù)的內存區(qū)域,用于存儲一組特定大小的對象。而 Object 是在 Slab 中實際存儲的數(shù)據(jù)單元。
Slab 分配器具有諸多優(yōu)勢。首先,它通過對象復用,避免了頻繁的分配與釋放操作,極大地提高了內存分配的效率。其次,它能夠保證內存分配尺寸和對齊一致,從而有效減少內存碎片問題。最后,Slab 分配器適用于不同大小的對象,能夠顯著提升內核的整體性能。
通過查看/proc/slabinfo文件,可以了解 Slab 分配器的內存使用情況,幫助調優(yōu)系統(tǒng)性能。該文件輸出的頭部包含多個字段,其中name字段表示 slab 緩存的名稱,每個 slab 緩存存儲相同類型和大小的對象;active_objs字段表示當前在使用的(已分配的)對象數(shù)量;num_objs字段表示緩存中分配的對象總數(shù),包括已經(jīng)分配和空閑的對象;objsize字段表示每個對象的大小(以字節(jié)為單位);objperslab字段表示每個 slab 中包含的對象數(shù)量;pagesperslab字段表示每個 slab 使用的頁數(shù);tunables字段中的值控制 slab 緩存的行為,如limit表示 slab 緩存中每個 CPU 可以緩存的最大對象數(shù)量,batchcount表示每次從全局緩存到 CPU 本地緩存中批量獲取的對象數(shù),sharedfactor控制多個 CPU 是否共享 slab 緩存;slabdata部分包含有關 slab 使用的統(tǒng)計數(shù)據(jù),如active_slabs表示當前正在使用的 slab 數(shù)量,num_slabs表示系統(tǒng)中分配的總 slab 數(shù)量,sharedavail表示 CPU 本地緩存中可用對象的數(shù)量。
二、核心技術
2.1 slab機制
slab分配器最終還使用伙伴系統(tǒng)來分配實際的物理頁面,只不過slab分配器在這些連續(xù)的物理頁面上實現(xiàn)了自己的機制,以此來對小內存塊進行管理。slab機制如下圖所示:
圖片
其中每個slab描述符都會建立共享對象緩沖池和本地對象緩沖池。slab機制有如下特性:
把分配的內存塊當作對象(object)來看待。對象可以自定義構造函數(shù)(constructor) 和析構函數(shù)(destructor)來初始化對象的內容并釋放對象的內容。
slab對象被釋放之后不會馬上丟棄而是繼續(xù)保留在內存中,可能稍后會被用到,這樣不需要重新向伙伴系統(tǒng)申請內存。
slab機制可以根據(jù)特定大小的內存塊來創(chuàng)建slab描述符,如內存中常見的數(shù)據(jù)結構、打開文件對象等,這樣可以有效地避免內存碎片的產(chǎn)生,也可以快速獲得頻繁訪問的數(shù)據(jù)結構。另外,slab機制也支持按2的n次方字節(jié)大小分配內存塊。
slab機制創(chuàng)建了多層的緩沖池,充分利用了空間換時間的思想,未雨綢謬,有效地解決了效率問題。
每個CPU有本地對象緩沖池,避免了多核之間的鎖爭用問題。
每個內存節(jié)點有共享對象緩沖池。
2.2 slab框架
為了更好地理解slab分配器的細節(jié),我們先從宏觀上大致了解下slab系統(tǒng)的架構,如下圖所示:
圖片
slab系統(tǒng)由slab描述符、slab節(jié)點、本地對象緩沖池、共享對象緩沖池、3個slab鏈表、n個slab分配器,以及眾多slab緩存對象組成,相關數(shù)據(jù)結構的注解如下。slab描述符:
// kmem_cache數(shù)據(jù)結構是 slab 分配器中的核心成員,每個 slab 描述符都用一個 kmem_cache 數(shù)據(jù)結構來抽象描述
struct kmem_cache {
// Per-cpu 變量的 array_cache 數(shù)據(jù)結構,每個CPU一個,表示本地 CPU 的對象緩沖池
struct array_cache __percpu *cpu_cache;
/* 1) Cache tunables. Protected by slab_mutex */
// 表示在當前 CPU 的本地對象緩沖池 array_cache 為空時,從共享對象緩沖池或 slabs_partial/slabs_free 列表中獲取的對象的數(shù)目
unsigned int batchcount;
// 當本地對象緩沖池中的空閑對象的數(shù)目大于 limit 時,會主動釋放 batchcount 個對象,便于內核回收和銷毀 slab
unsigned int limit;
// 用于多核系統(tǒng)
unsigned int shared;
// 對象的長度,這個長度要加上 align 對齊字節(jié)
unsigned int size;
struct reciprocal_value reciprocal_buffer_size;
/* 2) touched by every alloc & free from the backend */
// 對象的分配掩碼
slab_flags_t flags; /* constant flags */
// 一個 slab 中最多有多少個對象
unsigned int num; /* # of objs per slab */
/* 3) cache_grow/shrink */
/* order of pgs per slab (2^n) */
unsigned int gfporder;
/* force GFP flags, e.g. GFP_DMA */
gfp_t allocflags;
// 一個 slab 中可以有多少個不同的緩存行
size_t colour; /* cache colouring range */
// 著色區(qū)的長度,和 L1 緩存行大小相同
unsigned int colour_off; /* colour offset */
struct kmem_cache *freelist_cache;
// 每個對象要占用 1 字節(jié)來存放 freelist
unsigned int freelist_size;
/* constructor func */
void (*ctor)(void *obj);
/* 4) cache creation/removal */
// slab 描述符的名稱
const char *name;
struct list_head list;
int refcount;
// 對象的實際大小
int object_size;
// 對齊的長度
int align;
...
// slab 節(jié)點
// 在 NUMA 系統(tǒng)中,每個節(jié)點有一個 kmem_cache_node 數(shù)據(jù)結構
// 在 ARM Vexpress 平臺上,只有一個節(jié)點
struct kmem_cache_node *node[MAX_NUMNODES];
};
slab節(jié)點:
struct kmem_cache_node {
// 用于保護 slab 節(jié)點中的 slab 鏈表
spinlock_t list_lock;
#ifdef CONFIG_SLAB
// slab 鏈表,表示 slab 節(jié)點中有部分空閑對象
struct list_head slabs_partial; /* partial list first, better asm code */
// slab 鏈表,表示 slab 節(jié)點中沒有空閑對象
struct list_head slabs_full;
// slab 鏈表,表示 slab 節(jié)點中全部都是空閑對象
struct list_head slabs_free;
// 表示 slab 節(jié)點中有多少個 slab 對象
unsigned long total_slabs; /* length of all slab lists */
// 表示 slab 節(jié)點中有多少個全是空閑對象的 slab 對象
unsigned long free_slabs; /* length of free slab list only */
// 空閑對象的數(shù)目
unsigned long free_objects;
// 表示 slab 節(jié)點中所有空閑對象的最大閾值,即 slab 節(jié)點中可容許的空閑對象數(shù)目最大閾值
unsigned int free_limit;
// 記錄當前著色區(qū)的編號。所有 slab 節(jié)點都按照著色編號來計算著色區(qū)的大小,達到最大值后又從 0 開始計算
unsigned int colour_next; /* Per-node cache coloring */
// 共享對象緩沖區(qū)。在多核 CPU 中,除了本地 CPU 外,slab 節(jié)點中還有一個所有 CPU 都共享的對象緩沖區(qū)
struct array_cache *shared; /* shared per node */
// 用于 NUMA 系統(tǒng)
struct alien_cache **alien; /* on other nodes */
// 下一次收割 slab 節(jié)點的時間
unsigned long next_reap; /* updated without locking */
// 表示訪問了 slabs_free 的 slab 節(jié)點
int free_touched; /* updated without locking */
#endif
...
};
對象緩沖池:
// slab 描述符會給每個 CPU 提供一個對象緩沖池(array_cache)
// array_cache 可以描述本地對象緩沖池,也可以描述共享對象緩沖池
struct array_cache {
// 對象緩沖池中可用對象的數(shù)目
unsigned int avail;
// 對象緩沖池中可用對象數(shù)目的最大閾值
unsigned int limit;
// 遷移對象的數(shù)目,如從共享對象緩沖池或者其他 slab 中遷移空閑對象到該對象緩沖池的數(shù)量
unsigned int batchcount;
// 從緩沖池中移除一個對象時,將 touched 置為 1 ;
// 當收縮緩沖池時,將 touched 置為 0;
unsigned int touched;
// 保存對象的實體
// 指向存儲對象的變長數(shù)組,每一個成員存放一個對象的指針。這個數(shù)組最初最多有 limit 個成員
void *entry[];
};
對象緩沖池的數(shù)據(jù)結構中采用了GCC編譯器的零長數(shù)組,entry[]數(shù)組用于存放多個對象,如下圖所示:
圖片
⑴Cache(緩存)
Slab 分配器中的緩存(Cache)扮演著關鍵的角色。它是為每種類型的對象專門創(chuàng)建的,每個緩存都存儲著相同大小的對象集合。比如,對于特定大小的內核數(shù)據(jù)結構,會有對應的緩存來管理這些對象的分配和釋放。這種設計使得在需要分配相同類型的對象時,可以快速從緩存中獲取,提高了內存分配的效率。
⑵Slab
Slab 是一塊連續(xù)的內存區(qū)域,用于存儲一組特定大小的對象。每個 Slab 都經(jīng)過精心劃分,以適應特定對象的存儲需求。Slab 的大小通常由所存儲對象的大小和數(shù)量決定。當系統(tǒng)需要為特定類型的對象分配內存時,Slab 分配器會從合適的 Slab 中分配空間。如果沒有合適的 Slab 可用,分配器可能會創(chuàng)建新的 Slab。
⑶Object(對象)
Object 是在 Slab 中實際存儲的數(shù)據(jù)單元。每個對象代表著特定類型的數(shù)據(jù)結構或資源。例如,在 Linux 內核中,進程控制塊(PCB)可以作為一個對象存儲在 Slab 中。對象可以處于不同的狀態(tài),如已分配、空閑或部分空閑??臻e對象通過空閑鏈表進行跟蹤,以便在需要分配新對象時能夠快速找到可用的空間。
三、優(yōu)勢解析
3.1 對象復用
Slab 分配器通過緩存對象避免了頻繁的分配與釋放操作。在內核中,會為有限的對象集分配大量內存,例如文件描述符和其他常見結構。而 Slab 分配器圍繞對象緩存進行,將內存保持為針對特定目的而初始化的狀態(tài)。例如,如果內存被分配給了一個互斥鎖,那么只需在為互斥鎖首次分配內存時執(zhí)行一次互斥鎖初始化函數(shù)即可。后續(xù)的內存分配不需要執(zhí)行這個初始化函數(shù),因為從上次釋放和調用析構之后,它已經(jīng)處于所需的狀態(tài)中了。這樣,當系統(tǒng)再次需要相同類型的對象時,可以直接從緩存中獲取,極大地提高了內存分配的效率。
3.2 減少內存碎片
Slab 分配器能夠保證內存分配尺寸和對齊一致,從而有效減少內存碎片問題。每個緩存結構都包括了兩個重要的成員:nodelists 和 array。nodelists 中的 kmem_list3 結構將 slab 分為完全用盡的 slab 鏈表、部分用盡的 slab 鏈表和空閑的 slab 鏈表。部分空閑的 slab 在最開始,當一個 slab 中的所有對象都被使用完時,就從 slabs_partial 列表中移動到 slabs_full 列表中。當一個 slab 完全被分配并且有對象被釋放后,就從 slabs_full 列表中移動到 slabs_partial 列表中。當所有對象都被釋放之后,就從 slabs_partial 列表移動到 slabs_empty 列表中。這種管理方式使得內存的分配和釋放更加有序,減少了內存碎片的產(chǎn)生。
此外,對象在 slab 中不是連續(xù)排列的,為了滿足對齊要求,會在 slab 對象中添加填充字節(jié)以滿足對齊要求,使用對齊的地址可以加速內存訪問。如果創(chuàng)建 slab 時指定了 SLAB_HWCACHE_ALIGN 標志,則會按照 cache_line_size 的返回值對齊,即對齊的硬件緩存行上。如果對象小于硬件緩存行的一半,則將多個對象放入一個緩存行。如果沒有指定對齊標記,則對齊到 BYTES_PER_WORD,即對齊到 void 指針所需字節(jié)數(shù)目。
3.2 高效的內存管理
Slab 分配器適用于不同大小的對象,能夠顯著提升內核的整體性能。Slab 分配器把對象分組放進高速緩存,每個高速緩存都是同種類型對象的一種 “儲備”。一個 cache 管理一組大小固定的內存塊,每個內存塊都可用作一種數(shù)據(jù)結構。cache 中的內存塊來自一到多個 slab,一個 slab 來自物理內存管理器的一到多個物理頁,該 slab 被分成一組固定大小的塊,被稱為 slab 對象。
與傳統(tǒng)的內存管理模式相比,Slab 緩存分配器提供了很多優(yōu)點。首先,內核通常依賴于對小對象的分配,它們會在系統(tǒng)生命周期內進行無數(shù)次分配。Slab 緩存分配器通過對類似大小的對象進行緩存而提供這種功能,從而避免了常見的碎片問題。Slab 分配器還支持通用對象的初始化,從而避免了為同一目的而對一個對象重復進行初始化。最后,Slab 分配器還可以支持硬件緩存對齊和著色,這允許不同緩存中的對象占用相同的緩存行,從而提高緩存的利用率并獲得更好的性能。
四、關鍵結構
4.1 kmem_cache
kmem_cache定義了要管理的給定大小的對象池,是 Linux 內存管理中 Slab 分配器的核心結構之一。它包含多個重要參數(shù)和引用,對內存分配起著關鍵作用。
kmem_cache結構中的struct array_cache __percpu *cpu_cache是一個重要的成員,它是每個 CPU 的對象緩存池,相當于快表。當系統(tǒng)進行內存分配時,會優(yōu)先從這個本地緩存中獲取對象,提高分配速度。
此外,batchcount、limit、shared等參數(shù)分別控制著從共享緩存或其他列表獲取對象的數(shù)量、本地緩存中空閑對象的最大數(shù)量以及多核系統(tǒng)中的共享設置。size參數(shù)表示要管理的對象的長度,這個長度需要加上對齊字節(jié)。flags是對象的分配掩碼,num表示一個 slab 中最多可以有多少個對象。
gfporder參數(shù)決定了一個 slab 中占用的連續(xù)頁框數(shù)的對數(shù),而allocflags則是與伙伴系統(tǒng)交互時提供的分配標識。colour和colour_off參數(shù)用于控制 slab 的顏色設置,實現(xiàn)緩存著色以提高緩存命中率。freelist_cache和freelist_size在 off-slab 時使用,將 freelist 放在 slab 物理頁面外部。
ctor是構造函數(shù)指針,用于在創(chuàng)建對象時進行初始化操作。name是 slab 描述符的名稱,list用于將該結構鏈接到全局鏈表中,refcount是引用次數(shù),在釋放 slab 描述符時會判斷,只有引用次數(shù)為 0 時才真正釋放。object_size是對象的實際大小,align是對齊的長度。
4.2 array_cache
array_cache是每個 CPU 的對象緩存池,在 Slab 分配器中起著實現(xiàn)快速分配和減少操作的關鍵作用。
array_cache結構中的avail表示對象緩存池中可用的對象數(shù)目。limit和batchcount與kmem_cache中的語義一致,分別控制著緩存的上限和從共享緩存或其他列表獲取對象的數(shù)量。touched參數(shù)在從緩存池移除一個對象時置 1,而收縮緩存時置 0。entry數(shù)組保存著對象的實體,采用 LIFO(后進先出)方式進行分配,即將該數(shù)組中的最后一個索引對應的對象分配出去,以保證該對象還駐留在高速緩存中的可能性。
4.3 kmem_cache_node
kmem_cache_node管理從伙伴系統(tǒng)分配的物理頁面,是 Slab 分配器在 NUMA 架構下的重要組成部分。它包含多個 slab 鏈表,對內存的分配和回收進行精細管理。
kmem_cache_node結構中的spinlock_t list_lock用于保護鏈表操作的互斥。在支持 CONFIG_SLAB 的情況下,它包含slabs_partial、slabs_full和slabs_free三個鏈表,分別對應部分用盡的 slab 鏈表、完全用盡的 slab 鏈表和空閑的 slab 鏈表。total_slabs表示三個鏈表中所有 slab 的總數(shù),free_slabs表示空閑 slab 的數(shù)量,free_objects表示三個鏈表中所有空閑對象數(shù)目,free_limit表示 slab 中可以容許的空閑對象數(shù)目最大閾值。colour_next用于控制每個節(jié)點的緩存著色。
shared是多核 CPU 中的共享緩存區(qū) slab 對象的指針。當一個 slab 中的所有對象都被使用完時,就從slabs_partial列表中移動到slabs_full列表中。當一個 slab 完全被分配并且有對象被釋放后,就從slabs_full列表中移動到slabs_partial列表中。當所有對象都被釋放之后,就從slabs_partial列表移動到slabs_empty列表中。這種管理方式使得內存的分配和釋放更加有序,減少了內存碎片的產(chǎn)生。
五、操作流程
5.1 創(chuàng)建 slab 緩存
使用kmem_cache_create函數(shù)創(chuàng)建一個描述特定對象類型內存池的結構。kmem_cache_create函數(shù)需要多個參數(shù),包括可讀的名稱、被管理對象以字節(jié)計的長度、對齊數(shù)據(jù)時使用的偏移量、一組標志以及構造函數(shù)等。首先,對象長度會向上舍入到處理器字長的倍數(shù)。如果設置了SLAB_HWCACHE_ALIGN標志,內核會按照特定于體系結構的函數(shù)cache_line_size給出的值來對齊數(shù)據(jù),并嘗試將盡可能多的對象填充到一個緩存行中。如果對象長度大于頁幀的 1/8,則將頭部管理數(shù)據(jù)存儲在 slab 之外,否則存儲在 slab 上。最后,通過迭代過程找到理想的 slab 長度,并對 slab 進行著色。
圖片
為了使讀者有更真切的理解,下文將根據(jù)流程圖圍繞源代碼進行講解這個過程:
kmem_cache_create
// 創(chuàng)建 slab 描述符
// kmem_cache_create() 函數(shù)用于創(chuàng)建自己的緩存描述符;kmalloc() 函數(shù)用于創(chuàng)建通用的緩存
// name:slab 描述符的名稱
// size:緩沖對象的大小
// align:緩沖對象需要對齊的字節(jié)數(shù)
// flags:分配掩碼
// ctor:對象的構造函數(shù)
struct kmem_cache *
kmem_cache_create(const char *name, unsigned int size, unsigned int align,
slab_flags_t flags, void (*ctor)(void *))
kmem_cache_create->...->__kmem_cache_create
// 創(chuàng)建 slab 緩存描述符
int __kmem_cache_create(struct kmem_cache *cachep, slab_flags_t flags)
{
...
// 讓 slab 描述符的大小和系統(tǒng)的 word 長度對齊(BYTES_PER_WORD)
// 當創(chuàng)建的 slab 描述符的 size 小于 word 長度時,slab 分配器會最終按 word 長度來創(chuàng)建
size = ALIGN(size, BYTES_PER_WORD);
// SLAB_RED_ZONE 檢查是否溢出,實現(xiàn)調試功能
if (flags & SLAB_RED_ZONE) {
ralign = REDZONE_ALIGN;
size = ALIGN(size, REDZONE_ALIGN);
}
/* 3) caller mandated alignment */
// 調用方強制對齊
if (ralign < cachep->align) {
ralign = cachep->align;
}
...
* 4) Store it.
*/
cachep->align = ralign;
// colour_off 表示一個著色區(qū)的長度,它和 L1 高速緩存行大小相同
cachep->colour_off = cache_line_size();
/* Offset must be a multiple of the alignment. */
if (cachep->colour_off < cachep->align)
cachep->colour_off = cachep->align;
// 枚舉類型 slab_state 用來表示 slab 系統(tǒng)中的狀態(tài),如 DOWN、PARTIAL、PARTIAL_NODE、UP 和 FULL 等。當 slab 機制完全初始化完成后狀態(tài)變成 FULL
// slab_is_available() 表示當 slab 分配器處于 UP 或者 FULL 狀態(tài)時,分配掩碼可以使用 GFP_KERNEL;否則,只能使用 GFP_NOWAIT
if (slab_is_available())
gfp = GFP_KERNEL;
else
gfp = GFP_NOWAIT;
...
// slab 對象的大小按照 cachep->align 大小來對齊
size = ALIGN(size, cachep->align);
...
// 若數(shù)組 freelist 小于一個 slab 對象的大小并且沒有指定構造函數(shù),那么 slab 分配器就可以采用 OBJFREELIST_SLAB 模式
if (set_objfreelist_slab_cache(cachep, size, flags)) {
flags |= CFLGS_OBJFREELIST_SLAB;
goto done;
}
// 若一個 slab 分配器的剩余空間小于 freelist 數(shù)組的大小,那么使用 OFF_SLAB 模式
if (set_off_slab_cache(cachep, size, flags)) {
flags |= CFLGS_OFF_SLAB;
goto done;
}
// 若一個 slab 分配器的剩余空間大于 slab 管理數(shù)組大小,那么使用正常模式
if (set_on_slab_cache(cachep, size, flags))
goto done;
return -E2BIG;
done:
// freelist_size 表示一個 slab 分配器中管理區(qū)————freelist 大小
cachep->freelist_size = cachep->num * sizeof(freelist_idx_t);
cachep->flags = flags;
cachep->allocflags = __GFP_COMP;
if (flags & SLAB_CACHE_DMA)
cachep->allocflags |= GFP_DMA;
if (flags & SLAB_RECLAIM_ACCOUNT)
cachep->allocflags |= __GFP_RECLAIMABLE;
// size 表示一個 slab 對象的大小
cachep->size = size;
cachep->reciprocal_buffer_size = reciprocal_value(size);
...
// 繼續(xù)配置 slab 描述符
err = setup_cpu_cache(cachep, gfp);
if (err) {
__kmem_cache_release(cachep);
return err;
}
return 0;
}
5.2 分配內存
通過kmem_cache_alloc從已創(chuàng)建的 slab 緩存中分配內存。首先會從每個 CPU 的本地對象緩存池(array_cache)中獲取對象,如果本地緩存為空,則從共享緩存或其他列表中獲取。如果所有列表中都沒有空閑對象,則會調用cache_grow函數(shù)創(chuàng)建新的 slab。kmem_cache_alloc() 函數(shù)的流程圖如下所示:
圖片
為了使讀者有更真切的理解,下文將根據(jù)流程圖圍繞源代碼進行講解這個過程:kmem_cache_alloc->slab_alloc
// slab_alloc() 函數(shù)在 slab 對象分配過程中是全程關閉本地中斷的
static __always_inline void *
slab_alloc(struct kmem_cache *cachep, gfp_t flags, unsigned long caller)
{
...
local_irq_save(save_flags);
// 獲取 slab 對象
objp = __do_cache_alloc(cachep, flags);
local_irq_restore(save_flags);
...
// 如果分配時設置了 __GFP_ZERO 標志位,那么使用 memset() 把 slab 對象的內容清零
if (unlikely(flags & __GFP_ZERO) && objp)
memset(objp, 0, cachep->object_size);
slab_post_alloc_hook(cachep, flags, 1, &objp);
return objp;
}
kmem_cache_alloc->slab_alloc->...->____cache_alloc
// 獲取 slab 對象
static inline void *____cache_alloc(struct kmem_cache *cachep, gfp_t flags)
{
void *objp;
struct array_cache *ac;
check_irq_off();
// 獲取 slab 描述符 cachep 中的本地對象緩沖池 ac
ac = cpu_cache_get(cachep);
// 判斷本地對象緩沖池中有沒有空閑的對象
if (likely(ac->avail)) {
ac->touched = 1;
// 獲取 slab 對象
objp = ac->entry[--ac->avail];
STATS_INC_ALLOCHIT(cachep);
goto out;
}
STATS_INC_ALLOCMISS(cachep);
// 第一次分配緩存對象時 ac->avail 值為 0,所以它應該在 cache_alloc_refill() 函數(shù)中
objp = cache_alloc_refill(cachep, flags);
...
return objp;
}
kmem_cache_alloc->slab_alloc->__do_cache_alloc->____cache_alloc->cache_alloc_refill
static void *cache_alloc_refill(struct kmem_cache *cachep, gfp_t flags)
{
...
// 獲取本地對象緩沖池 ac
ac = cpu_cache_get(cachep);
...
// 獲取 slab 節(jié)點
n = get_node(cachep, node);
BUG_ON(ac->avail > 0 || !n);
// shared 表示共享對象緩沖池
shared = READ_ONCE(n->shared);
// 若 slab 節(jié)點沒有空閑對象并且共享對象緩沖池 shared 為空或者共享對象緩沖池里也沒有空閑對象,那么直接跳轉到 direct_grow 標簽處
if (!n->free_objects && (!shared || !shared->avail))
goto direct_grow;
...
// 若共享對象緩沖池里有空閑對象,那么嘗試遷移 batchcount 個空閑對象到本地對象緩沖池 ac 中
// transfer_objects() 函數(shù)用于從共享對象緩沖池遷移空閑對象到本地對象緩沖池
if (shared && transfer_objects(ac, shared, batchcount)) {
shared->touched = 1;
goto alloc_done;
}
while (batchcount > 0) {
/* Get slab alloc is to come from. */
// 如果共享對象緩沖池中沒有空閑對象,那么 get_first_slab() 函數(shù)會查看 slab 節(jié)點中的 slabs_partial 鏈表和 slabs_free 鏈表
page = get_first_slab(n, false);
if (!page)
goto must_grow;
check_spinlock_acquired(cachep);
// 從 slab 分配器中遷移 batchcount 個空閑對象到本地對象緩沖池中
batchcount = alloc_block(cachep, ac, page, batchcount);
fixup_slab_list(cachep, n, page, &list);
}
must_grow:
// 更新 slab 節(jié)點中的 free_objects 計數(shù)值
n->free_objects -= ac->avail;
alloc_done:
spin_unlock(&n->list_lock);
fixup_objfreelist_debug(cachep, &list);
// 表示 slab 節(jié)點沒有空閑對象并且共享對象緩沖池中也沒有空閑對象,這說明整個內存節(jié)點里沒有 slab 空閑對象
// 這種情況下只能重新分配 slab 分配器,這就是一開始初始化和配置 slab 描述符的情景
direct_grow:
if (unlikely(!ac->avail)) {
/* Check if we can use obj in pfmemalloc slab */
if (sk_memalloc_socks()) {
void *obj = cache_alloc_pfmemalloc(cachep, n, flags);
if (obj)
return obj;
}
// 分配一個 slab 分配器
page = cache_grow_begin(cachep, gfp_exact_node(flags), node);
/*
* cache_grow_begin() can reenable interrupts,
* then ac could change.
*/
ac = cpu_cache_get(cachep);
if (!ac->avail && page)
// 從剛分配的 slab 分配器的空閑對象中遷移 batchcount 個空閑對象到本地對象緩沖池中
alloc_block(cachep, ac, page, batchcount);
// 把剛分配的 slab 分配器添加到合適的隊列中,這個場景下應該添加到 slabs_partial 鏈表中
cache_grow_end(cachep, page);
if (!ac->avail)
return NULL;
}
// 設置本地對象緩沖池的 touched 為 1,表示剛剛使用過本地對象緩沖池
ac->touched = 1;
// 返回一個空閑對象
return ac->entry[--ac->avail];
}
5.3 釋放內存
使用kmem_cache_free釋放內存。如果 per-CPU 緩存中的對象數(shù)目低于允許的限制,則在其中存儲一個指向緩存中對象的指針。否則,將一些對象從緩存移回 slab,并將剩余的對象向數(shù)組起始處移動。如果在刪除之后,slab 中的所有對象都是未使用的,且緩存中空閑對象的數(shù)目超過預定義的限制,則使用slab_destroy將整個 slab 返回給伙伴系統(tǒng)。如果 slab 同時包含使用和未使用對象,則插入到slabs_partial鏈表。如果要銷毀只包含未使用對象的一個緩存,則必須調用kmem_cache_destroy函數(shù)。該流程如下所示:
圖片
為了使讀者有更真切的理解,下文將根據(jù)流程圖圍繞源代碼進行講解這個過程:kmem_cache_free->__cache_free->___cache_free
void ___cache_free(struct kmem_cache *cachep, void *objp,
unsigned long caller)
{
struct array_cache *ac = cpu_cache_get(cachep);
...
// 當本地對象緩沖池的空閑對象數(shù)量 ac->avail 大于或等于 ac->limit 閾值時,就會調用 cache_flusharray() 做刷新動作,嘗試回收空閑對象
if (ac->avail < ac->limit) {
STATS_INC_FREEHIT(cachep);
} else {
STATS_INC_FREEMISS(cachep);
// 主要用于回收 slab 分配器
cache_flusharray(cachep, ac);
}
...
// 把對象釋放到本地對象緩沖池 ac 中
ac->entry[ac->avail++] = objp;
}
六、重要字段解析
name(緩存名稱)
表示 slab 緩存的名稱,存儲相同類型和大小的對象。例如 /proc/slabinfo 文件中的kmem_cache字段表示緩存的對象名,如task_struct。每個 slab 緩存存儲相同類型和大小的對象,例如kmalloc-32是用于分配 32 字節(jié)的內存塊。
active_objs(活動對象數(shù))
當前在使用的對象數(shù)量。該字段表示當前在使用的(已分配的)對象數(shù)量,即系統(tǒng)中實際分配給內核使用的對象數(shù)量。例如,在系統(tǒng)中實際分配給內核使用的進程控制塊等對象的數(shù)量就是活動對象數(shù)。
num_objs(總對象數(shù))
緩存中分配的對象總數(shù)。這個字段表示緩存中分配的對象總數(shù),包括已經(jīng)分配和空閑的對象。這個值通常大于或等于active_objs。比如一個特定大小的 slab 緩存中,所有已經(jīng)分配出去和尚未分配但已準備好的對象總數(shù)就是num_objs。
objsize(對象大小)
每個對象的大小。該字段表示每個對象的大?。ㄒ宰止?jié)為單位),即 slab 緩存中每個對象占用的內存空間大小。例如對于特定的內核數(shù)據(jù)結構,其對象大小可以通過這個字段確定。
objperslab(每個 slab 包含的對象數(shù))
每個 slab 中包含的對象數(shù)量。每個 slab 是一個較大的內存塊,其中包含多個對象,這個字段表示每個 slab 中具體包含的對象數(shù)量。例如對于一個特定大小的 slab,根據(jù)對象大小和 slab 總大小,可以計算出objperslab的值。
pagesperslab(每個 slab 使用的頁數(shù))
每個 slab 使用的頁數(shù),通常為 4KB 大小。該字段表示每個 slab 使用的頁數(shù)。Linux 內核使用分頁機制來管理內存,頁面通常為 4KB 大小。一個 slab 由一定數(shù)量的連續(xù)物理頁組成,這個字段反映了每個 slab 占用的物理頁數(shù)量。
tunables(可調參數(shù))
控制 slab 緩存的行為,如limit、batchcount、sharedfactor等。這個字段中的值控制 slab 緩存的行為:
- limit:slab 緩存中每個 CPU 可以緩存的最大對象數(shù)量。
- batchcount:每次從全局緩存到 CPU 本地緩存中批量獲取的對象數(shù)。
- sharedfactor:控制多個 CPU 是否共享 slab 緩存。
slabdata(slab 統(tǒng)計信息)
包含有關 slab 使用的統(tǒng)計數(shù)據(jù),如active_slabs、num_slabs、sharedavail等。這部分包含有關 slab 使用的統(tǒng)計數(shù)據(jù):
- active_slabs:當前正在使用的 slab 數(shù)量。
- num_slabs:系統(tǒng)中分配的總 slab 數(shù)量。
- sharedavail:CPU 本地緩存中可用對象的數(shù)量。通過這些統(tǒng)計數(shù)據(jù),管理員可以了解系統(tǒng)中 slab 分配器的內存使用情況,幫助調優(yōu)系統(tǒng)性能。
七、高速緩存分類
7.1 普通高速緩存
普通高速緩存為 kmem_cache結構本身和內核提供通用高速緩存。它通過通用高速緩存實現(xiàn)小塊連續(xù)內存的分配,為內核提供了一種高效的內存管理方式。
普通高速緩存首先會為 kmem_cache結構本身提供高速緩存,這類緩存保存在 cache_cache變量中。cache_cache變量代表著 cache_chain 鏈表中的第一個元素,它保存著對高速緩存描述符的高速緩存。
通用高速緩存所提供的對象具有幾何分布的大小,范圍為 32 到 131072 字節(jié)。內核中提供了 kmalloc()和 kfree()兩個接口分別進行內存的申請和釋放。
7.2 專用高速緩存
專用高速緩存是根據(jù)內核所需,通過指定具體的對象而創(chuàng)建。它提供一套完整的接口用于高速緩存的申請、釋放以及 slab 的申請和釋放。
內核為專用高速緩存的申請和釋放提供了接口,kmem_cache_create()用于對一個指定的對象創(chuàng)建高速緩存。它從 cache_cache普通高速緩存中為新的專有緩存分配一個高速緩存描述符,并把這個描述符插入到高速緩存描述符形成的 cache_chain鏈表中。kmem_cache_destory()用于撤銷一個高速緩存,并將它從 cache_chain鏈表上刪除。
對于 slab 的申請和釋放,kmem_cache_alloc()在其參數(shù)所指定的高速緩存中分配一個 slab。相反,kmem_cache_free()在其參數(shù)所指定的高速緩存中釋放一個 slab。
八、應用案例分析
8.1 定義和使用特定大小的對象
以下以專用 slab 緩存為例,展示如何定義和使用特定大小的對象。
首先,我們需要為特定的結構體創(chuàng)建一個專用的 slab 緩存。假設我們有一個名為sample_struct的結構體:
struct sample_struct {
int id;
char name[20];
char address[50];
};
我們可以使用kmem_cache_create函數(shù)來創(chuàng)建一個用于存儲sample_struct結構體的專用 slab 緩存:
static struct kmem_cache *sample_struct_cachep;
static void init_sample_struct_cache( void ){
sample_struct_cachep = kmem_cache_create(
"sample_struct_cachep", /* Name */
sizeof(struct sample_struct), /* Object Size */
0, /* Alignment */
SLAB_HWCACHE_ALIGN, /* Flags */
NULL); /* Constructor */
return;
}
這里創(chuàng)建的特定緩存包含sample_struct大小的對象,并且是硬件緩存對齊的(由標志參數(shù)SLAB_HWCACHE_ALIGN定義)。
使用所分配的 slab 緩存對象可以通過以下方式進行:
int slab_test( void ){
struct sample_struct *object;
printk( "Cache name is %s\n", kmem_cache_name( sample_struct_cachep ) );
printk( "Cache object size is %d\n", kmem_cache_size( sample_struct_cachep ) );
object = kmem_cache_alloc(sample_struct_cachep, GFP_KERNEL);
if (object) {
// 使用 object...
kmem_cache_free(sample_struct_cachep, object);
}
return 0;
}
8.2 銷毀緩存
調用者必須確保在執(zhí)行銷毀操作過程中,不要從緩存中分配對象。銷毀緩存的主要步驟如下:
- 將緩存從cache_chain鏈表中刪除。
- 將本地高速緩存、alien 高速緩存和共享本地高速緩存中的對象都釋放回 slab,并釋放所有的 free 鏈表,然后判斷 full 鏈表以及 partial 鏈表是否都為空,如果有一個不為空說明存在非空閑 slab,也就是說有對象還未釋放,此時無法銷毀緩存,重新將緩存添加到cache_chain鏈表中。
- 確定所有的 slab 都為空閑狀態(tài)后,將緩存涉及到的所有描述符都釋放(這些描述符都是保存在普通高速緩存中的)。
負責銷毀緩存的函數(shù)為kmem_cache_destroy,其代碼實現(xiàn)如下:
void kmem_cache_destroy(struct kmem_cache *cachep){
BUG_ON(!cachep || in_interrupt());
/* Find the cache in the chain of caches. */
get_online_cpus();
mutex_lock(&cache_chain_mutex);
/** the chain is never empty, cache_cache is never destroyed*//*將 cache 從 cache_chain 中刪除*/
list_del(&cachep->next);
/*釋放完 free 鏈表,如果 FULL 鏈表或 partial 鏈表中還有 slab,說明還有對象處于分配狀態(tài)因此不能銷毀該緩存!*/
if (__cache_shrink(cachep)) {
slab_error(cachep, "Can't free all objects");
/*重新將緩存添加到 cache_chain 鏈表中*/
list_add(&cachep->next, &cache_chain);
mutex_unlock(&cache_chain_mutex);
put_online_cpus();
return;
}
if (unlikely(cachep->flags & SLAB_DESTROY_BY_RCU))
rcu_barrier();
/*釋放 cache 所涉及到的各個描述符的存儲對象*/
__kmem_cache_destroy(cachep);
mutex_unlock(&cache_chain_mutex);
put_online_cpus();
}
static int __cache_shrink(struct kmem_cache *cachep){
int ret = 0, i = 0;
struct kmem_list3 *l3;
/*將本地高速緩存,share 本地高速緩存以及 alien 高速緩存的空閑對象釋放 slab*/
drain_cpu_caches(cachep);
check_irq_on();
for_each_online_node(i) {
l3 = cachep->nodelists[i];
if (!l3) continue;
/*銷毀空閑鏈表中的 slab*/
drain_freelist(cachep, l3, l3->free_objects);
/*判斷 full 和 partial 是否為空,有一個不為空則 ret 就為 1*/
ret +=!list_empty(&l3->slabs_full) ||!list_empty(&l3->slabs_partial);
}
return (ret? 1 : 0);
}
drain_cpu_caches()的最終落腳在 free_block()函數(shù)上,該函數(shù)在前面已做過分析,在此不再列出。
static int drain_freelist(struct kmem_cache *cache, struct kmem_list3 *l3, int tofree){
struct list_head *p;
int nr_freed;
struct slab *slabp;
nr_freed = 0;
/*slab 中的對象還未釋放完并且 free 鏈表不為空*/
while (nr_freed < tofree &&!list_empty(&l3->slabs_free)) {
spin_lock_irq(&l3->list_lock);
p = l3->slabs_free.prev;
if (p == &l3->slabs_free) {/*鏈表中已無元素*/
spin_unlock_irq(&l3->list_lock);
goto out;
}
/*從 free 鏈表中取出一個 slab*/
slabp = list_entry(p, struct slab, list);
#if DEBUG
BUG_ON(slabp->inuse);
#endif
/*從鏈表中刪除*/
list_del(&slabp->list);
/** Safe to drop the lock. The slab is no longer linked* to the cache.*//*空閑對象數(shù)量總數(shù)減去 num*/
l3->free_objects -= cache->num;
spin_unlock_irq(&l3->list_lock);
/*銷毀 slab*/
slab_destroy(cache, slabp);
nr_freed++;
}
out:
return nr_freed;
}
slab_destroy()函數(shù)已在前文中分析。
static void __kmem_cache_destroy(struct kmem_cache *cachep){
int i;
struct kmem_list3 *l3;
/*釋放存儲本地高速緩存描述符的對象*/
for_each_online_cpu(i)
kfree(cachep->array[i]);
/* NUMA: free the list3 structures */
for_each_online_node(i) {
l3 = cachep->nodelists[i];
if (l3) {
/*釋放存儲共享本地高速緩存描述符的對象*/
kfree(l3->shared);
/*釋放存儲 alien 本地高速緩存描述符的對象*/
free_alien_cache(l3->alien);
/*釋放存儲 kmem_list3 描述符的對象*/
kfree(l3);
}
/*釋放存儲緩存描述符的對象*/
kmem_cache_free(&cache_cache, cachep);
}}