slab 内存池的设计与实现

目录

从一个简单的内存页开始聊 slab

 slab 的总体架构设计

slab 的组织架构

slab内存分配

slab内存回收

 参考文献


伙伴系统内存分配原理的相关内容来看,伙伴系统管理物理内存的最小单位是物理内存页 page。也就是说,当我们向伙伴系统申请内存时,至少要申请一个物理内存页。

内核实际运行过程中来看,无论是从内核态还是从用户态的角度来说,对于内存的需求量往往是以字节为单位,通常是几十字节到几百字节不等,远远小于一个页面的大小。如果我们仅仅为了这几十字节的内存需求,而专门为其分配一整个内存页面,这无疑是对宝贵内存资源的一种巨大浪费。

于是在内核中,这种专门针对小内存的分配需求就应运而生了,而本文的主题—— slab 内存池就是专门应对小内存频繁的分配和释放的场景的。

slab 首先会向伙伴系统一次性申请一个或者多个物理内存页面,正是这些物理内存页组成了 slab 内存池。

这种小内存在内核中的使用场景非常之多,比如,内核中那些经常使用,需要频繁申请释放的一些核心数据结构对象:task_struct 对象,mm_struct 对象,struct page 对象,struct file 对象,socket 对象等。

事实上,凡是需要被内核频繁使用的内核对象都需要被 slab 对象池所管理。

从一个简单的内存页开始聊 slab

slab 对象池在内存管理系统中的架构层次是基于伙伴系统之上构建的,slab 对象池会一次性向伙伴系统申请一个或者多个完整的物理内存页,在这些完整的内存页内在逐步划分出一小块一小块的内存块出来,而这些小内存块的尺寸就是 slab 对象池所管理的内核核心对象占用的内存大小。

因为对象在 slab 中没有被分配出去使用的时候,其实对象所占的内存中存放什么,用户根本不会关心的。既然这样,内核干脆就把指向下一个空闲对象的 freepointer 指针直接存放在对象所占内存(object size)中,这样避免了为 freepointer 指针单独再分配内存空间。巧妙的利用了对象所在的内存空间(object size)。

内核为了应对内存读写越界的场景,于是在对象内存的周围插入了一段不可访问的内存区域,这些内存区域用特定的字节 0xbb 填充,当进程访问的到内存是 0xbb 时,表示已经越界访问了。这段内存区域在 slab 中的术语为 red zone,大家可以理解为红色警戒区域。

 当 slab 刚刚从伙伴系统中申请出来,并初始化划分物理内存页中的对象内存空间时,内核会将对象的 object size 内存区域用特殊字节 0x6b 填充,并用 0xa5 填充对象 object size 内存区域的最后一个字节表示填充完毕。或者当对象被释放回 slab 对象池中的时候,也会用这些字节填充对象的内存区域。

这种通过在对象内存区域填充特定字节表示对象的特殊状态的行为,在 slab 中有一个专门的术语叫做 SLAB_POISON (SLAB 中毒)。POISON 这个术语起的真的是只可意会不可言传,其实就是表示 slab 对象的一种状态。

是否毒化 slab 对象是可以设置的,当 slab 对象被 POISON 之后,那么会有一个问题,就是我们前边介绍的存放在对象内存区域 object size 里的 freepointer 就被会特殊字节 0x6b 覆盖掉。这种情况下,内核就只能为 freepointer 在额外分配一个 word size 大小的内存空间了。

 slab 对象的内存布局信息除了以上内容之外,有时候我们还需要去跟踪一下对象的分配和释放相关信息,而这些信息也需要在 slab 对象中存储,内核中使用一个 struct track 结构体来存储跟踪信息。

这样一来,slab 对象的内存区域中就需要在开辟出两个 sizeof(struct track) 大小的区域出来,用来分别存储 slab 对象的分配和释放信息。

 

 slab 的总体架构设计

slab cache 在内核中的数据结构为 struct kmem_cache,以上介绍的这些 slab 的基本信息以及 slab 的管理结构全部定义在该结构体中:


/*
 * Slab cache management.
 */
struct kmem_cache {
    // slab cache 的管理标志位,用于设置 slab 的一些特性
    // 比如:slab 中的对象按照什么方式对齐,对象是否需要 POISON  毒化,是否插入 red zone 在对象内存周围,是否追踪对象的分配和释放信息 等等
    slab_flags_t flags;
    // slab 对象在内存中的真实占用,包括为了内存对齐填充的字节数,red zone 等等
    unsigned int size;  /* The size of an object including metadata */
    // slab 中对象的实际大小,不包含填充的字节数
    unsigned int object_size;/* The size of an object without 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;
    // 向伙伴系统申请内存时使用的内存分配标识
    gfp_t allocflags; 
    // slab cache 的引用计数,为 0 时就可以销毁并释放内存回伙伴系统重
    int refcount;   
    // 池化对象的构造函数,用于创建 slab 对象池中的对象
    void (*ctor)(void *);
    // 对象的 object_size 按照 word 字长对齐之后的大小
    unsigned int inuse;  
    // 对象按照指定的 align 进行对齐
    unsigned int align; 
    // slab cache 的名称, 也就是在 slabinfo 命令中 name 那一列
    const char *name;  
};

 slab_flags_t flags 是 slab cache 的管理标志位,用于设置 slab 的一些特性,比如:

  • 当 flags 设置了 SLAB_HWCACHE_ALIGN 时,表示 slab 中的对象需要按照 CPU 硬件高速缓存行 cache line (64 字节) 进行对齐。

  • 当 flags 设置了 SLAB_POISON 时,表示需要在 slab 对象内存中填充特殊字节  0x6b 和  0xa5,表示对象的特定状态。

  • 当 flags 设置了 SLAB_RED_ZONE 时,表示需要在 slab 对象内存周围插入 red zone,防止内存的读写越界。

  • 当 flags 设置了 SLAB_CACHE_DMA 或者 SLAB_CACHE_DMA32 时,表示指定 slab 中的内存来自于哪个内存区域,DMA or DMA32  区域 ?如果没有特殊指定,slab 中的内存一般来自于 NORMAL 直接映射区域。

  • 当 flags 设置了 SLAB_STORE_USER 时,表示需要追踪对象的分配和释放相关信息,这样会在 slab 对象内存区域中额外增加两个 sizeof(struct track) 大小的区域出来,用于存储 slab 对象的分配和释放信息。

slab 的组织架构

内核在对 slab cache 的设计也是一样,也充分考虑了多进程并发访问 slab cache 所带来的同步性能开销,内核在 slab cache 的设计中为每个 cpu 引入了 struct kmem_cache_cpu 结构的 percpu 变量,作为 slab cache 在每个 cpu 中的本地缓存。

这样一来,当进程需要向 slab cache 申请对应的内存块(object)时,首先会直接来到 kmem_cache_cpu 中查看 cpu 本地缓存的 slab,如果本地缓存的 slab 中有空闲对象,那么就直接返回了,整个过程完全没有加锁。而且访问路径特别短,防止了对 CPU 硬件高速缓存 L1Cache 中的 Instruction Cache(指令高速缓存)污染。

kmem_cache_cpu 结构中的 tid 是内核为 slab cache 的 cpu 本地缓存结构设置的一个全局唯一的 transaction id ,这个 tid 在 slab cache 分配内存块的时候主要有两个作用:

  1. 内核会将 slab cache 每一次分配内存块或者释放内存块的过程视为一个事物,所以在每次向 slab cache 申请内存块或者将内存块释放回 slab cache 之后,内核都会改变这里的 tid。

  2. tid 也可以简单看做是 cpu 的一个编号,每个 cpu 的 tid 都不相同,可以用来标识区分不同 cpu 的本地缓存 kmem_cache_cpu 结构。

其中 tid 的第二个作用是最主要的,因为进程可能在执行的过程中被更高优先级的进程抢占 cpu (开启 CONFIG_PREEMPT 允许内核抢占)或者被中断,随后进程可能会被内核重新调度到其他 cpu 上执行,这样一来,进程在被抢占之前获取到的 kmem_cache_cpu 就与当前执行进程 cpu 的 kmem_cache_cpu 不一致了。

如果开启了 CONFIG_SLUB_CPU_PARTIAL 配置项,那么在 slab cache 的 cpu 本地缓存 kmem_cache_cpu 结构中就会多出一个 partial 列表,partial 列表中存放的都是 partial slub,相当于是 cpu 缓存的备用选择.

当 kmem_cache_cpu->page (被本地 cpu 所缓存的 slab)中的对象已经全部分配出去之后,内核会到 partial 列表中查找一个 partial slab 出来,并从这个 partial slab 中分配一个对象出来,最后将 kmem_cache_cpu->page 指向这个 partial slab,作为新的 cpu 本地缓存 slab。这样一来,下次分配对象的时候,就可以直接从 cpu 本地缓存中获取了。

slab cache 的仓库就在 NUMA 节点中,而且在每一个 NUMA 节点中都有一个仓库,当 slab cache 本地 cpu 缓存 kmem_cache_cpu 中没有足够的内存块可供分配时,内核就会来到 NUMA 节点的仓库中拿出 slab 填充到 kmem_cache_cpu 中。

slab内存分配

我们可以看到 slab cache 内存分配的整个流程分为 fastpath 快速路径和 slowpath 慢速路径。

其中在 fastpath 路径下,内核会直接从 slab cache 的本地 cpu 缓存中获取内存块,这是最快的一种方式。

在本地 cpu 缓存没有足够的内存块可供分配的时候,内核就进入到了 slowpath 路径,而 slowpath 下又分为多种情况:

  1. 从本地 cpu 缓存 partial 列表中分配

  2. 从 NUMA 节点缓存中分配,其中涉及到了对本地 cpu 缓存的填充。

  3. 从伙伴系统中重新申请 slab

slab内存回收

内存回收总体也是分为快速路径 fastpath 和慢速路径 slow path,在 do_slab_free 函数中内核会首先尝试 fastpath 的回收流程。

快路径:如果释放对象所在的 slab 刚好是 slab cache 在本地 cpu 缓存 kmem_cache_cpu->page 缓存的 slab,那么内核就会直接将对象释放回缓存 slab 中

慢路径:

  1. 如果 slab 本来就在 slab cache 本地 cpu 缓存 kmem_cache_cpu->partial 链表中,那么对象在释放之后,slab 的位置不做任何改变。

  2. 如果 slab 不在 kmem_cache_cpu->partial 链表中,并且该 slab 由于对象的释放刚好由一个 full slab 变为了一个 partial slab,为了利用局部性的优势,内核需要将该 slab 插入到 kmem_cache_cpu->partial 链表中。

  3. 如果 slab 不在 kmem_cache_cpu->partial 链表中,并且该 slab 由于对象的释放刚好由一个 partial slab 变为了一个 empty slab,说明该 slab 并不是很活跃,内核会将该 slab 放入对应 NUMA 节点缓存 kmem_cache_node->partial 链表中,刀枪入库,马放南山

  4. 如果不符合第 2, 3 种场景,但是 slab 本来就在对应的 NUMA 节点缓存 kmem_cache_node->partial 链表中,那么对象在释放之后,slab 的位置不做任何改变。

slab cache 销毁的核心步骤如下:

  1. 首先需要释放 slab cache 在所有 cpu 中的缓存 kmem_cache_cpu 中占用的资源,包括被 cpu 缓存的 slab (kmem_cache_cpu->page),以及 kmem_cache_cpu->partial 链表中缓存的所有 slab,将它们统统归还到伙伴系统中。

  2. 释放 slab cache 在所有 NUMA 节点中的缓存 kmem_cache_node 占用的资源,也就是将 kmem_cache_node->partial 链表中缓存的所有 slab ,统统释放回伙伴系统中。

  3. 在 sys 文件系统中移除 /sys/kernel/slab/<cacchename> 节点相关信息。

  4. 从 slab cache 的全局列表中删除该 slab cache。

  5. 释放 kmem_cache_cpu 结构,kmem_cache_node 结构,kmem_cache 结构。

 参考文献

细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值