一、概述
在上小节的内容中,笔者主要为大家介绍了 struct kmem_cache 结构中关于 slab 的一些基础信息,其中主要包括 slab cache 中所管理的 slabs 相关的容量控制,以及 slab 中对象的内存布局信息。那么 slab cache 中的这些 slabs 是如何被组织管理的呢 ?这主要设计到三个主要的结构体:
struct kmem_cache、struct kmem_cache_cpu、struct kmem_cache_node
- kmem_cache:slab内存池(又叫做slab描述符),管理着相同大小的内存块。每个kmem_chche之间以双向链表的形式进行组织。
- struct kmem_cache_cpu:内核在对 slab cache 的设计时,充分考虑了多进程并发访问 slab cache 所带来的同步性能开销,内核在 slab cache 的设计中为每个 cpu 引入了 struct kmem_cache_cpu 结构的 percpu 变量,作为 slab cache 在每个 cpu 中的本地缓存。
- struct kmem_cache_node:slab cache 中 numa node 中的缓存,每个 node 一个
可以把一个kmem_cache结构体看做是一个零售商,每个“零售商”只“零售”特定大小的内存,例如:有的“零售商”只"零售"8Byte大小的内存,有的只”零售“16Byte大小的内存。
每个零售商(kmem_cache)有两个“部门”,一个是“仓库”:kmem_cache_node,一个“营业厅”:kmem_cache_cpu。“营业厅”里只保留一个slab,只有在营业厅(kmem_cache_cpu)中没有空闲内存的情况下才会从仓库中换出其他的slab。
所谓slab就是零售商(kmem_cache)批发的连续的整页内存,零售商把这些整页的内存分成许多小内存,然后分别“零售”出去,一个slab可能包含多个连续的内存页。slab的大小和零售商有关。
当进程需要向 slab cache 申请对应的内存块(object)时,首先会直接来到 kmem_cache_cpu 中查看 cpu 本地缓存的 slab,如果本地缓存的 slab 中有空闲对象,那么就直接返回了,整个过程完全没有加锁。而且访问路径特别短,防止了对 CPU 硬件高速缓存 L1Cache 中的 Instruction Cache(指令高速缓存)污染。
当 kmem_cache_cpu->page (被本地 cpu 所缓存的 slab)中的对象已经全部分配出去之后,内核会到 partial 列表中查找一个 partial slab 出来,并从这个 partial slab 中分配一个对象出来,最后将 kmem_cache_cpu->page 指向这个 partial slab,作为新的 cpu 本地缓存 slab。这样一来,下次分配对象的时候,就可以直接从 cpu 本地缓存中获取了。
当cpu缓存的partial列表中也没有空余的object了,会从全局的kmem_cache_node申请,这个时候需要加锁,因此比直接从cpu cache中要慢一点。
如果kmem_cache_node也没有了,会从buddy system申请。
二、struct kmem_cache
slub_def.h - include/linux/slub_def.h - Linux source code v5.4.285 - Bootlin Elixir Cross Referencer
/*
* 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 cache
struct list_head list; /* List of slab caches */
unsigned int useroffset; /* Usercopy region offset */
unsigned int usersize; /* Usercopy region size */
struct kmem_cache_node *node[MAX_NUMNODES];
};
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 对象的分配和释放信息。
size 字段表示 slab 对象在内存中的真实占用大小,该大小包括对象所占内存中各种填充的内存区域大小,比如 red zone,track 区域,等等。
unsigned int object_size
表示单纯的存储 slab 对象所需要的实际内存大小。
unsigned int offset:
在上小节我们介绍 freepointer 指针的时候提到过,当对象在 slab 中缓存并没有被分配出去之前,其实对象所占内存中存储的是什么,用户根本不会去关心。内核会巧妙的利用对象的内存空间来存储 freepointer 指针,用于指向 slab 中的下一个空闲对象。
但是当 kmem_cache 结构中的 flags 设置了 SLAB_POISON 标志位之后,slab 中的对象会 POISON 毒化,被特殊字节 0x6b 和 0xa5 所填充,这样一来就会覆盖原有的 freepointer,在这种情况下,内核就需要把 freepointer 存储在对象所在内存区域的外面。所以内核就需要用一个字段来标识 freepointer 的位置, offset
字段干的就是这个事情,它表示对象的 freepointer 指针距离对象的起始内存地址的偏移 offset。
上小节中,我们也提到过,slab 的本质其实就是一个或者多个物理内存页,slab 在内核中的结构也是用 struct page 来表示的,那么一个 slab 中到底包含多少个内存页 ? 这些内存页中到底能容纳多少个内存块(object)呢?
struct kmem_cache_order_objects oo
字段就是保存这些信息的,这个结构体其实就是一个无符号的整形字段,它的高 16 位用来存储 slab 所需的物理内存页个数,低 16 位用来存储 slab 所能容纳的对象总数。
struct kmem_cache_order_objects max
字段表示 oo 的最大值,内核在初始化 slab 的时候,会将 max 的值设置为 oo。
struct kmem_cache_order_objects min
字段表示 slab 中至少需要容纳的对象个数以及容纳最少的对象所需要的内存页个数。内核在初始化 slab 的时候会 将 min 的值设置为至少需要容纳一个对象。
内核在创建 slab 的时候,最开始会按照 oo 指定的尺寸来向伙伴系统申请内存页,如果内存紧张,申请内存失败。那么内核会降级采用 min 的尺寸再次向伙伴系统申请内存。也就是说 slab 中至少会包含一个对象。
gfp_t allocflags
是内核在向伙伴系统为 slab 申请内存页的时候,所用到的内存分配标志位
unsigned int inuse
表示对象的 object size 按照 word size 对齐之后的大小,如果我们设置了SLAB_RED_ZONE,inuse 也会包括对象右侧 red zone 区域的大小。
unsigned int align
在创建 slab cache 的时候,我们可以向内核指定 slab 中的对象按照 align 的值进行对齐,内核会综合 word size , cache line ,align 计算出一个合理的对齐尺寸。
const char *name
表示该 slab cache 的名称,这里指定的 name 将会在 cat /proc/slabinfo
命令中显示,该命令用于查看系统中所有 slab cache 的信息。
struct list_head list:内核会将这些 slab cache 用一个双向链表统一串联起来。
可以使用 cat /proc/slabinfo 命令,查看slab cache的信息:
cat /proc/slabinfo
命令的显示结构主要由三部分组成:
statistics 部分显示的是 slab cache 的基本统计信息,这部分是我们最常用的,下面是每一列的含义:
- active_objs 表示 slab cache 中已经被分配出去的对象个数
- num_objs 表示 slab cache 中容纳的对象总数
- objsize 表示 slab 中对象的 object size ,单位为字节
- objperslab 表示 slab 中可以容纳的对象个数
- pagesperslab 表示 slab 所需要的物理内存页个数
tunables 部分显示的 slab cache 的动态可调节参数,如果我们采用的 slub 实现,那么 tunables 部分全是 0 ,
/proc/slabinfo
文件不可写,无法动态修改相关参数。如果我们使用的 slab 实现的话,可以通过# echo 'name limit batchcount sharedfactor' > /proc/slabinfo
命令动态修改相关参数。命令中指定的 name 就是 kmem_cache 结构中的 name 属性。tunables 这部分显示的信息均是 slab 实现中的相关字段,大家只做简单了解即可,与我们本文主题 slub 的实现没有关系。
- limit 表示在 slab 的实现中,slab cache 的 cpu 本地缓存 array_cache 最大可以容纳的对象个数
- batchcount 表示当 array_cache 中缓存的对象不够时,需要一次性填充的空闲对象个数。
slabdata 部分显示的 slab cache 的总体信息,其中 active_slabs 一列展示的 slab cache 中活跃的 slab 个数。nums_slabs 一列展示的是 slab cache 中管理的 slab 总数
三、struct kmem_cache_cpu
/*
* Slab cache management.
*/
struct kmem_cache {
// 每个 cpu 拥有一个本地缓存,用于无锁化快速分配释放对象
struct kmem_cache_cpu __percpu *cpu_slab;
}
struct kmem_cache_cpu {
// 指向被 CPU 本地缓存的 slab 中第一个空闲的对象
void **freelist; /* Pointer to next available object */
// 保证进程在 slab cache 中获取到的 cpu 本地缓存 kmem_cache_cpu 与当前执行进程的 cpu 是一致的。
unsigned long tid; /* Globally unique transaction id */
// slab cache 中 CPU 本地所缓存的 slab,由于 slab 底层的存储结构是内存页 page
// 所以这里直接用内存页 page 表示 slab
struct page *page; /* The slab from which we are allocating */
#ifdef CONFIG_SLUB_CPU_PARTIAL
// cpu cache 缓存的备用 slab 列表,同样也是用 page 表示
// 当被本地 cpu 缓存的 slab 中没有空闲对象时,内核会从 partial 列表中的 slab 中查找空闲对象
struct page *partial; /* Partially allocated frozen slabs */
#endif
#ifdef CONFIG_SLUB_STATS
// 记录 slab 分配对象的一些状态信息
unsigned stat[NR_SLUB_STAT_ITEMS];
#endif
};
struct page *page:
slab 在内核中是用 struct page 结构来描述的,这里 struct kmem_cache_cpu 结构中的 page 指针
指向的就是被 cpu 本地缓存的 slab。
void **freelist:
freelist
指针指向的是该 slab 中第一个空闲的对象。为了充分利用 slab 对象所占用的内存,内核会在对象占用内存区域内开辟一块区域来存放 freepointer 指针,而 freepointer 可以用来指向下一个空闲对象。
这样一来,通过这里的 freelist 和 freepointer 就将 slab 中所有的空闲对象串联了起来。
事实上,在 struct page 结构中也有一个 freelist 指针,用于指向该内存页中第一个空闲对象。当 slab 被缓存进 kmem_cache_cpu 中之后,page 结构中的 freelist 会赋值给 kmem_cache_cpu->freelist,然后 page->freelist 会置空。page 的 frozen 状态设置为1,表示 slab 在本地 cpu 中缓存。
struct page {
// 指向内存页中第一个空闲对象
void *freelist; /* first free object */
// 该 slab 是否在对应 slab cache 的本地 CPU 缓存中
// frozen = 1 表示缓存再本地 cpu 缓存中
unsigned frozen:1;
}
unsigned long tid:
kmem_cache_cpu 结构中的 tid 是内核为 slab cache 的 cpu 本地缓存结构设置的一个全局唯一的 transaction id ,这个 tid 在 slab cache 分配内存块的时候主要有两个作用:
-
内核会将 slab cache 每一次分配内存块或者释放内存块的过程视为一个事物,所以在每次向 slab cache 申请内存块或者将内存块释放回 slab cache 之后,内核都会改变这里的 tid。
-
tid 也可以简单看做是 cpu 的一个编号,每个 cpu 的 tid 都不相同,可以用来标识区分不同 cpu 的本地缓存 kmem_cache_cpu 结构。
其中 tid 的第二个作用是最主要的,因为进程可能在执行的过程中被更高优先级的进程抢占 cpu (开启 CONFIG_PREEMPT 允许内核抢占)或者被中断,随后进程可能会被内核重新调度到其他 cpu 上执行,这样一来,进程在被抢占之前获取到的 kmem_cache_cpu 就与当前执行进程 cpu 的 kmem_cache_cpu 不一致了。
所以在内核中,我们经常会看到如下的代码片段,目的就是为了保证进程在 slab cache 中获取到的 cpu 本地缓存 kmem_cache_cpu 与当前执行进程的 cpu 是一致的。
do {
// 获取执行当前进程的 cpu 中的 tid 字段
tid = this_cpu_read(s->cpu_slab->tid);
// 获取 cpu 本地缓存 cpu_slab
c = raw_cpu_ptr(s->cpu_slab);
// 如果两者的 tid 字段不一致,说明进程已经被调度到其他 cpu 上了
// 需要再次获取正确的 cpu 本地缓存
} while (IS_ENABLED(CONFIG_PREEMPT) &&
unlikely(tid != READ_ONCE(c->tid)));
struct page *partial
如果开启了 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 本地缓存中获取了。
unsigned stat[NR_SLUB_STAT_ITEMS]:
如果开启了 CONFIG_SLUB_STATS
配置项,内核就会记录一些关于 slab cache 的相关状态信息,这些信息同样也会在 cat /proc/slabinfo
命令中显示。
slab cache 的架构演变到现在,笔者已经为大家介绍了三种内核数据结构了,它们分别是:
- slab cache 在内核中的数据结构 struct kmem_cache
- slab cache 的本地 cpu 缓存结构 struct kmem_cache_cpu
- slab 在内核中的数据结构 struct page
现在我们把这种三种数据结构结合起来,得到下面这副 slab cache 的架构图:
四、struct kmem_cache_node
到目前为止我们的 slab cache 架构只演进到了一半,下面请大家继续跟随笔者的思路我们接着进行 slab cache 架构的演进。
我们先把 slab cache 比作一个大型超市,超市里摆放了一排一排的商品货架,毫无疑问,顾客进入超市直接从货架上选取自己想要的商品速度是最快的。 kmem_cache 结构就好比是超市,slab cache 的本地 cpu 缓存结构 kmem_cache_cpu 就好比超市的营业厅,营业厅内摆满了一排一排的货架,这些货架就是上图中的 slab,货架上的商品就是 slab 中划分出来的一个一个的内存块。毫无疑问,顾客来到超市,直接去营业厅的货架上拿取商品是最快的,那么如果货架上的商品卖完了,该怎么办呢?这时,超市的经理就会到超市的仓库中重新拿取商品填充货架,那么 slab cache 的仓库到底在哪里呢?在 NUMA 架构下,内存被划分成了一个一个的 NUMA 节点,每个 NUMA 节点内包含若干个 cpu。
每个 cpu 都可以任意访问所有 NUMA 节点中的内存,但是会有访问速度上的差异, cpu 在访问本地 NUMA 节点的速度是最快的,当本地 NUMA 节点中的内存不足时,cpu 会跨节点访问其他 NUMA 节点。
slab cache 的仓库就在 NUMA 节点中,而且在每一个 NUMA 节点中都有一个仓库,当 slab cache 本地 cpu 缓存 kmem_cache_cpu 中没有足够的内存块可供分配时,内核就会来到 NUMA 节点的仓库中拿出 slab 填充到 kmem_cache_cpu 中。
那么 slab cache 在 NUMA 节点的仓库中也没有足够的货物了,那该怎么办呢?这时,内核就会到伙伴系统中重新批量申请一批 slabs,填充到本地 cpu 缓存 kmem_cache_cpu 结构中。
伙伴系统就好比上面那个超市例子中的进货商,当超市经理发现仓库中也没有商品之后,就会联系进货商,从进货商那里批发商品,重新填充货架。
slab cache 的仓库在内核中采用 struct kmem_cache_node 结构来表示:
struct kmem_cache {
// slab cache 中 numa node 中的缓存,每个 node 一个
struct kmem_cache_node *node[MAX_NUMNODES];
}
/*
* The slab lists for all objects.
*/
struct kmem_cache_node {
spinlock_t list_lock;
....... 省略 slab 相关字段 ........
#ifdef CONFIG_SLUB
// 该 node 节点中缓存的 slab 个数
unsigned long nr_partial;
// 该链表用于组织串联 node 节点中缓存的 slabs
// partial 链表中缓存的 slab 为部分空闲的(slab 中的对象部分被分配出去)
struct list_head partial;
#ifdef CONFIG_SLUB_DEBUG // 开启 slab_debug 之后会用到的字段
// slab 的个数
atomic_long_t nr_slabs;
// 该 node 节点中缓存的所有 slab 中包含的对象总和
atomic_long_t total_objects;
// full 链表中包含的 slab 全部是已经被分配完毕的 full slab
struct list_head full;
#endif
#endif
};
这里笔者省略了 slab 实现相关的字段,我们只关注 slub 实现的部分,nr_partial
表示该 NUMA 节点缓存中缓存的 slab 总数。这些被缓存的 slabs 也是通过一个 partial 列表
被串联管理起来。
如果我们配置了 CONFIG_SLUB_DEBUG
选项,那么 kmem_cache_node 结构中就会多出一些字段来存储更加丰富的信息。nr_slabs
表示 NUMA 节点缓存中 slabs 的总数,这里会包含 partial slub 和 full slab,这时,nr_partial
表示的是 partial slab 的个数,其中 full slab 会被串联在 full 列表上。total_objects
表示该 NUMA 节点缓存中缓存的对象的总数。
在介绍完 struct kmem_cache_node 结构之后,我们终于看到了 slab cache 的架构全貌,如下图所示:
上图中展示的 slab cache 本地 cpu 缓存 kmem_cache_cpu 中的 partial 列表以及 NUMA 节点缓存 kmem_cache_node 结构中的 partial 列表并不是无限制增长的,它们的容量收到下面两个参数的限制:
/*
* Slab cache management.
*/
struct kmem_cache {
// slab cache 在 numa node 中缓存的 slab 个数上限,slab 个数超过该值,空闲的 empty slab 则会被回收至伙伴系统
unsigned long min_partial;
#ifdef CONFIG_SLUB_CPU_PARTIAL
// 限定 slab cache 在每个 cpu 本地缓存 partial 链表中所有 slab 中空闲对象的总数
// cpu 本地缓存 partial 链表中空闲对象的数量超过该值,则会将 cpu 本地缓存 partial 链表中的所有 slab 转移到 numa node 缓存中。
unsigned int cpu_partial;
#endif
};
-
min_partial 主要控制 NUMA 节点缓存 partial 列表 slab 个数,如果超过该值,那么列表中空闲的 empty slab 就会被释放回伙伴系统中。
-
cpu_partial 主要控制 slab cache 本地 cpu 缓存 kmem_cache_cpu 结构 partial 链表中缓存的空闲对象总数,如果超过该值,那么 kmem_cache_cpu->partial 列表中缓存的 slab 将会被全部转移至 kmem_cache_node->partial 列表中。
现在 slab cache 的整个架构全貌已经展现在了我们面前,下面我们基于 slab cache 的整个架构,来看一下它是如何分配和释放内存的。
ref:
细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现 - bin的技术小屋 - 博客园
【原创】(十一)Linux内存管理slub分配器 - LoyenWang - 博客园
linux内核内存分配界的葵花宝典,耐心看完,功力大增,slub内存分配器_哔哩哔哩_bilibili
13 slab、slob和slub分配器_哔哩哔哩_bilibili
https://www.youtube.com/watch?v=4UucTCiQLXE