孙俪 发表于 2023-6-21 16:49:14

深度解读 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 指向的通用 slab cache 尺寸,也就是说 kmalloc 内存池体系中的每个通用 slab cache 中内存块的尺寸由其所在的 kmalloc_info[] 数组 index 决定,对应内存块大小为:2^index 字节,比如:

[*]kmalloc_info 对应的通用 slab cache 中所管理的内存块尺寸为 8 字节。
[*]kmalloc_info 对应的通用 slab cache 中所管理的内存块尺寸为 32 字节。
[*]kmalloc_info 对应的通用 slab cache 中所管理的内存块尺寸为 512 字节。
[*]kmalloc_info 对应的通用 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 数组来存放申请内存块大小在 192 字节以下的 kmalloc 内存池选取规则。
其中 size_index 数组中每个元素后面跟的注释部分为内核要申请的字节数,size_index 数组中每个元素表示最佳合适尺寸的通用 slab cache 在kmalloc_info[] 数组中的索引。
static u8 size_index __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 存储的信息表示,如果内核申请的内存块低于 8 字节时,那么 kmalloc 将会到 kmalloc_info 所指定的通用 slab cache —— kmalloc-8 中分配一个 8 字节大小的内存块。
[*]size_index 存储的信息表示,如果内核申请的内存块在 128 字节到 136 字节之间时,那么 kmalloc 将会到 kmalloc_info 所指定的通用 slab cache —— kmalloc-192 中分配一个 192 字节大小的内存块。
[*]同样的道理,申请 144,152,160 ..... 192 等字节尺寸的内存块对应的最佳 slab cache 选取规则也是如此,都是通过 size_index 数组中的值找到 kmalloc_info 数组的索引,然后通过 kmalloc_info 指定的 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;

[*]第一维数组用于表示 kmalloc 内存池中的内存来源于哪些物理内存区域中,也就是前边介绍的 enum kmalloc_cache_type。
[*]第二维数组中的元素一共 KMALLOC_SHIFT_HIGH 个,用于存储每种内存块尺寸对应的 slab cache。在 slub 实现中,kmalloc 内存池中的内存块尺寸在 8字节到 8K 之间,其中还包括了两个特殊的尺寸分别为 96 字节 和 192 字节。
第二维数组中的 index 表示的含义和kmalloc_info[] 数组中的 index 含义一模一样,均是表示对应 slab cache 中内存块尺寸的分配阶(2 的次幂)。96 和 192 这两个内存块尺寸除外,它们的 index 分别是 1 和2,单独特殊指定。
好了,到现在我们已经清楚了 kmalloc 内存池的整体架构,那么这个架构体系又是如何被创建出来的呢 ?我们带着这个疑问,接着往下看~~~
4. kmalloc 内存池的创建

由于 kmalloc 体系底层依赖的是 slab allocator 体系,所以 kmalloc 体系的创建是在 slab allocator 体系创建之后进行的,关于 slab allocator 体系创建的详细内容笔者已经在 《从内核源码看 slab 内存池的创建初始化流程》一文的 “12. 内核第一个 slab cache 是如何被创建出来的” 小节介绍过了,在内核初始化内存管理子系统的时候,会在 kmem_cache_init 函数中完成 slab alloactor 体系的创建初始化工作,之后紧接着就会创建初始化 kmalloc 体系。
asmlinkage __visible void __init start_kernel(void)
{   
      ........ 省略 .........
      // 初始化内存管理子系统
      mm_init();
      
      ........ 省略 .........
}

/*
* Set up kernel memory allocators
*/
static void __init mm_init(void)
{
      ........ 省略 .........
      // 创建并初始化 slab allocator 体系
      kmem_cache_init();

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

void __init kmem_cache_init(void)
{
    ........... 省略 slab allocator 体系的创建初始化过程 ......

    /* Now we can use the kmem_cache to allocate kmalloc slabs */
    // 初始化上边提到的 size_index 数组
    setup_kmalloc_cache_index_table();
    // 创建 kmalloc_info 数组中保存的各个内存块大小对应的 slab cache
    // 最终将这些不同尺寸的 slab cache 缓存在 kmalloc_caches 中
    create_kmalloc_caches(0);
}kmalloc 体系的初始化工作核心分为两个部分:

[*]setup_kmalloc_cache_index_table 初始化我们在本文 《2. kmalloc 内存池如何选取合适尺寸的内存块》小节中介绍的 size_index数组,后续 kmalloc 在分配 192 字节以下的内存块时,内核会利用该数组选取最佳合适尺寸的 slab cache。
[*]create_kmalloc_caches 创建初始化上一小节中介绍的 kmalloc_caches 二维数组,这个二维数组正式 kmalloc 体系的核心。内核会利用 kmalloc_caches 直接找到对应的 slab cache 进行内存块的分配和释放。
4.1 kmalloc_caches 的创建


struct kmem_cache *
kmalloc_caches;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   3void __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
页: [1]
查看完整版本: 深度解读 Linux 内核级通用内存池 —— kmalloc 体系