在上篇文章 《細節拉滿,80 張圖帶你一步一步推演 slab 記憶體池的設計與實現 》中,筆者從 slab cache 的總體架構演進角度以及 slab cache 的運行原理角度為大家勾勒出了 slab cache 的總體架構視圖,基於這個視圖詳細闡述了 slab cache 的記憶體分配以及釋放原理 ...
在上篇文章 《細節拉滿,80 張圖帶你一步一步推演 slab 記憶體池的設計與實現
》中,筆者從 slab cache 的總體架構演進角度以及 slab cache 的運行原理角度為大家勾勒出了 slab cache 的總體架構視圖,基於這個視圖詳細闡述了 slab cache 的記憶體分配以及釋放原理。
slab cache 機制確實比較複雜,涉及到的場景又很多,大家讀到這裡,我想肯定會好奇或者懷疑筆者在上篇文章中所論述的那些原理的正確性,畢竟 talk is cheap ,所以為了讓大家看著安心,理解起來放心,從本文開始,我們將正式進入 show you the code 的階段。筆者會基於內核 5.4 版本,詳細為大家剖析 slab cache 在內核中的源碼實現。
在上篇文章 《5. 從一個簡單的記憶體頁開始聊 slab》和 《6. slab 的總體架構設計》小節中,筆者帶大家從一個最簡單的物理記憶體頁開始,一步一步演進 slab cache 的架構,最終得到了一副 slab cache 完整的架構圖:
在本文的內容中,筆者會帶大家到內核源碼實現中,來看一下 slab cache 在內核中是如何被一步一步創建出來的,以及內核是如何安排 slab 對象在記憶體中的佈局的。
我們先以內核創建 slab cache 的介面函數 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 *))
{
return kmem_cache_create_usercopy(name, size, align, flags, 0, 0,
ctor);
}
kmem_cache_create 介面中的參數,是由用戶指定的關於 slab cache 的一些核心屬性,這些屬性值與我們在前文《細節拉滿,80 張圖帶你一步一步推演 slab 記憶體池的設計與實現》 的《6.1 slab 的基礎信息管理》小節中介紹 struct kmem_cache 結構的相應屬性一一對應,在創建 slab cache 的過程中,內核會將 kmem_cache_create 介面中參數指定的值一一賦值到 struct kmem_cache 結構中。
struct kmem_cache {
// slab cache 的名稱, 也就是在 slabinfo 命令中 name 那一列
const char *name;
// 對應參數 size,指 slab 中對象的實際大小,不包含填充的位元組數
unsigned int object_size;/* The size of an object without metadata */
// 對象按照指定的 align 進行對齊
unsigned int align;
// slab cache 的管理標誌位,用於設置 slab 的一些特性
slab_flags_t flags;
// 池化對象的構造函數,用於創建 slab 對象池中的對象
void (*ctor)(void *);
}
slab cache 的整個創建過程其實是封裝在 kmem_cache_create_usercopy 函數中,kmem_cache_create 直接調用了該函數,並將創建參數透傳過去。
struct kmem_cache *
kmem_cache_create_usercopy(const char *name,
unsigned int size, unsigned int align,
slab_flags_t flags,
unsigned int useroffset, unsigned int usersize,
void (*ctor)(void *))
內核提供 kmem_cache_create_usercopy 函數的目的其實是為了防止 slab cache 中管理的內核核心對象被泄露,通過 useroffset 和 usersize 兩個變數來指定內核對象記憶體佈局區域中 useroffset 到 usersize 的這段記憶體區域可以被覆制到用戶空間中,其他區域則不可以。
在 Linux 內核初始化的過程中會提前為內核核心對象創建好對應的 slab cache,比如:在內核初始化函數 start_kernel 中調用 fork_init 函數為 struct task_struct 創建其所屬的 slab cache —— task_struct_cachep。
在 fork_init 中就調用了 kmem_cache_create_usercopy 函數來創建 task_struct_cachep,同時指定 task_struct 對象中 useroffset 到 usersize 這段記憶體區域可以被覆制到用戶空間。例如:通過 ptrace 系統調用訪問進程的 task_struct 結構時,只能訪問 task_struct 對象 useroffset 到 usersize 的這段區域。
void __init fork_init(void)
{
......... 省略 ..........
unsigned long useroffset, usersize;
/* create a slab on which task_structs can be allocated */
task_struct_whitelist(&useroffset, &usersize);
task_struct_cachep = kmem_cache_create_usercopy("task_struct",
arch_task_struct_size, align,
SLAB_PANIC|SLAB_ACCOUNT,
useroffset, usersize, NULL);
......... 省略 ..........
}
struct kmem_cache *
kmem_cache_create_usercopy(const char *name,
unsigned int size, unsigned int align,
slab_flags_t flags,
unsigned int useroffset, unsigned int usersize,
void (*ctor)(void *))
{
struct kmem_cache *s = NULL;
const char *cache_name;
int err;
// 獲取 cpu_hotplug_lock,防止 cpu 熱插拔改變 online cpu map
get_online_cpus();
// 獲取 mem_hotplug_lock,防止訪問記憶體的時候進行記憶體熱插拔
get_online_mems();
// memory cgroup 相關,獲取 memcg_cache_ids_sem 讀寫信號量
// 防止 memcg_nr_cache_ids (caches array 大小)被修改
memcg_get_cache_ids();
// 獲取 slab cache 鏈表的全局互斥鎖
mutex_lock(&slab_mutex);
// 入參檢查,校驗 name 和 size 的有效性,防止創建過程在中斷上下文中進行
err = kmem_cache_sanity_check(name, size);
if (err) {
goto out_unlock;
}
// 檢查有效的 slab flags 標記位,如果傳入的 flag 是無效的,則拒絕本次創建請求
if (flags & ~SLAB_FLAGS_PERMITTED) {
err = -EINVAL;
goto out_unlock;
}
// 設置創建 slab cache 時用到的一些標誌位
flags &= CACHE_CREATE_MASK;
// 校驗 useroffset 和 usersize 的有效性
if (WARN_ON(!usersize && useroffset) ||
WARN_ON(size < usersize || size - usersize < useroffset))
usersize = useroffset = 0;
if (!usersize)
// 在全局 slab cache 鏈表中查找與當前創建參數相匹配的 kmem_cache
// 如果有,就不需要創建新的了,直接和已有的 slab cache 合併
// 並且在 sys 文件系統中使用指定的 name 作為已有 slab cache 的別名
s = __kmem_cache_alias(name, size, align, flags, ctor);
if (s)
goto out_unlock;
// 在內核中為指定的 name 生成字元串常量並分配記憶體
// 這裡的 cache_name 就是將要創建的 slab cache 名稱,用於在 /proc/slabinfo 中顯示
cache_name = kstrdup_const(name, GFP_KERNEL);
if (!cache_name) {
err = -ENOMEM;
goto out_unlock;
}
// 按照我們指定的參數,創建新的 slab cache
s = create_cache(cache_name, size,
calculate_alignment(flags, align, size),
flags, useroffset, usersize, ctor, NULL, NULL);
if (IS_ERR(s)) {
err = PTR_ERR(s);
kfree_const(cache_name);
}
out_unlock:
// 走到這裡表示創建 slab cache 失敗,釋放相關的自旋鎖和信號量
mutex_unlock(&slab_mutex);
memcg_put_cache_ids();
put_online_mems();
put_online_cpus();
if (err) {
if (flags & SLAB_PANIC)
panic("kmem_cache_create: Failed to create slab '%s'. Error %d\n",
name, err);
else {
pr_warn("kmem_cache_create(%s) failed with error %d\n",
name, err);
dump_stack();
}
return NULL;
}
return s;
}
在創建 slab cache 的開始,內核為了保證整個創建過程是併發安全的,所以需要先獲取一系列的鎖,比如:
- 獲取 cpu_hotplug_lock,mem_hotplug_lock 來防止在創建 slab cache 的過程中 cpu 或者記憶體進行熱插拔。
- 防止 memory group 相關的 caches array 被修改,cgroup 相關的不是本文重點,這裡簡單瞭解一下即可。
- 內核中使用一個全局的雙向鏈表來串聯起系統中所有的 slab cache,這裡需要獲取全局鏈表 list 的鎖,防止併發對 list 進行修改。
在確保 slab cache 的整個創建過程併發安全之後,內核會首先校驗 kmem_cache_create 介面函數傳遞進來的那些創建參數的合法有效性。
比如,kmem_cache_sanity_check 函數中會確保 slab cache 的創建過程不能在中斷上下文中進行,如果進程所處的上下文為中斷上下文,那麼內核就會返回 -EINVAL
錯誤停止 slab cache 的創建。因為中斷處理程式是不會被內核重新調度的,這就導致處於中斷上下文的操作必須是原子的,不能睡眠,不能阻塞,更不能持有鎖等同步資源。而 slab cache 的創建並不是原子的,內核需要確保整個創建過程不能在中斷上下文中進行。
除此之外 kmem_cache_sanity_check 函數還需要校驗用戶傳入的 name 和 對象大小 object size 的有效性,確保 object size 在有效範圍: 8 位元組到 4M 之間。
#define MAX_ORDER 11
#define PAGE_SHIFT 12
// 定義在 /include/linux/slab.h 文件
#ifdef CONFIG_SLUB
#define KMALLOC_SHIFT_MAX (MAX_ORDER + PAGE_SHIFT - 1)
/* Maximum allocatable size */
#define KMALLOC_MAX_SIZE (1UL << KMALLOC_SHIFT_MAX)
static int kmem_cache_sanity_check(const char *name, unsigned int size)
{
// 1: 傳入 slab cache 的名稱不能為空
// 2: 創建 slab cache 的過程不能處在中斷上下文中
// 3: 傳入的對象大小 size 需要在 8 位元組到 KMALLOC_MAX_SIZE = 4M 之間
if (!name || in_interrupt() || size < sizeof(void *) ||
size > KMALLOC_MAX_SIZE) {
pr_err("kmem_cache_create(%s) integrity check failed\n", name);
return -EINVAL;
}
WARN_ON(strchr(name, ' ')); /* It confuses parsers */
return 0;
}
最後內核會校驗傳入的 slab cache 管理標誌位 slab_flags_t 的合法性,確保 slab_flags_t 在內核規定的有效標誌集合中:
/* Common flags permitted for kmem_cache_create */
#define SLAB_FLAGS_PERMITTED (SLAB_CORE_FLAGS | \
SLAB_RED_ZONE | \
SLAB_POISON | \
SLAB_STORE_USER | \
SLAB_TRACE | \
SLAB_CONSISTENCY_CHECKS | \
SLAB_MEM_SPREAD | \
SLAB_NOLEAKTRACE | \
SLAB_RECLAIM_ACCOUNT | \
SLAB_TEMPORARY | \
SLAB_ACCOUNT)
隨後 flags &= CACHE_CREATE_MASK
初始化 slab_flags_t 標誌位:
/* Common flags available with current configuration */
#define CACHE_CREATE_MASK (SLAB_CORE_FLAGS | SLAB_DEBUG_FLAGS | SLAB_CACHE_FLAGS)
在校驗完各項創建參數的有效性之後,按照常理來說就應該進入 slab cache 的創建流程了,但是現在還沒到創建的時候,內核的理念是盡最大可能復用系統中已有的 slab cache。
在 __kmem_cache_alias 函數中,內核會遍歷系統中 slab cache 的全局鏈表 list,試圖在系統現有 slab cache 中查找到一個各項核心參數與我們指定的創建參數貼近的 slab cache。比如,系統中存在一個 slab cache 它的各項核心參數,object size,align,slab_flags_t 和我們指定的創建參數非常貼近。
這樣一來內核就不需要重覆創建新的 slab cache 了,直接復用原有的 slab cache 即可,將我們指定的 name 作為原有 slab cache 的別名。
如果找不到這樣一個可以被覆用的 slab cache,那麼內核就會調用 create_cache 開始創建 slab cache 流程。
以上是 slab cache 創建的總體框架流程,接下來,我們來詳細看下創建流程中涉及到的幾個核心函數。
1. __kmem_cache_alias
__kmem_cache_alias 函數的核心是在 find_mergeable 方法中,內核在 find_mergeable 方法裡邊會遍歷 slab cache 的全局鏈表 list,查找與當前創建參數貼近可以被覆用的 slab cache。
一個可以被覆用的 slab cache 需要滿足以下四個條件:
-
指定的 slab_flags_t 相同。
-
指定對象的 object size 要小於等於已有 slab cache 中的對象 size (kmem_cache->size)。
-
如果指定對象的 object size 與已有 kmem_cache->size 不相同,那麼它們之間的差值需要再一個 word size 之內。
-
已有 slab cache 中的 slab 對象對齊 align (kmem_cache->align)要大於等於指定的 align 並且可以整除 align 。
struct kmem_cache *
__kmem_cache_alias(const char *name, unsigned int size, unsigned int align,
slab_flags_t flags, void (*ctor)(void *))
{
struct kmem_cache *s, *c;
// 在全局 slab cache 鏈表中查找與當前創建參數相匹配的 slab cache
// 如果在全局查找到一個 slab cache,它的核心參數和我們指定的創建參數很貼近
// 那麼就沒必要再創建新的 slab cache了,復用已有的 slab cache
s = find_mergeable(size, align, flags, name, ctor);
if (s) {
// 如果存在可復用的 kmem_cache,則將它的引用計數 + 1
s->refcount++;
// 採用較大的值,更新已有的 kmem_cache 相關的元數據
s->object_size = max(s->object_size, size);
s->inuse = max(s->inuse, ALIGN(size, sizeof(void *)));
// 遍歷 mem cgroup 中的 cache array,更新對應的元數據
// cgroup 相關,這裡簡單瞭解也可直接忽略
for_each_memcg_cache(c, s) {
c->object_size = s->object_size;
c->inuse = max(c->inuse, ALIGN(size, sizeof(void *)));
}
// 由於這裡我們會復用已有的 kmem_cache 並不會創建新的,而且我們指定的 kmem_cache 名稱是 name。
// 為了看起來像是創建了一個名稱為 name 的新 kmem_cache,所以要給被覆用的 kmem_cache 起一個別名,這個別名就是我們指定的 name
// 在 sys 文件系統中使用我們指定的 name 為被覆用 kmem_cache 創建別名
// 這樣一來就會在 sys 文件系統中出現一個這樣的目錄 /sys/kernel/slab/name ,該目錄下的文件包含了對應 slab cache 運行時的詳細信息
if (sysfs_slab_alias(s, name)) {
s->refcount--;
s = NULL;
}
}
return s;
}
如果通過 find_mergeable 在現有系統中所有 slab cache 中找到了一個可以復用的 slab cache,那麼就不需要在創建新的了,直接返回已有的 slab cache 就可以了。
但是在返回之前,需要更新一下已有 slab cache 結構 kmem_cache 中的相關信息:
struct kmem_cache {
// slab cache 的引用計數,為 0 時就可以銷毀並釋放記憶體回伙伴系統重
int refcount;
// slab 中對象的實際大小,不包含填充的位元組數
unsigned int object_size;/* The size of an object without metadata */
// 對象的 object_size 按照 word 字長對齊之後的大小
unsigned int inuse;
}
-
增加原有 slab cache 的引用計數 refcount++。
-
slab cache 中的 object size 更新為我們在創建參數中指定的 object size 與原有 object size 之間的最大值。
-
slab cache 中的 inuse 也是更新為原有 kmem_cache->inuse 與我們指定的對象 object size 與 word size 對齊之後的最大值。
最後調用 sysfs_slab_alias 在 sys 文件系統中創建一個這樣的目錄 /sys/kernel/slab/name
,name 就是 kmem_cache_create 介面函數傳遞過來的參數,表示要創建的 slab cache 名稱。
系統中的所有 slab cache 都會在 sys 文件系統中有一個專門的目錄:/sys/kernel/slab/<cachename>
,該目錄下的所有文件都是 read only 的,每一個文件代表 slab cache 的一項運行時信息,比如:
/sys/kernel/slab/<cachename>/align
文件標識該 slab cache 中的 slab 對象的對齊 align/sys/kernel/slab/<cachename>/alloc_fastpath
文件記錄該 slab cache 在快速路徑下分配的對象個數/sys/kernel/slab/<cachename>/alloc_from_partial
文件記錄該 slab cache 從本地 cpu 緩存 partial 鏈表中分配的對象次數/sys/kernel/slab/<cachename>/alloc_slab
文件記錄該 slab cache 從伙伴系統中申請新 slab 的次數/sys/kernel/slab/<cachename>/cpu_slabs
文件記錄該 slab cache 的本地 cpu 緩存中緩存的 slab 個數/sys/kernel/slab/<cachename>/partial
文件記錄該 slab cache 在每個 NUMA 節點緩存 partial 鏈表中的 slab 個數/sys/kernel/slab/<cachename>/objs_per_slab
文件記錄該 slab cache 中管理的 slab 可以容納多少個對象。
該目錄下還有很多文件筆者就不一一列舉了,但是我們可以看到 /sys/kernel/slab/<cachename>
目錄下的文件描述了對應 slab cache 非常詳細的運行信息。前邊我們介紹的 cat /proc/slabinfo
命名輸出的信息就來源於 /sys/kernel/slab/<cachename>
目錄下的各個文件。
由於我們當前並沒有真正創建一個新的 slab cache,而是復用系統中已有的 slab cache,但是內核需要讓用戶感覺上已經按照我們指定的創建參數創建了一個新的 slab cache,所以需要為我們要創建的 slab cache 也單獨在 sys 文件系統中創建一個 /sys/kernel/slab/name
目錄,但是該目錄下的文件需要軟鏈接到原有 slab cache 在 sys 文件系統對應目錄下的文件。
這就相當於給原有 slab cache 起了一個別名,這個別名就是我們指定的 name,但是 /sys/kernel/slab/name
目錄下的文件還是用的原有 slab cache 的。
我們可以通過 /sys/kernel/slab/<cachename>/aliases
文件查看該 slab cache 的所有別名個數,也就是說有多少個 slab cache 復用了該 slab cache 。
1.1 find_mergeable 查找可被覆用的 slab cache
struct kmem_cache *find_mergeable(unsigned int size, unsigned int align,
slab_flags_t flags, const char *name, void (*ctor)(void *))
{
struct kmem_cache *s;
// 與 word size 進行對齊
size = ALIGN(size, sizeof(void *));
// 根據我們指定的對齊參數 align 並結合 CPU cache line 大小,計算出一個合適的對齊參數
align = calculate_alignment(flags, align, size);
// 對象 size 重新按照 align 進行對齊
size = ALIGN(size, align);
// 如果 flag 設置的是不允許合併,則停止
if (flags & SLAB_NEVER_MERGE)
return NULL;
// 開始遍歷內核中已有的 slab cache,尋找可以合併的 slab cache
list_for_each_entry_reverse(s, &slab_root_caches, root_caches_node) {
if (slab_unmergeable(s))
continue;
// 指定對象 size 不能超過已有 slab cache 中的對象 size
if (size > s->size)
continue;
// 校驗指定的 flag 是否與已有 slab cache 中的 flag 一致
if ((flags & SLAB_MERGE_SAME) != (s->flags & SLAB_MERGE_SAME))
continue;
// 兩者的 size 相差在一個 word size 之內
if (s->size - size >= sizeof(void *))
continue;
// 已有 slab cache 中對象的對齊 align 要大於等於指定的 align並且可以整除 align。
if (IS_ENABLED(CONFIG_SLAB) && align &&
(align > s->align || s->align % align))
continue;
// 查找到可以合併的已有 slab cache,不需要再創建新的 slab cache 了
return s;
}
return NULL;
}
一個可以被覆用的 slab cache 需要滿足以下四個條件:
-
指定的 slab_flags_t 相同。
-
指定對象的 object size 要小於等於已有 slab cache 中的對象 size (kmem_cache->size)。
-
如果指定對象的 object size 與已有 kmem_cache->size 不相同,那麼它們之間的差值需要再一個 word size 之內。
-
已有 slab cache 中的 slab 對象對齊 align (kmem_cache->align)要大於等於指定的 align 並且可以整除 align 。
1.2 calculate_alignment 綜合計算出一個合理的對齊 align
事實上,內核並不會完全按照我們指定的 align 進行記憶體對齊,而是會綜合考慮 cpu 硬體 cache line 的大小,以及 word size 計算出一個合理的 align 值。
內核在對 slab 對象進行記憶體佈局的時候,會按照這個最終的 align 進行記憶體對齊。
static unsigned int calculate_alignment(slab_flags_t flags,
unsigned int align, unsigned int size)
{
// SLAB_HWCACHE_ALIGN 表示需要按照硬體 cache line 對齊
if (flags & SLAB_HWCACHE_ALIGN) {
unsigned int ralign;
// 獲取 cache line 大小 通常為 64 位元組
ralign = cache_line_size();
// 根據指定對齊參數 align ,對象 object size 以及 cache line 大小
// 綜合計算出一個合適的對齊參數 ralign 出來
while (size <= ralign / 2)
ralign /= 2;
align = max(align, ralign);
}
// ARCH_SLAB_MINALIGN 為 slab 設置的最小對齊參數, 8 位元組大小,align 不能小於該值
if (align < ARCH_SLAB_MINALIGN)
align = ARCH_SLAB_MINALIGN;
// 與 word size 進行對齊
return ALIGN(align, sizeof(void *));
}
// 定義在文件:/include/linux/slab.h
#define ARCH_SLAB_MINALIGN __alignof__(unsigned long long)
2. create_cache 開始正式創建 slab cache
在前文《細節拉滿,80 張圖帶你一步一步推演 slab 記憶體池的設計與實現》 中的 《6.2 slab 的組織架構》小節中,為大家介紹的 slab cache 的整體架構就是在 create_cache 函數中搭建完成的。
create_cache 函數的主要任務就是為 slab cache 創建它的內核數據結構 struct kmem_cache,併為其填充我們在前文 《6.1 slab 的基礎信息管理》小節中介紹的關於 struct kmem_cache 相關的屬性。
隨後內核會為其創建 slab cache 的本地 cpu 結構 kmem_cache_cpu,每個 cpu 對應一個這樣的緩存結構。
struct kmem_cache {
// 每個 cpu 擁有一個本地緩存,用於無鎖化快速分配釋放對象
struct kmem_cache_cpu __percpu *cpu_slab;
}
最後為 slab cache 創建 NUMA 節點緩存結構 kmem_cache_node,每個 NUMA 節點對應一個。
struct kmem_cache {
// slab cache 中 numa node 中的緩存,每個 node 一個
struct kmem_cache_node *node[MAX_NUMNODES];
}
當 slab cache 的整個骨架被創建出來之後,內核會為其在 sys 文件系統中創建 /sys/kernel/slab/name
目錄節點,用於詳細記錄該 slab cache 的運行狀態以及行為信息。
最後將新創建出來的 slab cache 添加到全局雙向鏈表 list 的末尾。下麵我們來一起看下這個創建過程的詳細實現。
static struct kmem_cache *create_cache(const char *name,
unsigned int object_size, unsigned int align,
slab_flags_t flags, unsigned int useroffset,
unsigned int usersize, void (*ctor)(void *),
struct mem_cgroup *memcg, struct kmem_cache *root_cache)
{
struct kmem_cache *s;
// 為將要創建的 slab cache 分配 kmem_cache 結構
// kmem_cache 也是內核的一個核心數據結構,同樣也會被它對應的 slab cache 所管理
// 這裡就是從 kmem_cache 所屬的 slab cache 中拿出一個 kmem_cache 對象出來
s = kmem_cache_zalloc(kmem_cache, GFP_KERNEL);
// 利用我們指定的創建參數初始化 kmem_cache 結構
s->name = name;
s->size = s->object_size = object_size;
s->align = align;
s->ctor = ctor;
s->useroffset = useroffset;
s->usersize = usersize;
// 創建 slab cache 的核心函數,這裡會初始化 kmem_cache 結構中的其他重要屬性
// 包括創建初始化 kmem_cache_cpu 和 kmem_cache_node 結構
err = __kmem_cache_create(s, flags);
if (err)
goto out_free_cache;
// slab cache 初始狀態下,引用計數為 1
s->refcount = 1;
// 將剛剛創建出來的 slab cache 加入到 slab cache 在內核中的全局鏈表管理
list_add(&s->list, &slab_caches);
out:
if (err)
return ERR_PTR(err);
return s;
out_free_cache:
// 創建過程出現錯誤之後,釋放 kmem_cache 對象
kmem_cache_free(kmem_cache, s);
goto out;
}
內核中的每個核心數據結構都會有其專屬的 slab cache 來管理,比如,筆者在本文 《3. slab 對象池在內核中的應用場景》小節介紹的 task_struct,mm_struct,page,file,socket 等等一系列的內核核心數據結構。
而這裡的 slab cache 的數據結構 struct kmem_cache 同樣也屬於內核的核心數據結構,它也有其專屬的 slab cache 來專門管理 kmem_cache 對象的分配與釋放。
內核在啟動階段,會專門為 struct kmem_cache 創建其專屬的 slab cache,保存在全局變數 kmem_cache 中。
// 全局變數,用於專門管理 kmem_cache 對象的 slab cache
// 定義在文件:/mm/slab_common.c
struct kmem_cache *kmem_cache;
同理,slab cache 的 NUMA 節點緩存 kmem_cache_node 結構也是如此,內核也會為其創建一個專屬的 slab cache,保存在全局變數 kmem_cache_node 中。
// 全局變數,用於專門管理 kmem_cache_node 對象的 slab cache
// 定義在文件:/mm/slub.c
static struct kmem_cache *kmem_cache_node;
在 create_cache 函數的開始,內核會從 kmem_cache 專屬的 slab cache 中申請一個 kmem_cache 對象。
s = kmem_cache_zalloc(kmem_cache, GFP_KERNEL);
然後用我們在 kmem_cache_create 介面函數中指定的參數初始化 kmem_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 函數中近一步初始化 kmem_cache 對象的其他重要屬性。比如,初始化 slab 對象的記憶體佈局相關信息,計算 slab 所需要的物理記憶體頁個數以及所能容納的對象個數,創建初始化 cpu 本地緩存結構以及 NUMA 節點的緩存結構。
最後將剛剛創建出來的 slab cache 加入到 slab cache 在內核中的全局鏈表 list 中管理
list_add(&s->list, &slab_caches);
3. __kmem_cache_create 初始化 kmem_cache 對象
__kmem_cache_create 函數的主要工作就是建立 slab cache 的基本骨架,包括初始化 kmem_cache 結構中的其他重要屬性,創建初始化本地 cpu 緩存結構以及 NUMA 節點緩存結構,這一部分的重要工作封裝在 kmem_cache_open 函數中完成。
隨後會檢查內核 slab allocator 整個體系的狀態,只有 slab_state = FULL
的狀態才表示整個 slab allocator 體系已經在內核中建立並初始化完成了,可以正常運轉了。
通過 slab allocator 的狀態檢查之後,就是 slab cache 整個創建過程的最後一步,利用 sysfs_slab_add 為其在 sys 文件系統中創建 /sys/kernel/slab/name
目錄,該目錄下的文件詳細記錄了 slab cache 運行時的各種信息。
int __kmem_cache_create(struct kmem_cache *s, slab_flags_t flags)
{
int err;
// 核心函數,在這裡會初始化 kmem_cache 的其他重要屬性
err = kmem_cache_open(s, flags);
if (err)
return err;
// 檢查內核中 slab 分配器的整體體系是否已經初始化完畢,只有狀態是 FULL 的時候才是初始化完畢,其他的狀態表示未初始化完畢。
// 在 slab allocator 體系初始化的時候在 slab_sysfs_init 函數中將 slab_state 設置為 FULL
if (slab_state <= UP)
return 0;
// 在 sys 文件系統中創建 /sys/kernel/slab/name 節點,該目錄下的文件包含了對應 slab cache 運行時的詳細信息
err = sysfs_slab_add(s);
if (err)
// 出現錯誤則釋放 kmem_cache 結構
__kmem_cache_release(s);
return err;
}
4. slab allocator 整個體系的狀態變遷
__kmem_cache_create 函數的整個邏輯還是比較好理解的,這裡唯一不好理解的就是 slab allocator 整個體系的狀態 slab_state。
只有 slab_state 為 FULL 狀態的時候,才代表 slab allocator 體系能夠正常運轉,包括這裡的創建 slab cache,以及後續從 slab cache 分配對象,釋放對象等操作。
只要 slab_state 不是 FULL 狀態,slab allocator 體系就是處於半初始化狀態,下麵筆者就為大家介紹一下 slab_state 的狀態變遷流程,這裡大家只做簡單瞭解,因為隨著後續源碼的深入,筆者還會在相關章節重覆提起。
// slab allocator 整個體系的狀態 slab_state。
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_state 的初始化狀態就是 DOWN
。
當內核啟動的過程中,會開始創建初始化 slab allocator 體系,第一步就是為 struct kmem_cache_node 結構創建其專屬的 slab cache —— kmem_cache_node
。後續再創建新的 slab cache 的時候,其中的 NUMA 節點緩存結構就是從 kmem_cache_node
里分配。
當 kmem_cache_node 專屬的 slab cache 創建完畢之後, slab_state 的狀態就變為了 PARTIAL
。
slab allocator 體系建立的最後一項工作,就是創建 kmalloc 記憶體池體系,kmalloc 體系成功創建之後,slab_state 的狀態就變為了 UP
,其實現在 slab allocator 體系就可以正常運轉了,但是還不是最終的理想狀態。
當內核的初始化工作全部完成的時候,會在 arch_call_rest_init
函數中調用 do_initcalls()
,開啟內核的 initcall 階段。
asmlinkage __visible void __init start_kernel(void)
{
........ 省略 .........
/* Do the rest non-__init'ed, we're now alive */
arch_call_rest_init();
}
在內核的 initcall 階段,會調用內核中定義的所有 initcall,而建立 slab allocator 體系的最後一項工作就為其在 sys 文件系統中創建 /sys/kernel/slab
目錄節點,這裡會存放系統中所有 slab cache 的詳細運行信息。
這一項工作就封裝在 slab_sysfs_init
函數中,而 slab_sysfs_init 在內核中被定義成了一個 __initcall 函數。
__initcall(slab_sysfs_init);
static int __init slab_sysfs_init(void)
{
struct kmem_cache *s;
int err;
mutex_lock(&slab_mutex);
slab_kset = kset_create_and_add("slab", &slab_uevent_ops, kernel_kobj);
if (!slab_kset) {
mutex_unlock(&slab_mutex);
pr_err("Cannot register slab subsystem.\n");
return -ENOSYS;
}
slab_state = FULL;
....... 省略 ......
}
當 /sys/kernel/slab
目錄節點被創建之後,在 slab_sysfs_init 函數中會將 slab_state 變為 FULL。至此內核中的 slab allocator 整個體系就全部建立起來了。
5. 初始化 slab cache 的核心函數 kmem_cache_open
kmem_cache_open 是初始化 slab cache 內核數據結構 kmem_cache 的核心函數,在這裡會初始化 kmem_cache 結構中的一些重要核心參數,以及為 slab cache 創建初始化本地 cpu 緩存結構 kmem_cache_cpu 和 NUMA 節點緩存結構 kmem_cache_node。
經歷過 kmem_cache_open 之後,如下圖所示的 slab cache 的整個骨架就全部創建出來了。
static int kmem_cache_open(struct kmem_cache *s, slab_flags_t flags)
{
// 計算 slab 中對象的整體記憶體佈局所需要的 size
// slab 所需最合適的記憶體頁面大小 order,slab 中所能容納的對象個數
// 初始化 slab cache 中的核心參數 oo ,min,max的值
if (!calculate_sizes(s, -1))
goto error;
// 設置 slab cache 在 node 緩存 kmem_cache_node 中的 partial 列表中 slab 的最小個數 min_partial
set_min_partial(s, ilog2(s->size) / 2);
// 設置 slab cache 在 cpu 本地緩存的 partial 列表中所能容納的最大空閑對象個數
set_cpu_partial(s);
// 為 slab cache 創建並初始化 node cache 數組
if (!init_kmem_cache_nodes(s))
goto error;
// 為 slab cache 創建並初始化 cpu 本地緩存列表
if (alloc_kmem_cache_cpus(s))
return 0;
}
calculate_sizes 函數中封裝了 slab 對象記憶體佈局的全部邏輯,筆者在上篇文章《細節拉滿,80 張圖帶你一步一步推演 slab 記憶體池的設計與實現》 中的 《5. 從一個簡單的記憶體頁開始聊 slab》小節中介紹的內容,背後的實現邏輯全部封裝在此。
除了確定 slab 對象的記憶體佈局之外,calculate_sizes 函數還會初始化 kmem_cache 的其他核心參數:
struct kmem_cache {
// slab 中管理的對象大小,註意:這裡包含對象為了對齊所填充的位元組數
unsigned int size; /* The size of an object including metadata */
// slab 對象池中的對象在沒有被分配之前,我們是不關心對象裡邊存儲的內容的。
// 內核巧妙的利用對象占用的記憶體空間存儲下一個空閑對象的地址。
// offset 表示用於存儲下一個空閑對象指針的位置距離對象首地址的偏移
unsigned int offset; /* Free pointer offset */
// 表示 cache 中的 slab 大小,包括 slab 所申請的頁面個數,以及所包含的對象個數
// 其中低 16 位表示一個 slab 中所包含的對象總數,高 16 位表示一個 slab 所占有的記憶體頁個數。
struct kmem_cache_order_objects oo;
// slab 中所能包含對象以及記憶體頁個數的最大值
struct kmem_cache_order_objects max;
// 當按照 oo 的尺寸為 slab 申請記憶體時,如果記憶體緊張,會採用 min 的尺寸為 slab 申請記憶體,可以容納一個對象即可。
struct kmem_cache_order_objects min;
}
在完成了對 kmem_cache 結構的核心屬性初始化工作之後,內核緊接著會調用 set_min_partial
來設置 kmem_cache->min_partial
,從而限制 slab cache 在 numa node 中緩存的 slab 個數上限。
struct kmem_cache {
// slab cache 在 numa node 中緩存的 slab 個數上限,slab 個數超過該值,空閑的 empty slab 則會被回收至伙伴系統
unsigned long min_partial;
}
調用 set_cpu_partial
來設置 kmem_cache->cpu_partial
,從而限制 slab cache 在 cpu 本地緩存 partial 鏈表中空閑對象個數的上限。
struct kmem_cache {
// 限定 slab cache 在每個 cpu 本地緩存 partial 鏈表中所有 slab 中空閑對象的總數
// cpu 本地緩存 partial 鏈表中空閑對象的數量超過該值,則會將 cpu 本地緩存 partial 鏈表中的所有 slab 轉移到 numa node 緩存中。
unsigned int cpu_partial;
};
最後調用 init_kmem_cache_nodes
函數為 slab cache 在每個 NUMA 節點中創建其所屬的緩存結構 kmem_cache_node。
調用 alloc_kmem_cache_cpus
函數為 slab cache 創建每個 cpu 的本地緩存結構 kmem_cache_cpu。
現在 slab cache 的整個骨架就被完整的創建出來了,下麵我們來看一下這個過程中涉及到的幾個核心函數。
6. slab 對象的記憶體佈局
在上篇文章《細節拉滿,80 張圖帶你一步一步推演 slab 記憶體池的設計與實現》的《5. 從一個簡單的記憶體頁開始聊 slab》小節的內容介紹中,筆者詳細的為大家介紹了 slab 對象的記憶體佈局,本小節,我們將從內核源碼實現角度再來談一下 slab 對象的記憶體佈局,看一下內核是如何具體規劃 slab 對象的記憶體佈局的。
再開始本小節的內容之前,筆者建議大家先去回顧下前文第五小節的內容。
static int calculate_sizes(struct kmem_cache *s, int forced_order)
{
slab_flags_t flags = s->flags;
unsigned int size = s->object_size;
unsigned int order;
// 為了提高 cpu 訪問對象的速度,slab 對象的 object size 首先需要與 word size 進行對齊
size = ALIGN(size, sizeof(void *));
#ifdef CONFIG_SLUB_DEBUG
// SLAB_POISON:對象中毒標識,是 slab 中的一個術語,用於將對象所占記憶體填充某些特定的值,表示這塊對象不同的使用狀態,防止非法越界訪問。
// 比如:在將對象分配出去之前,會將對象所占記憶體用 0x6b 填充,並用 0xa5 填充 object size 區域的最後一個位元組。
// SLAB_TYPESAFE_BY_RCU:啟用 RCU 鎖釋放 slab
if ((flags & SLAB_POISON) && !(flags & SLAB_TYPESAFE_BY_RCU) &&
!s->ctor)
s->flags |= __OBJECT_POISON;
else
s->flags &= ~__OBJECT_POISON;
// SLAB_RED_ZONE:表示在空閑對象前後插入 red zone 紅色區域(填充特定位元組 0xbb),防止對象溢出越界
// size == s->object_size 表示對象 object size 與 word size 本來就是對齊的,並沒有填充任何位元組
// 這時就需要在對象 object size 記憶體區域的後面插入一段 word size 大小的 red zone。
// 如果對象 object size 與 word size 不是對齊的,填充了位元組,那麼這段填充的位元組恰好可以作為右側 red zone,而不需要額外分配 red zone 空間
if ((flags & SLAB_RED_ZONE) && size == s->object_size)
size += sizeof(void *);
#endif
// inuse 表示 slab 中的對象實際使用的記憶體區域大小
// 該值是經過與 word size 對齊之後的大小,如果設置了 SLAB_RED_ZONE,則也包括紅色區域大小
s->inuse = size;
if (((flags & (SLAB_TYPESAFE_BY_RCU | SLAB_POISON)) ||
s->ctor)) {
// 如果我們開啟了 RCU 保護或者設置了對象 poison或者設置了對象的構造函數
// 這些都會占用對象中的記憶體空間。這種情況下,我們需要額外增加一個 word size 大小的空間來存放 free pointer,否則 free pointer 存儲在對象的起始位置
// offset 為 free pointer 與對象起始地址的偏移
s->offset = size;
size += sizeof(void *);
}
#ifdef CONFIG_SLUB_DEBUG
if (flags & SLAB_STORE_USER)
// SLAB_STORE_USER 表示需要跟蹤對象的分配和釋放信息
// 需要再對象的末尾增加兩個 struct track 結構,存儲分配和釋放的信息
size += 2 * sizeof(struct track);
#ifdef CONFIG_SLUB_DEBUG
if (flags & SLAB_RED_ZONE) {
// 在對象記憶體區域的左側增加 red zone,大小為 red_left_pad
// 防止對這塊對象記憶體的寫越界
size += sizeof(void *);
s->red_left_pad = sizeof(void *);
s->red_left_pad = ALIGN(s->red_left_pad, s->align);
size += s->red_left_pad;
}
#endif
// slab 從它所申請的記憶體頁 offset 0 開始,一個接一個的存儲對象
// 調整對象的 size 保證對象之間按照指定的對齊方式 align 進行對齊
size = ALIGN(size, s->align);
s->size = size;
// 這裡 forced_order 傳入的是 -1
if (forced_order >= 0)
order = forced_order;
else
// 計算 slab 所需要申請的記憶體頁數(2 ^ order 個記憶體頁)
order = calculate_order(size);
if ((int)order < 0)
return 0;
// 根據 slab 的 flag 設置,設置向伙伴系統申請記憶體時使用的 allocflags
s->allocflags = 0;
if (order)
// slab 所需要的記憶體頁多於 1 頁時,則向伙伴系統申請複合頁。
s->allocflags |= __GFP_COMP;
// 從 DMA 區域中獲取適用於 DMA 的記憶體頁
if (s->flags & SLAB_CACHE_DMA)
s->allocflags |= GFP_DMA;
// 從 DMA32 區域中獲取適用於 DMA 的記憶體頁
if (s->flags & SLAB_CACHE_DMA32)
s->allocflags |= GFP_DMA32;
// 申請可回收的記憶體頁
if (s->flags & SLAB_RECLAIM_ACCOUNT)
s->allocflags |= __GFP_RECLAIMABLE;
// 計算 slab cache 中的 oo,min,max 值
// 一個 slab 到底需要多少個記憶體頁,能夠存儲多少個對象
// 低 16 為存儲 slab 所能包含的對象總數,高 16 為存儲 slab 所需的記憶體頁個數
s->oo = oo_make(order, size);
// get_order 函數計算出的 order 為容納一個 size 大小的對象至少需要的記憶體頁個數
s->min = oo_make(get_order(size), size);
if (oo_objects(s->oo) > oo_objects(s->max))
// 初始時 max 和 oo 相等
s->max = s->oo;
// 返回 slab 中所能容納的對象個數
return !!oo_objects(s->oo);
}
在內核對 slab 對象開始記憶體佈局之前,為了提高 cpu 訪問對象的速度,首先需要將 slab 對象的 object size 與 word size 進行對齊。如果 object size 與 word size 本來就是對齊的,那麼內核不會做任何事情。如果不是對齊的,那麼就需要在對象後面填充一些位元組,達到與 word size 對齊的目的。
size = ALIGN(size, sizeof(void *));
如果我們設置了 SLAB_RED_ZONE,表示需要再對象 object size 記憶體區域前後各插入一段 red zone 區域,目的是為了防止記憶體的讀寫越界。
如果對象 object size 與 word size 本來就是對齊的,並沒有填充任何位元組:size == s->object_size
,那麼此時就需要在對象 object size 記憶體區域的後面插入一段 word size 大小的 red zone。
如果對象 object size 與 word size 不是對齊的,那麼內核就會在 object size 區域後面填充位元組達到與 word size 對齊的目的,而這段填充的位元組恰好可以作為對象右側 red zone ,而不需要額外為右側 red zone 分配記憶體空間。
if ((flags & SLAB_RED_ZONE) && size == s->object_size)
size += sizeof(void *);
如果我們設置了 SLAB_POISON 或者開啟了 RCU 或者設置了對象的構造函數,它們都會占用對象的實際記憶體區域 object size。
比如我們設置 SLAB_POISON 之後, slab 對象的 object size 記憶體區域會被內核用特殊字元 0x6b 填充,並用 0xa5 填充對象 object size 記憶體區域的最後一個位元組表示填充完畢。
這樣一來,用於指向下一個空閑對象的 freepointer 就沒地方存放了,所以需要在當前對象記憶體區域的基礎上再額外開闢一段 word size 大小的記憶體區域專門存放 freepointer。
if (((flags & (SLAB_TYPESAFE_BY_RCU | SLAB_POISON)) ||
s->ctor)) {
// offset 為 free pointer 與對象起始地址的偏移
s->offset = size;
size += sizeof(void *);
}
除此之外,對象的 freepointer 指針就會放在對象本身記憶體區域 object size 中,因為在對象被分配出去之前,用戶根本不會關心對象記憶體里到底存放的是什麼。
如果我們設置了 SLAB_STORE_USER,表示我們期望跟蹤 slab 對象的分配與釋放相關的信息,而這些跟蹤信息內核使用一個 struct track 結構來存儲。
所以在這種情況下,內核需要在目前 slab 對象的記憶體區域後面額外增加兩個 sizeof(struct track)
大小的區域出來,用來分別存儲 slab 對象的分配和釋放信息。
如果我們設置了 SLAB_RED_ZONE,最後,還需要再 slab 對象記憶體區域的左側填充一段 red_left_pad 大小的記憶體區域作為左側 red zone。另外還需要再 slab 對象記憶體區域的末尾再次填充一段 word size 大小的記憶體區域作為 padding 部分。
右側 red zone,在本小節開始的地方已經被填充了。
if (flags & SLAB_RED_ZONE) {
size += sizeof(void *);
s->red_left_pad = sizeof(void *);
s->red_left_pad = ALIGN(s->red_left_pad, s->align);
size += s->red_left_pad;
}
現在關於 slab 對象記憶體佈局的全部內容,我們就介紹完了,最終我們得到了 slab 對象真實占用記憶體大小 size,內核會根據這個 size,在物理記憶體頁中劃分出一個一個的對象出來。
那麼一個 slab 到底需要多少個物理記憶體頁呢?內核會通過 calculate_order 函數根據一定的演算法計算出一個合理的 order 值。這個過程筆者後面會細講,現在我們主要關心整體流程。
slab 所需的物理記憶體頁個數計算出來之後,內核會根據 slab 對象占用記憶體的大小 size,計算出一個 slab 可以容納的對象個數。並將這個結果保存在 kmem_cache 結構中的 oo
屬性中。
s->oo = oo_make(order, size);
struct kmem_cache {
// 表示 cache 中的 slab 大小,包括 slab 所申請的頁面個數,以及所包含的對象個數
// 其中低 16 位表示一個 slab 中所包含的對象總數,高 16 位表示一個 slab 所占有的記憶體頁個數。
struct kmem_cache_order_objects oo;
}
內核會通過 struct kmem_cache_order_objects 這樣一個結構來保存 slab 所需要的物理記憶體頁個數以及 slab 所能容納的對象個數,其中 kmem_cache_order_objects 的高 16 位保存 slab 所需要的物理記憶體頁個數,低 16 位保存 slab 所能容納的對象個數。
#define OO_SHIFT 16
struct kmem_cache_order_objects {
// 高 16 為存儲 slab 所需的記憶體頁個數,低 16 為存儲 slab 所能包含的對象總數
unsigned int x;
};
static inline struct kmem_cache_order_objects oo_make(unsigned int order,
unsigned int size)
{
struct kmem_cache_order_objects x = {
// 高 16 為存儲 slab 所需的記憶體頁個數,低 16 為存儲 slab 所能包含的對象總數
(order << OO_SHIFT) + order_objects(order, size)
};
return x;
}
static inline unsigned int order_objects(unsigned int order, unsigned int size)
{
// 根據 slab 中包含的物理記憶體頁個數以及對象的 size,計算 slab 可容納的對象個數
return ((unsigned int)PAGE_SIZE << order) / size;
}
static inline unsigned int oo_order(struct kmem_cache_order_objects x)
{
// 獲取高 16 位,slab 中所需要的記憶體頁 order
return x.x >> OO_SHIFT;
}
// 十進位為:65535,二進位為:16 個 1,用於截取低 16 位
#define OO_MASK ((1 << OO_SHIFT) - 1)
static inline unsigned int oo_objects(struct kmem_cache_order_objects x)
{
// 獲取低 16 位,slab 中能容納的對象個數
return x.x & OO_MASK;
}
隨後內核會通過 get_order 函數來計算,容納一個 size 大小的對象所需要的最少物理記憶體頁個數。用這個值作為 kmem_cache 結構中的 min 屬性。
s->min = oo_make(get_order(size), size);
struct kmem_cache {
struct kmem_cache_order_objects min;
}
內核在創建 slab 的時候,最開始會按照 oo 指定的尺寸來向伙伴系統申請記憶體頁,如果記憶體緊張,申請記憶體失敗。那麼內核會降級採用 min 的尺寸再次向伙伴系統申請記憶體。也就是說 slab 中至少會包含一個對象。
最後會設置 max 的值,從源碼中我們可以看到 max 的值與 oo 的值是相等的
if (oo_objects(s->oo) > oo_objects(s->max))
// 初始時 max 和 oo 相等
s->max = s->oo;
到現在為止,筆者在本文 《6.1 slab 的基礎信息管理》小節中介紹的 kmem_cache 結構相關的重要屬性就全部設置完成了。
7. 計算 slab 所需要的 page 個數
一個 slab 究竟需要多少個物理記憶體頁就是在這裡計算出來的,這裡內核會根據一定的演算法,儘量保證 slab 中的記憶體碎片最小化,綜合計算出一個合理的 order 值。下麵我們來一起看下這個計算邏輯:
static unsigned int slub_min_order;
static unsigned int slub_max_order = PAGE_ALLOC_COSTLY_ORDER;// 3
static unsigned int slub_min_objects;
static inline int calculate_order(unsigned int size)
{
unsigned int order;
unsigned int min_objects;
unsigned int max_objects;
// 計算 slab 中可以容納的最小對象個數
min_objects = slub_min_objects;
if (!min_objects)
// nr_cpu_ids 表示當前系統中的 cpu 個數
// fls 可以獲取參數的最高有效 bit 的位數,比如 fls(0)=0,fls(1)=1,fls(4) = 3
// 如果當前系統中有4個cpu,那麼 min_object 的初始值為 4*(3+1) = 16
min_objects = 4 * (fls(nr_cpu_ids) + 1);
// slab 最大記憶體頁 order 初始為 3,計算 slab 最大可容納的對象個數
max_objects = order_objects(slub_max_order, size);
min_objects = min(min_objects, max_objects);
while (min_objects > 1) {
// slab 中的碎片控制繫數,碎片大小不能超過 (slab所占記憶體大小 / fraction)
// fraction 值越大,slab 中所能容忍的碎片就越小
unsigned int fraction;
fraction = 16;
while (fraction >= 4) {
// 根據當前 fraction 計算 order,需要查找出能夠使 slab 產生碎片最小化的 order 值出來
order = slab_order(size, min_objects,
slub_max_order, fraction);
// order 不能超過 max_order,否則需要降低 fraction,放寬對碎片的要求限制,重新迴圈計算
if (order <= slub_max_order)
return order;
fraction /= 2;
}
// 進一步放寬對 min_object 的要求,slab 會嘗試少放一些對象
min_objects--;
}
// 經過前邊 while 迴圈的計算,我們無法在這一個 slab 中放置多個 size 大小的對象,因為 min_object = 1 的時候就退出迴圈了。
// 那麼下麵就會嘗試看能不能只放入一個對象
order = slab_order(size, 1, slub_max_order, 1);
if (order <= slub_max_order)
return order;
// 流程到這裡表示,我們要池化的對象 size 太大了,slub_max_order 都放不下
// 現在只能放寬對 max_order 的限制到 MAX_ORDER = 11
order = slab_order(size, 1, MAX_ORDER, 1);
if (order < MAX_ORDER)
return order;
return -ENOSYS;
}
首先內核會計算出 slab 需要容納對象的最小個數 min_objects,計算公式: min_objects = 4 * (fls(nr_cpu_ids) + 1)
:
-
nr_cpu_ids 表示當前系統中的 cpu 個數
-
fls 獲取參數二進位形式的最高有效 bit 的位數,比如 fls(0)=0,fls(1)=1,fls(4) = 3
這裡我們看到 min_objects 是和當前系統中的 cpu 個數有關係的。
內核規定 slab 所需要的物理記憶體頁個數的最大值 slub_max_order 初始化為 3,也就是 slab 最多只能向伙伴系統中申請 8 個記憶體頁。
根據這裡的 slub_max_order 和 slab 對象的 size 通過 order_objects 函數計算出 slab 所能容納對象的最大值。
slab 所能容納的對象個數越多,那麼所需要的物理記憶體頁就越多,slab 所能容納的對象個數越少,那麼所需要的物理記憶體頁就越少。
內核通過剛剛計算出的 min_objects 可以計算出 slab 所需要的最小記憶體頁個數,我們暫時稱為 min_order。
隨後內核會遍歷 min_order 與 slub_max_order 之間的所有 order 值,直到找到滿足記憶體碎片限制要求的一個 order。
那麼內核對於記憶體碎片限制的要求具體如何定義呢?
內核會定義一個 fraction 變數作為 slab 記憶體碎片的控制繫數,內核要求 slab 中記憶體碎片大小不能超過 (slab所占記憶體大小 / fraction)
,fraction 的值越大,表示 slab 中所能容忍的記憶體碎片就越小。fraction 的初始值為 16。
在內核尋找最佳合適 order 的過程中,最高優先順序是要將記憶體碎片控制在一個非常低的範圍內,在這個基礎之上,遍歷 min_order 與 slub_max_order 之間的所有 order 值,看他們產生碎片的大小是否低於 (slab所占記憶體大小 / fraction)
的要求。如果滿足,那麼這個 order 就是最終的計算結果,後續 slab 會根據這個 order 值向伙伴系統申請物理記憶體頁。這個邏輯封裝在 slab_order 函數中。
如果內核遍歷完一遍 min_order 與 slub_max_order 之間的所有 order 值均不符合記憶體碎片限制的要求,那麼內核只能嘗試放寬對記憶體碎片的要求,將 fraction 調小一些——fraction /= 2
,再次重新遍歷所有 order。但 fraction 繫數最低不能低於 4。
如果 fraction 繫數低於 4 了,說明內核已經將碎片限制要求放到最寬了,在這麼寬鬆的條件下依然無法找到一個滿足限制要求的 order 值,那麼內核會在近一步的降級,放寬對 min_objects 的要求——min_objects--
,嘗試在 slab 中少放一些對象。fraction 繫數恢復為 16,在重新遍歷,嘗試查找符合記憶體碎片限制要求的 order 值。
最極端的情況就是,無論內核怎麼放寬對記憶體碎片的限制,無論怎麼放寬 slab 中容納對象的最小個數要求,內核始終無法找到一個 order 值能夠滿足如此寬鬆的記憶體碎片限制條件。當 min_objects == 1 的時候就會退出 while (min_objects > 1)
迴圈停止尋找。
最終內核的托底方案是將 min_objects 調整為 1,fraction 調整為 1,再次調用 slab_order ,這裡的語義是:在這種極端的情況下,slab 中最少只能容納一個對象,那麼內核就分配容納一個對象所需要的記憶體頁。
如果 slab 對象太大了,有可能突破了 slub_max_order = 3 的限制,內核會近一步放寬至 MAX_ORDER = 11,這裡我們可以看出內核的決心,無論如何必須保證 slab 中至少容納一個對象。
下麵是 slab_order 函數的邏輯,它是整個計算過程的核心:
// 一個 page 最多允許存放 32767 個對象
#define MAX_OBJS_PER_PAGE 32767
static inline unsigned int slab_order(unsigned int size,
unsigned int min_objects, unsigned int max_order,
unsigned int fract_leftover)
{
unsigned int min_order = slub_min_order;
unsigned int order;
// 如果 2^min_order個記憶體頁可以存放的對象個數超過 32767 限制
// 那麼返回 size * MAX_OBJS_PER_PAGE 所需要的 order 減 1
if (order_objects(min_order, size) > MAX_OBJS_PER_PAGE)
return get_order(size * MAX_OBJS_PER_PAGE) - 1;
// 從 slab 所需要的最小 order 到最大 order 之間開始遍歷,查找能夠使 slab 碎片最小的 order 值
for (order = max(min_order, (unsigned int)get_order(min_objects * size));
order <= max_order; order++) {
// slab 在當前 order 下,所占用的記憶體大小
unsigned int slab_size = (unsigned int)PAGE_SIZE << order;
unsigned int rem;
// slab 的碎片大小:分配完 object 之後,所產生的碎片大小
rem = slab_size % size;
// 碎片大小 rem 不能超過 slab_size / fract_leftover 即符合要求
if (rem <= slab_size / fract_leftover)
break;
}
return order;
}
get_order(size)
函數的邏輯就比較簡單了,它不會像 calculate_order 函數那樣複雜,不需要考慮記憶體碎片的限制。它的邏輯只是簡單的計算分配一個 size 大小的對象所需要的最少記憶體頁個數,用於在 calculate_sizes 函數的最後計算 kmem_cache 結構的 min 值。
s->min = oo_make(get_order(size), size);
get_order 函數的計算邏輯如下:
- 如果給定的 size 在 [0,PAGE_SIZE] 之間,那麼 order = 0 ,需要一個記憶體頁面即可。
- size 在 [PAGE_SIZE + 1, 2^1 * PAGE_SIZE] 之間, order = 1
- size 在 [2^1 * PAGE_SIZE + 1, 2^2 * PAGE_SIZE] 之間, order = 2
- size 在 [2^2 * PAGE_SIZE + 1, 2^3 * PAGE_SIZE] 之間, order = 3
- size 在 [2^3 * PAGE_SIZE + 1, 2^4 * PAGE_SIZE] 之間, order = 4
// 定義在文件 /include/asm-generic/getorder.h
// 該函數的主要作用就是根據給定的 size 計算出所需最小的 order
static inline __attribute_const__ int get_order(unsigned long size)
{
if (__builtin_constant_p(size)) {
if (!size)
return BITS_PER_LONG - PAGE_SHIFT;
if (size < (1UL << PAGE_SHIFT))
return 0;
return ilog2((size) - 1) - PAGE_SHIFT + 1;
}
size--;
size >>= PAGE_SHIFT;
#if BITS_PER_LONG == 32
return fls(size);
#else
return fls64(size);
#endif
}
現在,一個 slab 所需要的記憶體頁個數的計算過程,筆者就為大家交代完畢了,下麵我們來看一下 kmem_cache 結構的其他屬性的初始化過程。
8. set_min_partial
該函數的主要目的是為了計算 slab cache 在 NUMA 節點緩存 kmem_cache_node->partial 鏈表中的 slab 個數上限,超過該值,空閑的 empty slab 則會被回收至伙伴系統中。
kmem_cache 結構中的 min_partial 初始值為 min = ilog2(s->size) / 2
,需要保證 min_partial 的值在 [5,10] 的範圍之內。
#define MIN_PARTIAL 5
#define MAX_PARTIAL 10
// 計算 slab cache 在 node 中緩存的個數,kmem_cache_node 中 partial 列表中 slab 個數的上限 min_partial
// 超過該值,空閑的 slab 就會被回收
// 初始 min = ilog2(s->size) / 2,必須保證 min_partial 的值 在 [MIN_PARTIAL,MAX_PARTIAL] 之間
static void set_min_partial(struct kmem_cache *s, unsigned long min)
{
if (min < MIN_PARTIAL)
min = MIN_PARTIAL;
else