slab 内存池的设计与实现

slab 内存池的设计与实现目录 从一个简单的内存页开始聊 slab slab 的总体架构设计 slab 的组织架构 slab 内存分配 slab 内存回收 参考文献 伙伴系统内存分配原理的相关内容来看 伙伴系统管理物理内存的最小单位是物理内存页 page 也就是说 当我们向伙伴系统申请内存时 至少要申请一个物理内存页

大家好,我是讯享网,很高兴认识大家。

目录

从一个简单的内存页开始聊 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 内存池的设计与实现

小讯
上一篇 2025-03-29 10:33
下一篇 2025-01-14 15:06

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/75701.html