深度解讀 Linux 核心級通用記憶體池 —— kmalloc 體系

2023-06-21 12:01:09

本文是筆者 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 體系的初始化工作核心分為兩個部分:

  1. setup_kmalloc_cache_index_table 初始化我們在本文 《2. kmalloc 記憶體池如何選取合適尺寸的記憶體塊》小節中介紹的 size_index 陣列,後續 kmalloc 在分配 192 位元組以下的記憶體塊時,核心會利用該陣列選取最佳合適尺寸的 slab cache。

  2. 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 的選擇

這裡的邏輯比較簡單,核心就是以下三個規則:

  1. 如果 gfp_t flags 沒有特殊指定,那麼在預設情況下,核心向 kmalloc 記憶體池申請的記憶體均來自於 ZONE_NORMAL 實體記憶體區域。

  2. 如果 gfp_t flags 明確指定了 __GFP_DMA,則核心向 kmalloc 記憶體池申請的記憶體均來自於 ZONE_DMA 實體記憶體區域。

  3. 如果 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 體系的全部內容到這裡就全部介紹完了,感謝大家的收看,我們下篇文章見~~~