![image](https://img2023.cnblogs.com/blog/2907560/202306/2907560-20230621111419985-1823538417.png) 本文是筆者 slab 系列的最後一篇文章,為了方便大家快速檢索,先將相關的文章列舉出來: - [《細節 ...
本文是筆者 slab 系列的最後一篇文章,為了方便大家快速檢索,先將相關的文章列舉出來:
在之前的這四篇文章中,筆者詳細的為大家介紹了 slab 記憶體池的整體架構演化過程,隨後基於這個演化過程,介紹了整個 slab alloactor 體系的創建,記憶體分配,記憶體釋放以及銷毀等相關複雜流程在內核中的實現。
我們知道 slab 記憶體池是專門為了應對內核中關於小記憶體分配需求而應運而生的,內核會為每一個核心數據結構創建一個專屬的 slab 記憶體池,專門用於內核核心對象頻繁分配和釋放的場景。比如,內核中的 task_struct 結構,mm_struct 結構,struct page 結構,struct file 結構,socket 結構等等,在內核中都有一個屬於自己的專屬 slab 記憶體池。
而之前介紹的這些都屬於專有的 slab 記憶體池,slab 在向伙伴系統申請若幹物理記憶體頁 page 之後,內核會按照需要被池化的專有數據結構在記憶體中的佈局 size,從這些物理記憶體頁中劃分出多個大小相同的記憶體塊出來,然後將這些劃分出來的記憶體塊統一交給其所屬的 slab 記憶體池管理。每個記憶體塊用來專門存儲特定結構的內核對象,不能用作其他用途。
內核中除了上述這些專有記憶體的分配需求之外,其實更多的是通用小記憶體的分配需求,比如說,內核會申請一些 8 位元組,16 位元組,32 位元組等特定尺寸的通用記憶體塊,內核並不會限制這些通用記憶體塊的用途,可以拿它們來存儲任何信息。
內核為了應對這些通用小記憶體的頻繁分配釋放需求,於是本文的主題 —— kmalloc 記憶體池體系就應用而生了,在內核啟動初始化的時候,通過 kmem_cache_create 介面函數預先創建多個特定尺寸的 slab cache 出來,用以應對不同尺寸的通用記憶體塊的申請。
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 函數中的 size 參數來指定要創建的通用記憶體塊尺寸,相關的創建流程細節,感興趣的同學可以回看下這篇文章 《從內核源碼看 slab 記憶體池的創建初始化流程》。
kmalloc 記憶體池體系的底層基石是基於 slab alloactor 體系構建的,其本質其實就是各種不同尺寸的通用 slab cache。
我們可以通過 cat /proc/slabinfo
命令來查看系統中不同尺寸的通用 slab cache:
kmalloc-32
是專門為 32 位元組的記憶體塊定製的 slab cache,用於應對 32 位元組小記憶體塊的分配與釋放。kmalloc-64
是專門為 64 位元組的記憶體塊定製的 slab cache,kmalloc-1k
是專門為 1K 大小的記憶體塊定製的 slab cache 等等。那麼 kmalloc 體系究竟包含了哪些尺寸的通用 slab cache 呢 ?
1. kmalloc 記憶體池中都有哪些尺寸的記憶體塊
本文內核源碼部分基於 5.4 版本討論
內核將這些不同尺寸的 slab cache 分類信息定義在 kmalloc_info[]
數組中,數組中的元素類型為 kmalloc_info_struct 結構,裡邊定義了對應尺寸通用記憶體池的相關信息。
const struct kmalloc_info_struct kmalloc_info[];
/* A table of kmalloc cache names and sizes */
extern const struct kmalloc_info_struct {
// slab cache 的名字
const char *name;
// slab cache 提供的記憶體塊大小,單位為位元組
unsigned int size;
} kmalloc_info[];
- size 用於指定該 slab cache 中所管理的通用記憶體塊尺寸。
- name 為該通用 slab cache 的名稱,名稱形式為
kmalloc-記憶體塊尺寸(單位位元組)
,這一點我們可以通過cat /proc/slabinfo
命令查看。
const struct kmalloc_info_struct kmalloc_info[] __initconst = {
{NULL, 0}, {"kmalloc-96", 96},
{"kmalloc-192", 192}, {"kmalloc-8", 8},
{"kmalloc-16", 16}, {"kmalloc-32", 32},
{"kmalloc-64", 64}, {"kmalloc-128", 128},
{"kmalloc-256", 256}, {"kmalloc-512", 512},
{"kmalloc-1k", 1024}, {"kmalloc-2k", 2048},
{"kmalloc-4k", 4096}, {"kmalloc-8k", 8192},
{"kmalloc-16k", 16384}, {"kmalloc-32k", 32768},
{"kmalloc-64k", 65536}, {"kmalloc-128k", 131072},
{"kmalloc-256k", 262144}, {"kmalloc-512k", 524288},
{"kmalloc-1M", 1048576}, {"kmalloc-2M", 2097152},
{"kmalloc-4M", 4194304}, {"kmalloc-8M", 8388608},
{"kmalloc-16M", 16777216}, {"kmalloc-32M", 33554432},
{"kmalloc-64M", 67108864}
};
從 kmalloc_info[] 數組中我們可以看出,kmalloc 記憶體池體系理論上最大可以支持 64M 尺寸大小的通用記憶體池。
kmalloc_info[] 數組中的 index 有一個特點,從 index = 3 開始一直到數組的最後一個 index,這其中的每一個 index 都表示其對應的 kmalloc_info[index] 指向的通用 slab cache 尺寸,也就是說 kmalloc 記憶體池體系中的每個通用 slab cache 中記憶體塊的尺寸由其所在的 kmalloc_info[] 數組 index 決定,對應記憶體塊大小為:2^index
位元組,比如:
- kmalloc_info[3] 對應的通用 slab cache 中所管理的記憶體塊尺寸為 8 位元組。
- kmalloc_info[5] 對應的通用 slab cache 中所管理的記憶體塊尺寸為 32 位元組。
- kmalloc_info[9] 對應的通用 slab cache 中所管理的記憶體塊尺寸為 512 位元組。
- kmalloc_info[index] 對應的通用 slab cache 中所管理的記憶體塊尺寸為 2^index 位元組。
但是這裡的 index = 1 和 index = 2 是個例外,內核單獨支持了 kmalloc-96 和 kmalloc-192 這兩個通用 slab cache。它們分別管理了 96 位元組大小和 192 位元組大小的通用記憶體塊。這些記憶體塊的大小都不是 2 的次冪。
那麼內核為什麼會單獨支持這兩個尺寸而不是其他尺寸的通用 slab cache 呢?
因為在內核中,對於記憶體塊的申請需求大部分情況下都在 96 位元組或者 192 位元組附近,如果內核不單獨支持這兩個尺寸的通用 slab cache。那麼當內核申請一個尺寸在 64 位元組到 96 位元組之間的記憶體塊時,內核會直接從 kmalloc-128 中分配一個 128 位元組大小的記憶體塊,這樣就導致了記憶體塊內部碎片比較大,浪費寶貴的記憶體資源。
同理,當內核申請一個尺寸在 128 位元組到 192 位元組之間的記憶體塊時,內核會直接從 kmalloc-256 中分配一個 256 位元組大小的記憶體塊。
當內核申請超過 256 位元組的記憶體塊時,一般都是會按照 2 的次冪來申請的,所以這裡只需要單獨支持 kmalloc-96 和 kmalloc-192 即可。
在我們清楚了 kmalloc 體系中通用記憶體塊的尺寸分佈之後,那麼當內核向 kmalloc 申請通用記憶體塊的時候,在 kmalloc 的內部又是如何查找出一個最合適的尺寸呢 ?
2. kmalloc 記憶體池如何選取合適尺寸的記憶體塊
既然 kmalloc 體系中通用記憶體塊的尺寸分佈信息可以通過一個數組 kmalloc_info[] 來定義,那麼同理,最佳記憶體塊尺寸的選取規則也可以被定義在一個數組中。
內核通過定義一個 size_index[24]
數組來存放申請記憶體塊大小在 192 位元組以下的 kmalloc 記憶體池選取規則。
其中 size_index[24]
數組中每個元素後面跟的註釋部分為內核要申請的位元組數,size_index[24]
數組中每個元素表示最佳合適尺寸的通用 slab cache 在 kmalloc_info[] 數組中的索引。
static u8 size_index[24] __ro_after_init = {
3, /* 8 */
4, /* 16 */
5, /* 24 */
5, /* 32 */
6, /* 40 */
6, /* 48 */
6, /* 56 */
6, /* 64 */
1, /* 72 */
1, /* 80 */
1, /* 88 */
1, /* 96 */
7, /* 104 */
7, /* 112 */
7, /* 120 */
7, /* 128 */
2, /* 136 */
2, /* 144 */
2, /* 152 */
2, /* 160 */
2, /* 168 */
2, /* 176 */
2, /* 184 */
2 /* 192 */
};
-
size_index[0] 存儲的信息表示,如果內核申請的記憶體塊低於 8 位元組時,那麼 kmalloc 將會到 kmalloc_info[3] 所指定的通用 slab cache —— kmalloc-8 中分配一個 8 位元組大小的記憶體塊。
-
size_index[16] 存儲的信息表示,如果內核申請的記憶體塊在 128 位元組到 136 位元組之間時,那麼 kmalloc 將會到 kmalloc_info[2] 所指定的通用 slab cache —— kmalloc-192 中分配一個 192 位元組大小的記憶體塊。
-
同樣的道理,申請 144,152,160 ..... 192 等位元組尺寸的記憶體塊對應的最佳 slab cache 選取規則也是如此,都是通過 size_index 數組中的值找到 kmalloc_info 數組的索引,然後通過 kmalloc_info[index] 指定的 slab cache,分配對應尺寸的記憶體塊。
size_index 數組只是定義申請記憶體塊在 192 位元組以下的 kmalloc 記憶體池選取規則,當申請記憶體塊的尺寸超過 192 位元組時,內核會通過 fls 函數來計算 kmalloc_info 數組中的通用 slab cache 索引。這一點我們在後續源碼分析中還會在提到,這裡大家有個大概印象即可。
關於 fls 函數筆者在之前的文章中已經多次提到過,fls 可以獲取參數的最高有效 bit 的位數,比如: fls(0)=0,fls(1)=1,fls(4) = 3。
3. kmalloc 記憶體池的整體架構
kmalloc 記憶體池的本質其實還是 slab 記憶體池,底層依賴於 slab alloactor 體系,在 kmalloc 體系的內部,管理了多個不同尺寸的 slab cache,kmalloc 只不過負責根據內核申請的記憶體塊尺寸大小來選取一個最佳合適尺寸的 slab cache。
最終記憶體塊的分配和釋放還需要由底層的 slab cache 來負責,經過前兩個小節的介紹,現在我們已經對 kmalloc 記憶體池架構有了一個初步的認識。
const struct kmalloc_info_struct kmalloc_info[] __initconst = {
{NULL, 0}, {"kmalloc-96", 96},
{"kmalloc-192", 192}, {"kmalloc-8", 8},
{"kmalloc-16", 16}, {"kmalloc-32", 32},
{"kmalloc-64", 64}, {"kmalloc-128", 128},
{"kmalloc-256", 256}, {"kmalloc-512", 512},
{"kmalloc-1k", 1024}, {"kmalloc-2k", 2048},
{"kmalloc-4k", 4096}, {"kmalloc-8k", 8192},
{"kmalloc-16k", 16384}, {"kmalloc-32k", 32768},
{"kmalloc-64k", 65536}, {"kmalloc-128k", 131072},
{"kmalloc-256k", 262144}, {"kmalloc-512k", 524288},
{"kmalloc-1M", 1048576}, {"kmalloc-2M", 2097152},
{"kmalloc-4M", 4194304}, {"kmalloc-8M", 8388608},
{"kmalloc-16M", 16777216}, {"kmalloc-32M", 33554432},
{"kmalloc-64M", 67108864}
};
我們看到 kmalloc_info[] 數組中定義的記憶體塊尺寸非常的多,但實際上 kmalloc 體系所支持的記憶體塊尺寸與 slab allocator 體系的實現有關,在 Linux 內核中 slab allocator 體系的實現分為三種:slab 實現,slub 實現,slob 實現。
而在被大規模運用的伺服器 Linux 操作系統中,slab allocator 體系採用的是 slub 實現,所以本文我們還是以 slub 實現來討論。
kmalloc 體系所能支持的記憶體塊尺寸範圍由 KMALLOC_SHIFT_LOW 和 KMALLOC_SHIFT_HIGH 決定,它們被定義在 /include/linux/slab.h
文件中:
#ifdef CONFIG_SLUB
// slub 最大支持分配 2頁 大小的對象,對應的 kmalloc 記憶體池中記憶體塊尺寸最大就是 2頁
// 超過 2頁 大小的記憶體塊直接向伙伴系統申請
#define KMALLOC_SHIFT_HIGH (PAGE_SHIFT + 1)
#define KMALLOC_SHIFT_LOW 3
#define PAGE_SHIFT 12
其中 kmalloc 支持的最小記憶體塊尺寸為:2^KMALLOC_SHIFT_LOW
,在 slub 實現中 KMALLOC_SHIFT_LOW = 3,kmalloc 支持的最小記憶體塊尺寸為 8 位元組大小。
kmalloc 支持的最大記憶體塊尺寸為:2^KMALLOC_SHIFT_HIGH
,在 slub 實現中 KMALLOC_SHIFT_HIGH = 13,kmalloc 支持的最大記憶體塊尺寸為 8K ,也就是兩個記憶體頁大小。
KMALLOC_SHIFT_LOW,KMALLOC_SHIFT_HIGH 在 slab 實現,slob 實現中的配置值均不一樣,這裡筆者就不詳細展開了。
所以,實際上,在內核的 slub 實現中,kmalloc 所能支持的記憶體塊大小在 8 位元組到 8K 之間。
好了,現在 kmalloc 體系中的記憶體塊尺寸我們已經劃分好了,那麼 kmalloc 體系中的這些不同尺寸的記憶體塊究竟來自於哪些物理記憶體區域呢 ?
筆者在 《一步一圖帶你深入理解 Linux 物理記憶體管理》一文中的 “4.3 NUMA 節點物理記憶體區域的劃分” 小節中曾介紹到,內核會根據各個物理記憶體區域的功能不同,將 NUMA 節點內的物理記憶體劃分為以下幾個物理記憶體區域:
// 定義在文件: /include/linux/mmzone.h
enum zone_type {
#ifdef CONFIG_ZONE_DMA
ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
ZONE_DMA32,
#endif
ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
ZONE_HIGHMEM,
#endif
ZONE_MOVABLE,
#ifdef CONFIG_ZONE_DEVICE
ZONE_DEVICE,
#endif
// 充當結束標記, 在內核中想要迭代系統中所有記憶體域時, 會用到該常量
__MAX_NR_ZONES
};
而 kmalloc 記憶體池中的記憶體來自於上面的 ZONE_DMA 和 ZONE_NORMAL 物理記憶體區域,也就是內核虛擬記憶體空間中的直接映射區域。
kmalloc 記憶體池中的記憶體來源類型定義在 /include/linux/slab.h
文件中:
enum kmalloc_cache_type {
// 規定 kmalloc 記憶體池的記憶體需要在 NORMAL 直接映射區分配
KMALLOC_NORMAL = 0,
// 規定 kmalloc 記憶體池中的記憶體是可以回收的,比如文件頁緩存,匿名頁
KMALLOC_RECLAIM,
#ifdef CONFIG_ZONE_DMA
// kmalloc 記憶體池中的記憶體用於 DMA,需要在 DMA 區域分配
KMALLOC_DMA,
#endif
NR_KMALLOC_TYPES
};
-
KMALLOC_NORMAL 表示 kmalloc 需要從 ZONE_NORMAL 物理記憶體區域中分配記憶體。
-
KMALLOC_DMA 表示 kmalloc 需要從 ZONE_DMA 物理記憶體區域中分配記憶體。
-
KMALLOC_RECLAIM 表示需要分配可以被回收的記憶體,RECLAIM 類型的記憶體頁,不能移動,但是可以直接回收,比如文件緩存頁,它們就可以直接被回收掉,當再次需要的時候可以從磁碟中讀取生成。或者一些生命周期比較短的記憶體頁,比如 DMA 緩存區中的記憶體頁也是可以被直接回收掉。
現在我們在把 kmalloc 記憶體池中的記憶體來源加上,kmalloc 的總體架構又有了新的變化:
上圖中所展示的 kmalloc 記憶體池整體架構體系,內核將其定義在一個 kmalloc_caches 二維數組中,位於文件:/include/linux/slab.h
中。
struct kmem_cache *
kmalloc_caches[NR_KMALLOC_TYPES][KMALLOC_SHIFT_HIGH + 1];
-
第一維數組用於表示 kmalloc 記憶體池中的記憶體來源於哪些物理記憶體區域中,也就是前邊介紹的
enum kmalloc_cache_type
。 -
第二維數組中的元素一共 KMALLOC_SHIFT_HIGH 個,用於存儲每種記憶體塊尺寸對應的 slab cache。在 slub 實現中,kmalloc 記憶體池中的記憶體塊尺寸在 8位元組到 8K 之間,其中還包括了兩個特殊的尺寸分別為 96 位元組 和 192 位元組。
第二維數組中的 index 表示的含義和 kmalloc_info[] 數組中的 index 含義一模一樣,均是表示對應 slab cache 中記憶體塊尺寸的分配階(2 的次冪)。96 和 192 這兩個記憶體塊尺寸除外,它們的 index 分別是 1 和 2,單獨特殊指定。
好了,到現在我們已經清楚了 kmalloc 記憶體池的整體架構,那麼這個架構體系又是如何被創建出來的呢 ?我們帶著這個疑問,接著往下看~~~
4. kmalloc 記憶體池的創建
由於 kmalloc 體系底層依賴的是 slab allocator 體系,所以 kmalloc 體系的創建是在 slab allocator 體系創建之後進行的,關於 slab allocator 體系創建的詳細內容筆者已經在 《從內核源碼看 slab 記憶體池的創建初始化流程》一文的 “12. 內核第一個 slab cache 是如何被創建出來的” 小節介紹過了,在內核初始化記憶體管理子系統的時候,會在 kmem_cache_init 函數中完成 slab alloactor 體系的創建初始化工作,之後緊接著就會創建初始化 kmalloc 體系。
asmlinkage __visible void __init start_kernel(void)
{
........ 省略 .........
// 初始化記憶體管理子系統
mm_init();
........ 省略 .........
}
/*
* Set up kernel memory allocators
*/
static void __init mm_init(void)
{
........ 省略 .........
// 創建並初始化 slab allocator 體系
kmem_cache_init();
........ 省略 .........
}
void __init kmem_cache_init(void)
{
........... 省略 slab allocator 體系的創建初始化過程 ......
/* Now we can use the kmem_cache to allocate kmalloc slabs */
// 初始化上邊提到的 size_index 數組
setup_kmalloc_cache_index_table();
// 創建 kmalloc_info 數組中保存的各個記憶體塊大小對應的 slab cache
// 最終將這些不同尺寸的 slab cache 緩存在 kmalloc_caches 中
create_kmalloc_caches(0);
}
kmalloc 體系的初始化工作核心分為兩個部分:
-
setup_kmalloc_cache_index_table 初始化我們在本文 《2. kmalloc 記憶體池如何選取合適尺寸的記憶體塊》小節中介紹的
size_index
數組,後續 kmalloc 在分配 192 位元組以下的記憶體塊時,內核會利用該數組選取最佳合適尺寸的 slab cache。 -
create_kmalloc_caches 創建初始化上一小節中介紹的 kmalloc_caches 二維數組,這個二維數組正式 kmalloc 體系的核心。內核會利用 kmalloc_caches 直接找到對應的 slab cache 進行記憶體塊的分配和釋放。
4.1 kmalloc_caches 的創建
struct kmem_cache *
kmalloc_caches[NR_KMALLOC_TYPES][KMALLOC_SHIFT_HIGH + 1];
create_kmalloc_caches 函數的主要任務就是創建和初始化這個二維數組,它會為每一個 enum kmalloc_cache_type
分別創建 2^KMALLOC_SHIFT_LOW(8 位元組)
到 2^KMALLOC_SHIFT_HIGH(8K)
範圍內的 slab cache。當然也包括兩個特殊的 slab cache 尺寸,他倆分別是:kmalloc-96,kmalloc-192,剩下的 slab cache 尺寸必須是 2 的次冪。
#define PAGE_SHIFT 12
#define KMALLOC_SHIFT_HIGH (PAGE_SHIFT + 1)
#define KMALLOC_SHIFT_LOW 3
void __init create_kmalloc_caches(slab_flags_t flags)
{
int i, type;
// 初始化二維數組 kmalloc_caches,為每一個 kmalloc_cache_type 類型創建記憶體塊尺寸從 KMALLOC_SHIFT_LOW 到 KMALLOC_SHIFT_HIGH 大小的 kmalloc 記憶體池
for (type = KMALLOC_NORMAL; type <= KMALLOC_RECLAIM; type++) {
// 這裡會從 8B 尺寸的記憶體池開始創建,一直到創建完 8K 尺寸的記憶體池
for (i = KMALLOC_SHIFT_LOW; i <= KMALLOC_SHIFT_HIGH; i++) {
if (!kmalloc_caches[type][i])
// 創建對應尺寸的 kmalloc 記憶體池,其中記憶體塊大小為 2^i 位元組
new_kmalloc_cache(i, type, flags);
// 創建 kmalloc-96 記憶體池管理 96B 尺寸的記憶體塊
// 專門特意創建一個 96位元組尺寸的記憶體池的目的是為了,應對 64B 到 128B 之間的記憶體分配需求,要不然超過 64B 就分配一個 128B 的記憶體塊有點浪費
if (KMALLOC_MIN_SIZE <= 32 && i == 6 &&
!kmalloc_caches[type][1])
new_kmalloc_cache(1, type, flags);
// 創建 kmalloc-192 記憶體池管理 192B 尺寸的記憶體塊
// 這裡專門創建一個 192位元組尺寸的記憶體池.是為了分配 128B 到 192B 之間的記憶體分配需求
// 要不然超過 128B 直接分配一個 256B 的記憶體塊太浪費了
if (KMALLOC_MIN_SIZE <= 64 && i == 7 &&
!kmalloc_caches[type][2])
new_kmalloc_cache(2, type, flags);
}
}
// 當 kmalloc 體系全部創建完畢之後,slab 體系的狀態就變為 up 狀態了
slab_state = UP;
#ifdef CONFIG_ZONE_DMA
// 如果配置了 DMA 記憶體區域,則需要為該區域也創建對應尺寸的記憶體池
for (i = 0; i <= KMALLOC_SHIFT_HIGH; i++) {
struct kmem_cache *s = kmalloc_caches[KMALLOC_NORMAL][i];
if (s) {
unsigned int size = kmalloc_size(i);
const char *n = kmalloc_cache_name("dma-kmalloc", size);
BUG_ON(!n);
kmalloc_caches[KMALLOC_DMA][i] = create_kmalloc_cache(
n, size, SLAB_CACHE_DMA | flags, 0, 0);
}
}
#endif
}
create_kmalloc_caches 函數的邏輯不複雜,比較容易理解,但是這裡有幾個特殊的點,筆者還是要給大家交代清楚。
在第一個 for 迴圈體內的二重迴圈里,當 i = 6
時,表示現在準備要創建 2^6 = 64
位元組尺寸的 slab cache —— kmalloc-64,當創建完 kmalloc-64 時,需要緊接著特殊創建 kmalloc-96,而 kmalloc-96 在 kmalloc_info 數組和 kmalloc_caches 二維數組中的索引均是 1,所以調用 new_kmalloc_cache 創建具體尺寸的 slab cache 時候,第一個參數指的是 slab cache 在 kmalloc_caches 數組中的 index,這裡傳入的是 1。
同樣的道理,在 當 i = 7
時,表示現在準備要創建 2^7 = 128
位元組尺寸的 slab cache —— kmalloc-128,然後緊接著就需要特殊創建 kmalloc-192,而 kmalloc-192 在 kmalloc_caches 二維數組中的索引是 2,所以 new_kmalloc_cache 第一個參數傳入的是 2。
當 KMALLOC_NORMAL 和 KMALLOC_RECLAIM 這兩個類型的 kmalloc 記憶體池建立起來之後,slab_state 就變成了 UP 狀態,表示現在 slab allocator 體系已經建立起來了,可以正常運轉了。
enum slab_state {
DOWN, /* No slab functionality yet */
PARTIAL, /* SLUB: kmem_cache_node available */
UP, /* Slab caches usable but not all extras yet */
FULL /* Everything is working */
};
關於 slab allocator 體系狀態變遷的詳細內容,感興趣的同學可以回看下《從內核源碼看 slab 記憶體池的創建初始化流程》》一文中的 “4. slab allocator 整個體系的狀態變遷” 小節。
最後一步就是創建 KMALLOC_DMA 類型的 kmalloc 記憶體池,這裡會將 KMALLOC_NORMAL 類型的記憶體池復刻一遍,記憶體池中 slab cache 的尺寸還是一樣的,只不過名稱加了 dma-
首碼,還有就是在創建相應 slab cache 的時候指定了 SLAB_CACHE_DMA
,表示 slab cache 中的記憶體頁需要來自於 ZONE_DMA 區域。
4.2 new_kmalloc_cache 創建具體尺寸的 slab cache
上一小節介紹的 create_kmalloc_caches 函數,是根據 kmalloc_info[ ] 數組中的 index 來創建對應尺寸的 slab cache 的。
const struct kmalloc_info_struct kmalloc_info[] __initconst = {
{NULL, 0}, {"kmalloc-96", 96},
{"kmalloc-192", 192}, {"kmalloc-8", 8},
{"kmalloc-16", 16}, {"kmalloc-32", 32},
{"kmalloc-64", 64}, {"kmalloc-128", 128},
{"kmalloc-256", 256}, {"kmalloc-512", 512},
{"kmalloc-1k", 1024}, {"kmalloc-2k", 2048},
{"kmalloc-4k", 4096}, {"kmalloc-8k", 8192},
{"kmalloc-16k", 16384}, {"kmalloc-32k", 32768},
{"kmalloc-64k", 65536}, {"kmalloc-128k", 131072},
{"kmalloc-256k", 262144}, {"kmalloc-512k", 524288},
{"kmalloc-1M", 1048576}, {"kmalloc-2M", 2097152},
{"kmalloc-4M", 4194304}, {"kmalloc-8M", 8388608},
{"kmalloc-16M", 16777216}, {"kmalloc-32M", 33554432},
{"kmalloc-64M", 67108864}
};
而具體尺寸的 slab cache 的創建工作由 new_kmalloc_cache 函數負責。
static void __init
new_kmalloc_cache(int idx, int type, slab_flags_t flags)
該函數的參數含義如下:
-
int idx 表示 kmalloc_info[ ] 數組中的 index,對應 slab cache 的尺寸為 2^index 位元組,96 位元組 和 192 位元組這兩個尺寸除外,它倆在 kmalloc_info[ ] 數組中的 index 分別為 1 和 2。在 create_kmalloc_caches 函數中會特殊指定。該 idx 也表示 slab cache 在 kmalloc_caches 二維數組中的索引。
-
int type 表示對應的 kmalloc 記憶體池類型,指定記憶體來源於哪個物理記憶體區域,取值範圍來自於 enum kmalloc_cache_type 。
-
slab_flags_t flags 指定創建 slab cache 時的標誌位,這裡主要用來指定 slab cache 中的記憶體來源於哪個記憶體區域。
static void __init
new_kmalloc_cache(int idx, int type, slab_flags_t flags)
{
// 參數 idx,即為 kmalloc_info 數組中的下標
// 根據 kmalloc_info 數組中的信息創建對應的 kmalloc 記憶體池
const char *name;
// 為 slab cache 創建名稱
if (type == KMALLOC_RECLAIM) {
flags |= SLAB_RECLAIM_ACCOUNT;
// kmalloc_cache_name 就是做簡單的字元串拼接
name = kmalloc_cache_name("kmalloc-rcl",
kmalloc_info[idx].size);
BUG_ON(!name);
} else {
name = kmalloc_info[idx].name;
}
// 底層調用 __kmem_cache_create 創建 kmalloc_info[idx].size 尺寸的 slab cache
kmalloc_caches[type][idx] = create_kmalloc_cache(name,
kmalloc_info[idx].size, flags, 0,
kmalloc_info[idx].size);
}
如果我們創建的是 KMALLOC_RECLAIM 類型的 kmalloc 記憶體池,那麼其下所管理的各種尺寸的 slab cache 名稱需要加上 kmalloc-rcl
首碼。
最後調用 create_kmalloc_cache 根據 kmalloc_info[idx].size 和 kmalloc_info[idx].name 指定的尺寸和名稱創建 slab cache。關於 slab cache 的詳細創建過程,感興趣的同學可以回看下《從內核源碼看 slab 記憶體池的創建初始化流程》。
5. kmalloc 記憶體池如何進行記憶體的分配與回收
現在 kmalloc 記憶體池的整體架構我們已經創建出來了,內核後續會基於這個架構從 kmalloc 記憶體池中申請記憶體塊,下麵我們一起來看下記憶體塊分配的過程:
static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
return __kmalloc(size, flags);
}
#define KMALLOC_MAX_CACHE_SIZE (1UL << KMALLOC_SHIFT_HIGH)
#define PAGE_SHIFT 12
#define KMALLOC_SHIFT_HIGH (PAGE_SHIFT + 1)
void *__kmalloc(size_t size, gfp_t flags)
{
struct kmem_cache *s;
void *ret;
// KMALLOC_MAX_CACHE_SIZE 規定 kmalloc 記憶體池所能管理的記憶體塊最大尺寸,在 slub 實現中是 2頁 大小
// 如果使用 kmalloc 申請超過 2頁 大小的記憶體,則直接走伙伴系統
if (unlikely(size > KMALLOC_MAX_CACHE_SIZE))
// 底層調用 alloc_pages 向伙伴系統申請超過 2頁 的記憶體塊
return kmalloc_large(size, flags);
// 根據申請記憶體塊的尺寸 size,在 kmalloc_caches 緩存中選擇合適尺寸的記憶體池
s = kmalloc_slab(size, flags);
// 向選取的 slab cache 申請記憶體塊
ret = slab_alloc(s, flags, _RET_IP_);
return ret;
}
當內核向 kmalloc 記憶體池申請的記憶體塊尺寸 size 超過了 KMALLOC_MAX_CACHE_SIZE
的限制時,內核會繞過 kmalloc 記憶體池直接到伙伴系統中去申請記憶體頁。
kmalloc_large 函數裡邊會調用 alloc_pages,隨後會進入伙伴系統中申請記憶體塊。關於 alloc_pages 函數的詳細內容,感興趣的同學可以回看下筆者之前的文章 《深入理解 Linux 物理記憶體分配全鏈路實現》。
KMALLOC_MAX_CACHE_SIZE 在 slub 的實現中,配置為 8K 大小,也就是說在 slub 的實現中,向 kmalloc 記憶體池申請的記憶體塊超過了 8K 就會直接走伙伴系統。
如果申請的記憶體塊尺寸 size 低於 8k,那麼內核就會從 kmalloc_caches 中選取一個最佳尺寸的 slab cache,然後通過這個 slab cache 進行記憶體塊的分配。關於 slab cache 記憶體分配的詳細過程,感興趣的同學可以回看下 《深入理解 slab cache 記憶體分配全鏈路實現》。
從這裡可以看出,kmalloc 記憶體池在 slub 的實現中,最大能申請的記憶體塊尺寸為 8K,也就是兩個物理記憶體頁大小。
5.1 如何從 kmalloc_caches 中選取最佳尺寸的 slab cache
struct kmem_cache *kmalloc_slab(size_t size, gfp_t flags)
{
unsigned int index;
// 如果申請的記憶體塊 size 在 192 位元組以下,則通過 size_index 數組定位 kmalloc_caches 緩存索引
// 從而獲取到最佳合適尺寸的記憶體池 slab cache
if (size <= 192) {
if (!size)
return ZERO_SIZE_PTR;
// 根據申請的記憶體塊 size,定義 size_index 數組索引,從而獲取 kmalloc_caches 緩存的 index
index = size_index[size_index_elem(size)];
} else {
// 如果申請的記憶體塊 size 超過 192 位元組,則通過 fls 定位 kmalloc_caches 緩存的 index
// fls 可以獲取參數的最高有效 bit 的位數,比如 fls(0)=0,fls(1)=1,fls(4) = 3
index = fls(size - 1);
}
// 根據 kmalloc_type 以及 index 獲取最佳尺寸的記憶體池 slab cache
return kmalloc_caches[kmalloc_type(flags)][index];
}
kmalloc 記憶體池分配記憶體塊的核心就是需要在 kmalloc_caches 二維數組中查找到最佳合適尺寸的 slab cache,所以目前擺在我們面前最緊迫的一個問題就是如何找到這個最佳的 slab cache 在 kmalloc_caches 中的索引 index。
當申請記憶體塊的尺寸在 192 位元組以下的時候,通過本文 《2. kmalloc 記憶體池如何選取合適尺寸的記憶體塊》小節的介紹我們知道,內核會通過 size_index 數組來定位 kmalloc_caches 中 slab cache 的 index。
size_index 數組中存放的值正是 kmalloc_caches 中的索引 index
static u8 size_index[24] __ro_after_init = {
3, /* 8 */
4, /* 16 */
5, /* 24 */
5, /* 32 */
6, /* 40 */
6, /* 48 */
6, /* 56 */
6, /* 64 */
1, /* 72 */
1, /* 80 */
1, /* 88 */
1, /* 96 */
7, /* 104 */
7, /* 112 */
7, /* 120 */
7, /* 128 */
2, /* 136 */
2, /* 144 */
2, /* 152 */
2, /* 160 */
2, /* 168 */
2, /* 176 */
2, /* 184 */
2 /* 192 */
};
如果我們能通過申請記憶體塊的大小 size,定位到 size_index 數組本身的索引 sizeindex,那麼我們就可以通過 size_index[sizeindex] 找到 kmalloc_caches 中的最佳 slab cache 了。
在內核中通過 size_index_elem 函數來根據申請記憶體塊的尺寸 bytes,定位 size_index 數組的索引 sizeindex。
static inline unsigned int size_index_elem(unsigned int bytes)
{
// sizeindex
return (bytes - 1) / 8;
}
然後根據 size_index[sizeindex] 的值以及 gfp_t flags 中指定的 kmalloc_type 從 kmalloc_caches 中直接查找出最佳合適尺寸的 slab cahe 出來。
當申請記憶體塊的尺寸在 192 位元組以上的時候,內核直接通過 fls(size - 1)
來定位 kmalloc_caches 數組中的索引 index。
目前,我們已經清楚了 slab cache 在 kmalloc_caches 數組中二維索引 index 的獲取邏輯,那麼一維索引也就是 kmalloc 記憶體池中的記憶體來源類型我們該如何獲取呢?
struct kmem_cache *
kmalloc_caches[NR_KMALLOC_TYPES][KMALLOC_SHIFT_HIGH + 1];
一維索引的獲取邏輯內核將它封裝在 kmalloc_type 函數中,在這裡會將 kmalloc 介面函數中 gfp_t flags
掩碼中指定的物理記憶體區域轉換為 enum kmalloc_cache_type
。
static __always_inline void *kmalloc(size_t size, gfp_t flags)
下麵我們就來一起看下這個轉換過程~~~
5.2 kmalloc_cache_type 的選擇
這裡的邏輯比較簡單,核心就是以下三個規則:
-
如果 gfp_t flags 沒有特殊指定,那麼在預設情況下,內核向 kmalloc 記憶體池申請的記憶體均來自於 ZONE_NORMAL 物理記憶體區域。
-
如果 gfp_t flags 明確指定了 __GFP_DMA,則內核向 kmalloc 記憶體池申請的記憶體均來自於 ZONE_DMA 物理記憶體區域。
-
如果 gfp_t flags 明確指定了 __GFP_RECLAIMABLE,則內核向 kmalloc 記憶體池申請的記憶體均是可以被回收的。
static __always_inline enum kmalloc_cache_type kmalloc_type(gfp_t flags)
{
#ifdef CONFIG_ZONE_DMA
// 通常情況下 kmalloc 記憶體池中的記憶體都來源於 NORMAL 直接映射區
// 如果沒有特殊設定,則從 NORMAL 直接映射區里分配
if (likely((flags & (__GFP_DMA | __GFP_RECLAIMABLE)) == 0))
return KMALLOC_NORMAL;
// DMA 區域中的記憶體是非常寶貴的,如果明確指定需要從 DMA 區域中分配記憶體
// 則選取 DMA 區域中的 kmalloc 記憶體池
return flags & __GFP_DMA ? KMALLOC_DMA : KMALLOC_RECLAIM;
#else
// 明確指定了從 RECLAIMABLE 區域中獲取記憶體,則選取 RECLAIMABLE 區域中 kmalloc 記憶體池,該區域中的記憶體頁是可以被回收的,比如:文件頁緩存
return flags & __GFP_RECLAIMABLE ? KMALLOC_RECLAIM : KMALLOC_NORMAL;
#endif
}
5.3 kmalloc 記憶體池回收記憶體
內核提供了 kfree 函數來釋放由 kmalloc 記憶體池分配的記憶體塊,參數 x 表示釋放記憶體塊的虛擬記憶體地址。
void kfree(const void *x)
{
struct page *page;
// x 為要釋放的記憶體塊的虛擬記憶體地址
void *object = (void *)x;
// 通過虛擬記憶體地址找到記憶體塊所在的 page
page = virt_to_head_page(x);
// 如果 page 不在 slab cache 的管理體系中,則直接釋放回伙伴系統
if (unlikely(!PageSlab(page))) {
__free_pages(page, order);
return;
}
// 將記憶體塊釋放回其所在的 slub 中
slab_free(page->slab_cache, page, object, NULL, 1, _RET_IP_);
}
首先內核需要通過 virt_to_head_page 函數,根據記憶體塊的虛擬記憶體地址 x 找到其所在的物理記憶體頁 page。
通過 PageSlab(page)
檢查釋放記憶體塊所在物理記憶體頁 struct page 結構中的 flag 屬性是否設置了 PG_slab 標識。
struct page {
unsigned long flags;
}
關於記憶體頁 page 中 flag 屬性的詳細內容介紹,感興趣的讀者可以回看下《深入理解 Linux 物理記憶體管理》 一文中的 “6.3 物理記憶體頁屬性和狀態的標誌位 flag” 小節。
如果 page->flag 沒有設置 PG_slab 標識,說明該物理記憶體頁沒有被 slab cache 管理,說明當初調用 kmalloc 分配的時候直接走的是伙伴系統,並沒有從 kmalloc 記憶體池中分配。
那麼在這種情況下,可以直接調用 __free_pages 將物理記憶體頁釋放回伙伴系統中。關於伙伴系統回收記憶體的詳細內容,感興趣的讀者可以回看下 《深度剖析 Linux 伙伴系統的設計與實現》 一文中的 “7. 記憶體釋放源碼實現” 小節。
如果 page->flag 設置了 PG_slab 標識,說明記憶體塊分配走的是 kmalloc 記憶體池,這種情況下,就需要將記憶體塊釋放回 kmalloc 記憶體池中相應的 slab cache 中。
struct page {
struct kmem_cache *slab_cache;
}
我們可以通過 struct page 結構的 slab_cache 屬性,獲取 page 所屬的 slab cache。近而通過內核提供的 kmem_cache_free 介面,將記憶體塊釋放回對應的 slab cache 中。
void kmem_cache_free(struct kmem_cache *s, void *x)
關於 slab cache 回收記憶體塊的詳細內容,感興趣的讀者可以回看下 《深度解析 slab 記憶體池回收記憶體以及銷毀全流程》 一文中的內容。
總結
整個 kmalloc 通用記憶體池體系的核心是圍繞著 kmalloc_caches 這個二維數組召開的。
struct kmem_cache *
kmalloc_caches[NR_KMALLOC_TYPES][KMALLOC_SHIFT_HIGH + 1];
其中一維數組中定義的是 kmalloc 記憶體池中的記憶體來源,在內核中使用 enum kmalloc_cache_type
來表示:
enum kmalloc_cache_type {
// 規定 kmalloc 記憶體池的記憶體需要在 NORMAL 直接映射區分配
KMALLOC_NORMAL = 0,
// 規定 kmalloc 記憶體池中的記憶體是可以回收的,比如文件頁緩存,匿名頁
KMALLOC_RECLAIM,
#ifdef CONFIG_ZONE_DMA
// kmalloc 記憶體池中的記憶體用於 DMA,需要在 DMA 區域分配
KMALLOC_DMA,
#endif
NR_KMALLOC_TYPES
};
我們可以通過 kmalloc_type 函數從用戶指定的 gfp_t flags 標記位中提取出 kmalloc_cache_type。
static __always_inline enum kmalloc_cache_type kmalloc_type(gfp_t flags)
通常情況下 kmalloc 記憶體池中的記憶體都來源於 NORMAL 直接映射區。
這樣我們就定位到了 kmalloc_caches 中的一維數組,二維數組中定義的是 kmalloc 記憶體池所支持的記憶體塊尺寸的範圍,二維數組中的 index 表示的含義比較巧妙,它表示了對應 slab cache 中所管理的記憶體塊尺寸的分配階(2 的次冪),96 和 192 這兩個記憶體塊尺寸除外,它們的 index 分別是 1 和 2,單獨特殊指定。
kmalloc 記憶體池所能支持的記憶體塊尺寸範圍定義在 kmalloc_info 數組中:
const struct kmalloc_info_struct kmalloc_info[] __initconst = {
{NULL, 0}, {"kmalloc-96", 96},
{"kmalloc-192", 192}, {"kmalloc-8", 8},
{"kmalloc-16", 16}, {"kmalloc-32", 32},
{"kmalloc-64", 64}, {"kmalloc-128", 128},
{"kmalloc-256", 256}, {"kmalloc-512", 512},
{"kmalloc-1k", 1024}, {"kmalloc-2k", 2048},
{"kmalloc-4k", 4096}, {"kmalloc-8k", 8192},
{"kmalloc-16k", 16384}, {"kmalloc-32k", 32768},
{"kmalloc-64k", 65536}, {"kmalloc-128k", 131072},
{"kmalloc-256k", 262144}, {"kmalloc-512k", 524288},
{"kmalloc-1M", 1048576}, {"kmalloc-2M", 2097152},
{"kmalloc-4M", 4194304}, {"kmalloc-8M", 8388608},
{"kmalloc-16M", 16777216}, {"kmalloc-32M", 33554432},
{"kmalloc-64M", 67108864}
};
但實際上 kmalloc 體系所支持的記憶體塊尺寸與 slab allocator 體系的實現有關,在 slub 實現中,kmalloc 所能支持的最小記憶體塊為 8 位元組,所能支持的最大記憶體塊為 8K,超過了 8K 就會直接到伙伴系統中去申請。
#ifdef CONFIG_SLUB
// slub 最大支持分配 2頁 大小的對象,對應的 kmalloc 記憶體池中記憶體塊尺寸最大就是 2頁
// 超過 2頁 大小的記憶體塊直接向伙伴系統申請
#define KMALLOC_SHIFT_HIGH (PAGE_SHIFT + 1)
#define KMALLOC_SHIFT_LOW 3
#define PAGE_SHIFT 12
當申請的記憶體塊尺寸在 192 位元組以下時,我們可以通過 size_index[] 數組中定義的規則,找到 kmalloc_caches 二維數組中的 index,從而定位到最佳合適尺寸的 slab cache。
當申請記憶體塊的尺寸在 192 位元組以上的時候,內核直接通過 fls(size - 1) 來定位 kmalloc_caches 數組中的索引 index。
當我們定位到具體的 slab cache 之後,剩下的事情就好辦了,直接從該 slab cache 中分配指定大小的記憶體塊,在使用完之後通過 kfree 函數在釋放回對應的 slab cache 中。
好了,關於 kmalloc 體系的全部內容到這裡就全部介紹完了,感謝大家的收看,我們下篇文章見~~~