從核心原始碼看 slab 記憶體池的建立初始化流程

bin的技術小屋發表於2023-04-12

在上篇文章 《細節拉滿,80 張圖帶你一步一步推演 slab 記憶體池的設計與實現
中,筆者從 slab cache 的總體架構演進角度以及 slab cache 的執行原理角度為大家勾勒出了 slab cache 的總體架構檢視,基於這個檢視詳細闡述了 slab cache 的記憶體分配以及釋放原理。

slab cache 機制確實比較複雜,涉及到的場景又很多,大家讀到這裡,我想肯定會好奇或者懷疑筆者在上篇文章中所論述的那些原理的正確性,畢竟 talk is cheap ,所以為了讓大家看著安心,理解起來放心,從本文開始,我們將正式進入 show you the code 的階段。筆者會基於核心 5.4 版本,詳細為大家剖析 slab cache 在核心中的原始碼實現。

image

在上篇文章 《5. 從一個簡單的記憶體頁開始聊 slab》和 《6. slab 的總體架構設計》小節中,筆者帶大家從一個最簡單的實體記憶體頁開始,一步一步演進 slab cache 的架構,最終得到了一副 slab cache 完整的架構圖:

image

在本文的內容中,筆者會帶大家到核心原始碼實現中,來看一下 slab cache 在核心中是如何被一步一步建立出來的,以及核心是如何安排 slab 物件在記憶體中的佈局的。

image

我們先以核心建立 slab cache 的介面函式 kmem_cache_create 為起點,來一步一步揭秘 slab cache 的建立過程。

image

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 進行修改。

image

在確保 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。

image

在 __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 需要滿足以下四個條件:

  1. 指定的 slab_flags_t 相同。

  2. 指定物件的 object size 要小於等於已有 slab cache 中的物件 size (kmem_cache->size)。

  3. 如果指定物件的 object size 與已有 kmem_cache->size 不相同,那麼它們之間的差值需要再一個 word size 之內。

  4. 已有 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> 目錄下的各個檔案。

image

由於我們當前並沒有真正建立一個新的 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 需要滿足以下四個條件:

  1. 指定的 slab_flags_t 相同。

  2. 指定物件的 object size 要小於等於已有 slab cache 中的物件 size (kmem_cache->size)。

  3. 如果指定物件的 object size 與已有 kmem_cache->size 不相同,那麼它們之間的差值需要再一個 word size 之內。

  4. 已有 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

image

在前文《細節拉滿,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 的整個骨架就全部建立出來了。

image

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 *));

image

如果我們設定了 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 *);

image

如果我們設定了 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 *);
    }

image

除此之外,物件的 freepointer 指標就會放在物件本身記憶體區域 object size 中,因為在物件被分配出去之前,使用者根本不會關心物件記憶體裡到底存放的是什麼。

image

如果我們設定了 SLAB_STORE_USER,表示我們期望跟蹤 slab 物件的分配與釋放相關的資訊,而這些跟蹤資訊核心使用一個 struct track 結構來儲存。

所以在這種情況下,核心需要在目前 slab 物件的記憶體區域後面額外增加兩個 sizeof(struct track) 大小的區域出來,用來分別儲存 slab 物件的分配和釋放資訊。

image

如果我們設定了 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;
    }

image

現在關於 slab 物件記憶體佈局的全部內容,我們就介紹完了,最終我們得到了 slab 物件真實佔用記憶體大小 size,核心會根據這個 size,在實體記憶體頁中劃分出一個一個的物件出來。

image

那麼一個 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 if (min > MAX_PARTIAL)
        min = MAX_PARTIAL;
    s->min_partial = min;
}

9. set_cpu_partial

這裡會設定 kmem_cache 結構的 cpu_partial 屬性,該值限制了 slab cache 在 cpu 本地快取的 partial 列表中所能容納的最大空閒物件個數。

同時該值也決定了當 kmem_cache_cpu->partial 連結串列為空時,核心會從 kmem_cache_node->partial 連結串列填充 cpu_partial / 2 個 slab 到 kmem_cache_cpu->partial 連結串列中。相關詳細內容可回顧上篇文章《細節拉滿,80 張圖帶你一步一步推演 slab 記憶體池的設計與實現》 中的 《7.3 從 NUMA 節點快取中分配》小節。

set_cpu_partial 函式的邏輯也很簡單,就是根據上篇文章 《6 slab 物件的記憶體佈局》小節中計算出的 slab 物件 size 大小來決定 cpu_partial 的值。

static void set_cpu_partial(struct kmem_cache *s)
{
// 當配置了 CONFIG_SLUB_CPU_PARTIAL,則 slab cache 的 cpu 本地快取 kmem_cache_cpu 中包含 partial 列表
#ifdef CONFIG_SLUB_CPU_PARTIAL
    // 判斷 kmem_cache_cpu 是否包含有 partial 列表
    if (!kmem_cache_has_cpu_partial(s))
        s->cpu_partial = 0;
    else if (s->size >= PAGE_SIZE)
        s->cpu_partial = 2;
    else if (s->size >= 1024)
        s->cpu_partial = 6;
    else if (s->size >= 256)
        s->cpu_partial = 13;
    else
        s->cpu_partial = 30;
#endif
}

10. init_kmem_cache_nodes

到現在為止,kmem_cache 結構中的所有重要屬性就已經初始化完畢了,slab cache 的建立過程也進入了尾聲,最後核心需要為 slab cache 建立本地 cpu 快取結構以及 NUMA 節點快取結構

本小節的主要內容就是核心如何為 slab cache 建立其在 NUMA 節點中的快取結構 :

struct kmem_cache {
    // slab cache 中 numa node 中的快取,每個 node 一個
    struct kmem_cache_node *node[MAX_NUMNODES];
}

slab cache 在每個 NUMA 節點中都有自己的快取結構 kmem_cache_node,init_kmem_cache_nodes 函式需要遍歷所有的 NUMA 節點,並利用 struct kmem_cache_node 專屬的 slab cache —— 全域性變數 kmem_cache_node,分配一個 kmem_cache_node 物件,並呼叫 init_kmem_cache_node 對其進行初始化。

static int init_kmem_cache_nodes(struct kmem_cache *s)
{
    int node;
    // 遍歷所有的 numa 節點,為 slab cache 建立 node cache
    for_each_node_state(node, N_NORMAL_MEMORY) {
        struct kmem_cache_node *n;

        if (slab_state == DOWN) {
            // 如果此時 slab allocator 體系還未建立,則呼叫該方法分配 kmem_cache_node 結構,並初始化。
            // slab cache 的正常建立流程不會走到這個分支,該分支用於在核心初始化的時候建立 kmem_cache_node 物件池使用
            early_kmem_cache_node_alloc(node);
            continue;
        }
        // 為 node cache 分配對應的 kmem_cache_node 物件
        // kmem_cache_node 物件也由它對應的 slab cache 管理
        n = kmem_cache_alloc_node(kmem_cache_node,
                        GFP_KERNEL, node);
        // 初始化 node cache
        init_kmem_cache_node(n);
        // 初始化 slab cache 結構 kmem_cache 中的 node 陣列
        s->node[node] = n;
    }
    return 1;
}
static void
init_kmem_cache_node(struct kmem_cache_node *n)
{
    n->nr_partial = 0;
    spin_lock_init(&n->list_lock);
    INIT_LIST_HEAD(&n->partial);
#ifdef CONFIG_SLUB_DEBUG
    atomic_long_set(&n->nr_slabs, 0);
    atomic_long_set(&n->total_objects, 0);
    INIT_LIST_HEAD(&n->full);
#endif
}

11. alloc_kmem_cache_cpus

這裡主要是為 slab cache 建立其 cpu 本地快取結構 kmem_cache_cpu,每個 cpu 一個這樣的結構,並呼叫 per_cpu_ptr 將建立好的 kmem_cache_cpu 結構與對應的 cpu 相關聯初始化。

struct kmem_cache {
    // 每個 cpu 擁有一個本地快取,用於無鎖化快速分配釋放物件
    struct kmem_cache_cpu __percpu *cpu_slab;
}
static inline int alloc_kmem_cache_cpus(struct kmem_cache *s)
{
    // 為 slab cache 分配 cpu 本地快取結構 kmem_cache_cpu
    // __alloc_percpu 函式在核心中專門用於分配 percpu 型別的結構體(the percpu allocator)
    //  kmem_cache_cpu 結構也是 percpu 型別的,這裡透過 __alloc_percpu 直接分配
    s->cpu_slab = __alloc_percpu(sizeof(struct kmem_cache_cpu),
                     2 * sizeof(void *));
    // 初始化 cpu 本地快取結構 kmem_cache_cpu
    init_kmem_cache_cpus(s);
    return 1;
}
static void init_kmem_cache_cpus(struct kmem_cache *s)
{
    int cpu;
    // 遍歷所有CPU,透過 per_cpu_ptr 將前面分配的 kmem_cache_cpu 結構與對應的CPU關聯對應起來
    // 同時初始化 kmem_cache_cpu 變數裡面的 tid 為其所關聯 cpu 的編號
    for_each_possible_cpu(cpu)
        per_cpu_ptr(s->cpu_slab, cpu)->tid = init_tid(cpu);
}

至此,slab cache 的整個骨架就全部被建立出來了,最終得到的 slab cache 完整架構如下圖所示:

image

最後,我們可以結合上面的 slab cache 架構圖與下面這副 slab cache 建立流程圖加以對比,回顧總結。

image

12. 核心第一個 slab cache 是如何被建立出來的

在上小節介紹 slab cache 的建立過程中,筆者其實暗暗地埋下了一個伏筆,不知道,大家有沒有發現,在 slab cache 建立的過程中需要建立兩個特殊的資料結構:

  • 一個是 slab cache 自身的管理結構 struct kmem_cache。

  • 另一個是 slab cache 在 NUMA 節點中的快取結構 struct kmem_cache_node。

而 struct kmem_cache 和 struct kmem_cache_node 同樣也都是核心的核心資料結構,他倆各自也有一個專屬的 slab cache 來管理 kmem_cache 物件和 kmem_cache_node 物件的分配與釋放。

// 全域性變數,用於專門管理 kmem_cache 物件的 slab cache
// 定義在檔案:/mm/slab_common.c
struct kmem_cache *kmem_cache;

// 全域性變數,用於專門管理 kmem_cache_node 物件的 slab cache
// 定義在檔案:/mm/slub.c
static struct kmem_cache *kmem_cache_node;

slab cache 的 cpu 本地快取結構 struct kmem_cache_cpu 是一個 percpu 型別的變數,由 __alloc_percpu直接建立,並不需要一個專門的 slab cache 來管理。

在 slab cache 的建立過程中,核心首先需要向 struct kmem_cache 結構專屬的 slab cache 申請一個 kmem_cache 物件。

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;
    s = kmem_cache_zalloc(kmem_cache, GFP_KERNEL);

                  ......... 省略 .........
}

當 kmem_cache 物件初始化完成之後,核心需要向 struct kmem_cache_node 結構專屬的 slab cache 申請一個 kmem_cache_node 物件,作為 slab cache 在 NUMA 節點中的快取結構。

static int init_kmem_cache_nodes(struct kmem_cache *s)
{
    int node;
    // 遍歷所有的 numa 節點,為 slab cache 建立 node cache
    for_each_node_state(node, N_NORMAL_MEMORY) {
        struct kmem_cache_node *n;

                ......... 省略 .........

        n = kmem_cache_alloc_node(kmem_cache_node,
                        GFP_KERNEL, node);
        init_kmem_cache_node(n);
        s->node[node] = n;
    }
    return 1;
}

那麼問題來了,kmem_cachekmem_cache_node 這兩個 slab cache 是怎麼來的?

因為他倆本質上是一個 slab cache,而 slab cache 的建立又需要 kmem_cache (slab cache)和 kmem_cache_node (slab cache),當系統中第一個 slab cache 被建立的時候,此時並沒有 kmem_cache (slab cache),也沒有 kmem_cache_node (slab cache),這就變成死鎖了,是一個先有雞還是先有蛋的問題。

image

那麼核心是如何來解決這個先有雞還是先有蛋的問題呢?讓我們先把思緒拉回到核心啟動的階段~~~

12.1 slab allocator 體系的初始化

核心啟動的核心初始化邏輯封裝 /init/main.c 檔案的 start_kernel 函式中,在這裡會初始化核心的各個子系統,記憶體管理子系統的初始化工作就在這裡,封裝在 mm_init 函式里。

在 mm_init 函式中會初始化核心的 slab allocator 體系 —— kmem_cache_init()。

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();

      ........ 省略 .........
}

而核心解決這個 “先有雞還是先有蛋” 問題的秘密就藏在 /mm/slub.c 檔案的 kmem_cache_init 函式中。

核心首先會定義兩個靜態的 static __initdata struct kmem_cache 結構 boot_kmem_cache,boot_kmem_cache_node ,用於在核心初始化記憶體管理子系統的時候臨時靜態地建立 kmem_cache(slab cache)和 kmem_cache_node (slab cache)所需要的 struct kmem_cache 和 struct kmem_cache_node 結構

這樣一來,核心就透過這兩個臨時的靜態 kmem_cache 結構 :boot_kmem_cache,boot_kmem_cache_node 打破了死鎖的迴圈等待條件。

當這兩個臨時的 boot_kmem_cache,boot_kmem_cache_node 被建立初始化之後,隨後核心會透過 bootstrap 將這兩個臨時 slab cache 深複製到全域性變數 kmem_cache(slab cache)和 kmem_cache_node (slab cache)中。

從此,核心就有了正式的 kmem_cache(slab cache)和 kmem_cache_node (slab cache),後續就可以按照正常流程動態地建立 slab cache 了,正常的建立流程就是筆者在本文前邊幾個小節中為大家介紹的內容。

下面我們來一起看下 slab allocator 體系的初始化過程:

// 全域性變數,用於專門管理 kmem_cache 物件的 slab cache
// 定義在檔案:/mm/slab_common.c
struct kmem_cache *kmem_cache;

// 全域性變數,用於專門管理 kmem_cache_node 物件的 slab cache
// 定義在檔案:/mm/slub.c
static struct kmem_cache *kmem_cache_node;

void __init kmem_cache_init(void)
{
    // slab allocator 體系結構中最核心的就是 kmem_cache 結構和 kmem_cache_node 結構,而這兩個結構同時又被各自的 slab cache 所管理
    // 而現在 slab allocator 體系還未建立,所以需要利用兩個靜態的結構來建立kmem_cache,kmem_cache_node 物件
    // 這裡就是定義兩個 kmem_cache 型別的靜態區域性變數(靜態結構):核心啟動的時候被載入進 BSS 段中,隨後會為其分配記憶體。
    // boot_kmem_cache 用於臨時建立 kmem_cache 結構。
    // boot_kmem_cache_node 用於臨時建立 kmem_cache_node 結構
    // boot_kmem_cache 和 boot_kmem_cache_node 現在只是兩個空的結構,需要靜態的進行初始化。
    static __initdata struct kmem_cache boot_kmem_cache,
        boot_kmem_cache_node;

    // 暫時先將這兩個靜態結構賦值給對應的全域性變數,後面會初始化這兩個全域性變數
    kmem_cache_node = &boot_kmem_cache_node;
    kmem_cache = &boot_kmem_cache;

    // 靜態地初始化 boot_kmem_cache_node 
    // 從這裡可以看出 slab體系,建立的第一個 slab cache 就是 kmem_cache_node(slab cache)
    create_boot_cache(kmem_cache_node, "kmem_cache_node",
        sizeof(struct kmem_cache_node), SLAB_HWCACHE_ALIGN, 0, 0);

    // 當 kmem_cache_node (slab cache)被建立初始化之後,slab_state 變為 PARTIAL
    // 這個狀態表示目前 kmem_cache_node cache已經建立完畢,可以利用它動態分配 kmem_cache_node 物件了。
    slab_state = PARTIAL;

    // 靜態地初始化 boot_kmem_cache
    // 從這裡可以看出 slab 體系,建立的第二個 slab cache 就是 kmem_cache(slab cache)
    create_boot_cache(kmem_cache, "kmem_cache",
            offsetof(struct kmem_cache, node) +
                nr_node_ids * sizeof(struct kmem_cache_node *),
               SLAB_HWCACHE_ALIGN, 0, 0);

    // 流程到這裡,兩個靜態的 kmem_cache 結構:boot_kmem_cache,boot_kmem_cache_node 就已經初始化完畢了。
    // 但是這兩個靜態結構只是臨時的,目的是為了在 slab 體系初始化階段靜態的建立 kmem_cache 物件和 kmem_cache_node 物件。
    // 在 bootstrap 中會將 boot_kmem_cache,boot_kmem_cache_node 中的內容深複製到最終的 kmem_cache(slab cache),kmem_cache_node(slab cache)中。
    // 後面我們就可以利用這兩個最終的核心結構來動態的進行 slab 建立。
    kmem_cache = bootstrap(&boot_kmem_cache);
    kmem_cache_node = bootstrap(&boot_kmem_cache_node);

    ........ 省略 kmalloc 相關初始化過程 .........
}

初始化 slab allocator 體系的核心就是如何靜態的建立和初始化這兩個靜態的 slab cache: boot_kmem_cache,boot_kmem_cache_node。

這個核心邏輯封裝在 create_boot_cache 函式中,大家需要注意該函式第一個引數 struct kmem_cache *s,引數 s 指向的是上面兩個臨時的靜態的 slab cache。現在是核心初始化階段,當前系統中並不存在一個正式完整的 slab cache,這一點大家在閱讀本小節的時候要時刻注意。

/* Create a cache during boot when no slab services are available yet */
void __init create_boot_cache(struct kmem_cache *s, const char *name,
        unsigned int size, slab_flags_t flags,
        unsigned int useroffset, unsigned int usersize)
{
    int err;
    unsigned int align = ARCH_KMALLOC_MINALIGN;

    // 下面就是靜態初始化 kmem_cache 結構的邏輯
    // 挨個對 kmem_cache 結構的核心屬性進行靜態賦值
    s->name = name;
    s->size = s->object_size = size;

    if (is_power_of_2(size))
        align = max(align, size);
    // 根據指定的對齊引數 align 以及 CPU Cache line 的大小計算出一個合適的 align 出來
    s->align = calculate_alignment(flags, align, size);

    s->useroffset = useroffset;
    s->usersize = usersize;
    // 這裡又來到了之前介紹的建立 slab cache 的建立流程
    // 該函式是建立 slab cache 的核心函式,這裡會初始化 kmem_cache 結構中的其他重要屬性
    // 以及建立初始化 slab cache 中的 cpu 本地快取 和 node 節點快取結構
    err = __kmem_cache_create(s, flags);
    // 暫時不需要合併 merge,引用計數設定為 -1
    s->refcount = -1; 
}

這裡在對靜態 kmem_cache 結構進行簡單初始化之後,核心又呼叫了 __kmem_cache_create 函式,這個函式我們已經非常熟悉了,忘記的同學可以回看下本文的 《3. __kmem_cache_create 初始化 kmem_cache 物件》小節。

__kmem_cache_create 函式的主要工作就是建立 slab cache 的基本骨架,包括初始化 kmem_cache 結構中的其他重要核心屬性,建立初始化本地 cpu 快取結構以及 NUMA 節點快取結構。

image

這裡我們來重點看下 init_kmem_cache_nodes 函式,在核心初始化靜態 boot_kmem_cache_node(靜態 slab cache)的場景下,這裡的流程邏輯與 《10. init_kmem_cache_nodes》小節中介紹的會有所不同。

在 slab allocator 體系中,第一個被建立出來的 slab cache 就是這裡的 boot_kmem_cache_node,當前 slab_state == DOWN。當前流程正在建立初始化 boot_kmem_cache_node,所以目前核心無法利用 boot_kmem_cache_node 來動態的分配 kmem_cache_node 物件。

所以當建立初始化 boot_kmem_cache_node 的時候,流程會進入 if (slab_state == DOWN) 分支,透過 early_kmem_cache_node_alloc 函式來靜態分配 kmem_cache_node 物件。

static int init_kmem_cache_nodes(struct kmem_cache *s)
{
    int node;
    // 遍歷所有的 numa 節點,為 slub cache 建立初始化 node cache 陣列
    for_each_node_state(node, N_NORMAL_MEMORY) {
        struct kmem_cache_node *n;
        // 當 slub 在系統啟動階段初始化時,建立 kmem_cache_node cache 的時候,此時 slab_state == DOWN
        // 由於此時 kmem_cache_node cache 正在建立,所以無法利用 kmem_cache_node 所屬的 slub cache 動態的分配 kmem_cache_node 物件
        // 這裡會透過 early_kmem_cache_node_alloc 函式靜態的分配 kmem_cache_node 物件,並初始化。
        if (slab_state == DOWN) {
             // 建立 boot_kmem_cache_node 時會走到這個分支
            early_kmem_cache_node_alloc(node);
            continue;
        }

        // 當 slab 體系在初始化 boot_kmem_cache 時,這時 slab_state 為 PARTIAL,流程就會走到這裡。
        // 表示此時 boot_kmem_cache_node 已經初始化,可以利用它動態的分配 kmem_cache_node 物件了
        // 這裡的 kmem_cache_node 就是 boot_kmem_cache_node
        n = kmem_cache_alloc_node(kmem_cache_node,
                        GFP_KERNEL, node);
        // 初始化 kmem_cache_node 物件
        init_kmem_cache_node(n);
        // 初始化 slab cache 結構 kmem_cache 中的 node 陣列
        s->node[node] = n;
    }
    return 1;
}

在 slab allocator 體系中,第二個被建立出來的 slab cache 就 boot_kmem_cache,在建立初始化 boot_kmem_cache 的時候,slab_state 就變為了 PARTIAL,表示 kmem_cache_node 結構的專屬 slab cache 已經建立出來了,可以利用它來動態分配 kmem_cache_node 物件了。

12.2 kmem_cache_node 結構的臨時靜態建立

正如前面小節中所介紹的,在 slab allocator 體系中第一個被核心建立出來的 slab cache 正是 boot_kmem_cache_node,而它本身就是一個 slab cache,專門用於分配 kmem_cache_node 物件。

而建立一個 slab cache 最核心的就是要為其分配 struct kmem_cache 結構 ( slab cache 在核心中的資料結構)還有就是 slab cache 在 NUMA 節點的快取結構 kmem_cache_node。

而針對 struct kmem_cache 結構核心已經透過定義靜態結構 boot_kmem_cache_node 解決了。

static __initdata struct kmem_cache boot_kmem_cache_node;

而針對 kmem_cache_node 結構,核心中既沒有定義這樣一個靜態資料結構,也沒有一個 slab cache 專門管理,所以核心會透過這裡的 early_kmem_cache_node_alloc 函式來建立 kmem_cache_node 物件。

注意:這裡是為 boot_kmem_cache_node 這個靜態的 slab cache 初始化它的 NUMA 節點快取陣列。

struct kmem_cache {
    // slab cache 中 numa node 中的快取,每個 node 一個
    struct kmem_cache_node *node[MAX_NUMNODES];
}
static void early_kmem_cache_node_alloc(int node)
{
    // slab 的本質就是一個或者多個實體記憶體頁 page,這裡用於指向 slab 所屬的 page。
    // 如果 slab 是由多個物理頁 page 組成(複合頁),這裡指向複合頁的首頁
    struct page *page;
    // 這裡主要為 boot_kmem_cache_node 初始化它的 node cache 陣列
    // 這裡會靜態建立指定 node 節點對應的 kmem_cache_node 結構
    struct kmem_cache_node *n;

    // 此時 boot_kmem_cache_node 這個 kmem_cache 結構已經初始化好了(參見第 9 小節的內容)。
    // 根據 kmem_cache 結構中的 kmem_cache_order_objects oo 屬性向指定 node 節點所屬的夥伴系統申請 2^order 個記憶體頁 page
    // 這裡返回複合頁的首頁,目的是為 kmem_cache_node 結構分配 slab,後續該 slab 會掛在 kmem_cache_node 結構中的 partial 列表中
    page = new_slab(kmem_cache_node, GFP_NOWAIT, node);

    // struct page 結構中的 freelist 指向 slab 中第一個空閒的物件
    // 這裡的物件就是  struct kmem_cache_node 結構
    n = page->freelist;
#ifdef CONFIG_SLUB_DEBUG
    // 根據 slab cache 中的 flag 初始化 kmem_cache_node 物件
    init_object(kmem_cache_node, n, SLUB_RED_ACTIVE);
#endif
    // 重新設定 slab 中的下一個空閒物件。
    // 這裡是獲取物件 n 中的 free_pointer 指標,指向 n 的下一個空閒物件
    page->freelist = get_freepointer(kmem_cache_node, n);
    // 表示 slab 中已經有一個物件被使用了
    page->inuse = 1;
    // 這裡可以看出 boot_kmem_cache_node 的 NUMA 節點快取在這裡初始化的時候
    // 核心會為每個 NUMA 節點申請一個 slab,並快取在它的 partial 連結串列中
    // 並不是快取在 boot_kmem_cache_node 的本地 cpu 快取中
    page->frozen = 0;
    // 這裡的 kmem_cache_node 指的是 boot_kmem_cache_node
    // 初始化 boot_kmem_cache_node 中的 node cache 陣列
    kmem_cache_node->node[node] = n;
    // 初始化 node 節點對應的 kmem_cache_node 結構
    init_kmem_cache_node(n);
    // kmem_cache_node 結構中的 nr_slabs 計數加1,total_objects 加 page->objects
    inc_slabs_node(kmem_cache_node, node, page->objects);
    // 將新建立出來的 slab (page表示),新增到物件 n (kmem_cache_node結構)中的 partial 列表頭部
    __add_partial(n, page, DEACTIVATE_TO_HEAD);
}

當 boot_kmem_cache_node 被初始化之後,它的整個結構如下圖所示:

image

12.3 將臨時靜態的 kmem_cache 結構變為正式的 slab cache

流程到這裡 boot_kmem_cache,boot_kmem_cache_node 這兩個靜態結構就已經被初始化好了,現在核心就可以透過他們來動態的建立 kmem_cache 物件和 kmem_cache_node 物件了。

但是這裡的 boot_kmem_cache 和 boot_kmem_cache_node 只是臨時的 kmem_cache 結構,目的是在 slab allocator 體系初始化的時候用於靜態建立 kmem_cache (slab cache), kmem_cache_node (slab cache)。

// 全域性變數,用於專門管理 kmem_cache 物件的 slab cache
// 定義在檔案:/mm/slab_common.c
struct kmem_cache *kmem_cache;

// 全域性變數,用於專門管理 kmem_cache_node 物件的 slab cache
// 定義在檔案:/mm/slub.c
static struct kmem_cache *kmem_cache_node;

既然是臨時的結構,所以這裡需要建立兩個最終的全域性 kmem_cache 結構,然後將這兩個靜態臨時結構深複製到最終的全域性 kmem_cache 結構中。

static struct kmem_cache * __init bootstrap(struct kmem_cache *static_cache)
{
    int node;
    // kmem_cache 指向專門管理 kmem_cache 物件的 slab cache
    // 該 slab cache 現在已經全部初始化完畢,可以利用它動態的分配最終的 kmem_cache 物件
    struct kmem_cache *s = kmem_cache_zalloc(kmem_cache, GFP_NOWAIT);
    struct kmem_cache_node *n;
    // 將靜態的 kmem_cache 物件,比如:boot_kmem_cache,boot_kmem_cache_node
    // 深複製到最終的 kmem_cache 物件 s 中
    memcpy(s, static_cache, kmem_cache->object_size);

    // 釋放本地 cpu 快取的 slab
    __flush_cpu_slab(s, smp_processor_id());
    // 遍歷 node cache 陣列,修正 kmem_cache_node 結構中 partial 連結串列中包含的 slab ( page 表示)對應 page 結構的 slab_cache 指標
    // 使其指向最終的 kmem_cache 結構,之前在 create_boot_cache 中指向的靜態 kmem_cache 結構,這裡需要修正
    for_each_kmem_cache_node(s, node, n) {
        struct page *p;

        list_for_each_entry(p, &n->partial, slab_list)
            p->slab_cache = s;
    }
    // 將最終的 kmem_cache 結構加入到全域性 slab cache 連結串列中
    list_add(&s->list, &slab_caches);
    return s;
}

12.4 為什麼要先建立 boot_kmem_cache_node 而不是 boot_kmem_cache

現在關於 slab alloactor 體系的初始化流程筆者就為大家全部介紹完了,最後我們借用這個問題,再對這個流程做一個簡單的總體回顧。

首先 slab cache 建立要依賴兩個核心的資料機構,kmem_cache,kmem_cache_node:

image

其中 kmem_cache 結構是 slab cache 在核心中的資料結構,同樣也需要被一個專門的 slab cache 所管理,但是在核心初始化階段 slab 體系還未建立,所以核心透過定義兩個區域性靜態變數來解決 kmem_cache 結構的建立問題。

  static __initdata struct kmem_cache boot_kmem_cache,
        boot_kmem_cache_node;

隨後核心會在 calculate_size 函式中初始化 struct kmem_cache 結構中的核心屬性。詳細內容可回顧上篇文章的 《6 slab 物件的記憶體佈局》小節的內容。

image

現在 kmem_cache 結構的問題解決了,但是這兩個 slab cache 中的 kmem_cache_node 結構的問題又來了。

所以核心決定首先建立 boot_kmem_cache_node,並透過 early_kmem_cache_node_alloc 函式為 boot_kmem_cache_node 建立 kmem_cache_node 結構。

當 boot_kmem_cache_node 被建立出來之後,核心就可以動態的分配 kmem_cache_node 物件了。

所以最後建立 boot_kmem_cache,在遇到 kmem_cache_node 結構建立的時候,直接使用 boot_kmem_cache_node 進行動態建立。

最後透過 bootstrap 將這兩個臨時靜態的 slab cache : boot_kmem_cache,boot_kmem_cache_node 深複製到最終的全域性 slab cache 中:

// 全域性變數,用於專門管理 kmem_cache 物件的 slab cache
// 定義在檔案:/mm/slab_common.c
struct kmem_cache *kmem_cache;

// 全域性變數,用於專門管理 kmem_cache_node 物件的 slab cache
// 定義在檔案:/mm/slub.c
static struct kmem_cache *kmem_cache_node;

從此以後,核心就可以動態建立 slab cache 了。

總結

image

本文筆者基於核心 5.4 版本,從原始碼角度詳細討論了 slab cache 的建立初始化過程,建立流程如下圖所示:

image

經過該流程的建立之後,我們得到了如下圖所示的 slab cache 架構:

image

在這個過程中,筆者又近一步從原始碼角度介紹了核心具體是如何對 slab 物件進行記憶體佈局的。

image

在這個記憶體佈局的基礎上,筆者又近一步展開了核心如何計算一個 slab 到底需要多少個實體記憶體頁,以及一個 slab 到底能夠容納多少記憶體塊的相關邏輯。

最後我們介紹了 slab cache 在核心中的資料結構 struct kmem_cache 裡的 min_partial,cpu_partial 的計算邏輯。以及 slab cache 的 cpu 快取結構 cpu_slab 以及 NUMA 節點快取結構 node[MAX_NUMNODES] 的詳細初始化過程。

/*
 * Slab cache management.
 */
struct kmem_cache {

    // slab cache 在 numa node 中快取的 slab 個數上限,slab 個數超過該值,空閒的 empty slab 則會被回收至夥伴系統
    unsigned long min_partial;

    // slab cache 中 numa node 中的快取,每個 node 一個
    struct kmem_cache_node *node[MAX_NUMNODES];

#ifdef CONFIG_SLUB_CPU_PARTIAL
    // 限定 slab cache 在每個 cpu 本地快取 partial 連結串列中所有 slab 中空閒物件的總數
    // cpu 本地快取 partial 連結串列中空閒物件的數量超過該值,則會將 cpu 本地快取 partial 連結串列中的所有 slab 轉移到 numa node 快取中。
    unsigned int cpu_partial;

    // 每個 cpu 擁有一個本地快取,用於無鎖化快速分配釋放物件
    struct kmem_cache_cpu __percpu *cpu_slab;
#endif

};

在介紹完 slab cache 的整個建立流程之後,筆者在本文的最後一個小節裡又詳細的為大家介紹了整個 slab allocator 體系的初始化過程,並從原始碼實現上,看到了核心是如何解決這個先有雞還是先有蛋的問題

image

好了,本文的內容就到這裡了,在下篇文章中,筆者會帶大家繼續深入到核心原始碼中,去看一下 slab cache 是如何進行記憶體分配的~~~

相關文章