乐趣区

关于后端:深度解读-Linux-内核级通用内存池-kmalloc-体系

本文是笔者 slab 系列的最初一篇文章,为了不便大家疾速检索,先将相干的文章列举进去:

  • 《细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现》
  • 《从内核源码看 slab 内存池的创立初始化流程》
  • 《深刻了解 slab cache 内存调配全链路实现》
  • 《深度解析 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 体系的全部内容到这里就全副介绍完了,感激大家的收看,咱们下篇文章见~~~

退出移动版