非专业理解slub
1. slub的管理
slub overview
在slab中,struct kmem_cache是管理每种缓存的数据结构。linux为了实现kmem_cache的实例也由slab管理,对kmem_cache进行了巧妙的初始化,而不是简单的静态声明kmem_cache缓存。
我们先来看一下slub缓存数据结构的组织关系:
内核中所有缓存通过list_head类型的slab_caches全局变量,链接成一个双向链表。每创建一个新的slab缓存,则分配一个kmem_cache对象,并通过list成员链接到slab_caches链表中。然后,我们先看一下struct kmem_cache的定义:
struct kmem_cache {
/* percpu变量,由于每个cpu分配对象减少cpu直接的竞争 */
struct kmem_cache_cpu __percpu *cpu_slab;
/* Used for retrieving partial slabs, etc. */
slab_flags_t flags;
/* 部分满slab的最少数量 */
unsigned long min_partial;
/* 包含元数据的对象大小 */
unsigned int size; /* The size of an object including metadata */
/* 不包含元数据的对象大小 */
unsigned int object_size;/* The size of an object without metadata */
struct reciprocal_value reciprocal_size;
/* 空闲对象的起始地址+offset就是FP的值,也就是下一个空闲对象的地址 */
unsigned int offset; /* Free pointer offset */
#ifdef CONFIG_SLUB_CPU_PARTIAL
/* Number of per cpu partial objects to keep around */
unsigned int cpu_partial;
/* Number of per cpu partial pages to keep around */
unsigned int cpu_partial_pages;
#endif
struct kmem_cache_order_objects oo;
/* Allocation and freeing of slabs */
struct kmem_cache_order_objects max;
struct kmem_cache_order_objects min;
gfp_t allocflags; /* gfp flags to use on each alloc */
int refcount; /* Refcount for slab cache destroy */
void (*ctor)(void *);
unsigned int inuse; /* Offset to metadata */
unsigned int align; /* Alignment */
unsigned int red_left_pad; /* Left redzone padding size */
const char *name; /* Name (only for display!) */
/* 用于链接到全局链表slab_caches上 */
struct list_head list; /* List of slab caches */
#ifdef CONFIG_SYSFS
struct kobject kobj; /* For sysfs */
#endif
##ifdef CONFIG_SLAB_FREELIST_HARDENED
unsigned long random;
#endif
#ifdef CONFIG_NUMA
/*
* Defragmentation by allocating from a remote node.
*/
unsigned int remote_node_defrag_ratio;
#endif
#ifdef CONFIG_SLAB_FREELIST_RANDOM
unsigned int *random_seq;
#endif
#ifdef CONFIG_KASAN
struct kasan_cache kasan_info;
#endif
unsigned int useroffset; /* Usercopy region offset */
unsigned int usersize; /* Usercopy region size */
/* 对应系统中每个node的部分满slab */
struct kmem_cache_node *node[MAX_NUMNODES];
};
这里我们只关心cpu_slab和node[MAX_NUMNODES],因为缓存初始化时主要是对这两个结构进行分配和初始化。slab初始化的核心问题就是我们想要在kmem_cache缓存中分配一个kmem_cache实例去创建新的缓存,那么kmem_cache本身的slab是怎么创建的呢?
slub的初始化
我们通过读代码得知,在slab中声明了两个struct kmem_cache类型的全局指针kmem_cache_node 和 kmem_cache,分别用来指向管理kmem_cache的缓存和管理kmem_cache_node的缓存。那么这两个缓存是如何创建的呢?我们可以通过分析kmem_cache_init()函数来了解kmem_cache_node 和 kmem_cache缓存的初始化过程。
首先中定义两个全局变量指针:
static struct kmem_cache *kmem_cache_node; ------ mm/slub.c
struct kmem_cache *kmem_cache; ------ mm/slab_common.c
其中kmem_cache_node是在slub中专用的,所以用static关键字所修饰。而kmem_cache指针在slub/slab/slob中都会用到,所有作用域为全局。
下面是kmem_cache_init()函数的实现,代码中的注释详细解释了slub初始化的过程:
void __init kmem_cache_init(void)
{
/* 创建两个局部静态变量用来初始化缓存的基础数据结构kmem_cache和kmem_cache_node。它们用__initdata
* 进行修饰,说明只在内核初始化的时候才有效。
*/
static __initdata struct kmem_cache boot_kmem_cache,
boot_kmem_cache_node;
int node;
if (debug_guardpage_minorder())
slub_max_order = 0;
/* Print slub debugging pointers without hashing */
if (__slub_debug_enabled())
no_hash_pointers_enable(NULL);
/* 让两个全局变量指向静态局部变量 */
kmem_cache_node = &boot_kmem_cache_node;
kmem_cache = &boot_kmem_cache;
/*
* Initialize the nodemask for which we will allocate per node
* structures. Here we don't need taking slab_mutex yet.
*/
for_each_node_state(node, N_NORMAL_MEMORY)
node_set(node, slab_nodes);
/* 先创建kmem_cache_node缓存,因为在下面创建kmem_cache缓存的时候要分配
* kmem_cache_node实例用来填充node数组。
* 这里还有一个问题是在创建kmem_cache_node缓存时如何填充该缓存中的node数组呢?
* 这需要early_kmem_cache_node_alloc()函数来完成,下面会说到。
*/
create_boot_cache(kmem_cache_node, "kmem_cache_node",
sizeof(struct kmem_cache_node), SLAB_HWCACHE_ALIGN, 0, 0);
register_hotmemory_notifier(&slab_memory_callback_nb);
/* Able to allocate the per node structures */
/* 设置slab的全局状态,PARTIAL意味着可以从kmem_cache_node缓存中获取对象了。*/
slab_state = PARTIAL;
/* 创建kmem_cache缓存。*/
create_boot_cache(kmem_cache, "kmem_cache",
offsetof(struct kmem_cache, node) +
nr_node_ids * sizeof(struct kmem_cache_node *),
SLAB_HWCACHE_ALIGN, 0, 0);
/* 此时kmem_cache和kmem_cache_node两个缓存都创建好了,但是它们的数据存储在本地
* 静态局部变量boot_kmem_cache和boot_kmem_cache_node中。bootstrap()函数从这两个
* 缓存中分别再动态分配一个实例,然后复制两个静态局部变量的内容到动态分配的实例中。bootstrap()
* 函数执行完成后两个静态局部变量就被放弃了,还因为它们只是用于初始化的变量,所以必须
* 被丢弃。
*/
kmem_cache = bootstrap(&boot_kmem_cache);
kmem_cache_node = bootstrap(&boot_kmem_cache_node);
/* Now we can use the kmem_cache to allocate kmalloc slabs */
/* 这时候可以通过前面分配的kmem_cache和kmem_cache_node两个缓存创建通用缓存了。*/
setup_kmalloc_cache_index_table();
create_kmalloc_caches(0);
/* Setup random freelists for each cache */
init_freelist_randomization();
cpuhp_setup_state_nocalls(CPUHP_SLUB_DEAD, "slub:dead", NULL,
slub_cpu_dead);
/* 宣告slab初始化完成 */
pr_info("SLUB: HWalign=%d, Order=%u-%u, MinObjects=%u, CPUs=%u, Nodes=%u\n",
cache_line_size(),
slub_min_order, slub_max_order, slub_min_objects,
nr_cpu_ids, nr_node_ids);
}
early_kmem_cache_node_alloc()函数如何完成kmem_cache_node缓存的创建工作?
static void early_kmem_cache_node_alloc(int node)
{
struct page *page;
struct kmem_cache_node *n;
BUG_ON(kmem_cache_node->size < sizeof(struct kmem_cache_node));
/* 分配一个新页 */
page = new_slab(kmem_cache_node, GFP_NOWAIT, node);
BUG_ON(!page);
if (page_to_nid(page) != node) {
pr_err("SLUB: Unable to allocate memory from node %d\n", node);
pr_err("SLUB: Allocating a useless per node structure in order to be able to continue\n");
}
n = page->freelist;
BUG_ON(!n);
/* 打开调试的情况下,初始化slab中的调试内容 */
#ifdef CONFIG_SLUB_DEBUG
init_object(kmem_cache_node, n, SLUB_RED_ACTIVE);
init_tracking(kmem_cache_node, n);
#endif
n = kasan_slab_alloc(kmem_cache_node, n, GFP_KERNEL, false);
/* 给page中与slab有关的域赋值 */
page->freelist = get_freepointer(kmem_cache_node, n);
page->inuse = 1;
page->frozen = 0;
/* 新分配的kmem_cache_node对象复制给全局kmem_cache_node缓存 */
kmem_cache_node->node[node] = n;
/* 初始化per-node */
init_kmem_cache_node(n);
inc_slabs_node(kmem_cache_node, node, page->objects);
/*
* No locks need to be taken here as it has just been
* initialized and there is no concurrent access.
*/
/* 如函数名,把新的slab也就是page添加到对应node缓存的部分满链表。这里需要注意的是
* n是从当前的page中分配出来的一个对象,而现在page属于部分满slab,由缓存对象n进行
* 管理。
*/
__add_partial(n, page, DEACTIVATE_TO_HEAD);
}
bootstrap()函数的实现:
static struct kmem_cache * __init bootstrap(struct kmem_cache *static_cache)
{
int node;
/* 通过create_boot_cache()函数创建的全局kmem_cache缓存可以分配对象了,这里直接分配一个kmem_cache对象 */
struct kmem_cache *s = kmem_cache_zalloc(kmem_cache, GFP_NOWAIT);
struct kmem_cache_node *n;
/* 把boot_kmem_cache的内容复制到新分配的kmem_cache对象中 */
memcpy(s, static_cache, kmem_cache->object_size);
/*
* This runs very early, and only the boot processor is supposed to be
* up. Even if it weren't true, IRQs are not up so we couldn't fire
* IPIs around.
*/
/* 把per-cpu的slab放回到kmem_cache_node中的部分满链表,因为cpu_slab变量中的page
* 指向boot_kmem_cache,如果执行list_for_each_entry(p, &n->partial, slab_list)
* 此时partial中是空的,page没有指向正确的slab缓存。
*/
__flush_cpu_slab(s, smp_processor_id());
for_each_kmem_cache_node(s, node, n) {
struct page *p;
list_for_each_entry(p, &n->partial, slab_list)
p->slab_cache = s;
#ifdef CONFIG_SLUB_DEBUG
list_for_each_entry(p, &n->full, slab_list)
p->slab_cache = s;
#endif
}
/* 把kmem_cache缓存添加到全局链表 */
list_add(&s->list, &slab_caches);
return s;
}
直到最后一个动作list_add(&s->list, &slabcaches),缓存的缓存就完全创建好了。然后其他缓存在创建时就可以直接在前面两个缓存中分配对象,建立自己的管理数据了。比如kmalloc的缓存就是在slub初始化后马上创建的。
所以slub初始化的关键流程是:
- 定义全局指针kmem_cache和kmem_cache_node 。
- 定义局部静态变量boot_kmem_cache和boot_kmem_cache_node。
- 使两个全局指针指向两个静态局部变量。
- 初始化boot_kmem_cache_node和boot_kmem_cache。
- 从boot_kmem_cache_node和boot_kmem_cache两个缓存中分别分配实例,并把boot_kmem_cache_node和boot_kmem_cache的内存拷贝到两个动态分配的实例中。然后分别链接到slab_caches全局缓存链表中。也就是kmem_cache的第一个实例管理着自己的kmem_cache缓存,kmem_cache_node一样。
- kmem_cache和kmem_cache_node 分别指向5中动态分配的实例,至此slub创建过程完毕。
slab缓存的创建
linux内核中用来创建slab的接口是kmem_cache_create(),其原型如下:
struct kmem_cache *
kmem_cache_create(const char *name, unsigned int size, unsigned int align,
slab_flags_t flags, void (*ctor)(void *))
参数的含义分别是:
- name - 用于标识缓存名称的字符串,会显示在/proc/slabinfo中
- size - 缓存中对象的大小
- align - 对象对齐字节数
- flags - SLAB标志,指示slab的状态或者行为,下面列出了slab中全部的标志:
SLAB_HWCACHE_ALIGN - 对象按硬件缓存行对齐
SLAB_CACHE_DMA - 从DMA区分配内存
SLAB_CACHE_DMA32 - 从DMA32区分配内存
SLAB_PANIC - 创建缓存失败时出发panic
SLAB_TYPESAFE_BY_RCU - 通过RCU释放slab
SLAB_MEM_SPREAD - 可以在远端node分配对象
SLAB_ACCOUNT - 用memcg进行记账
SLAB_RECLAIM_ACCOUNT - slab缓存可回收
SLAB_TEMPORARY - SLAB_RECLAIM_ACCOUNT的重定义
SLAB_DEACTIVATED - 未使用
* 用于DEBUG的slab标志 *
SLAB_CONSISTENCY_CHECKS - 在alloc/free时进行检查
SLAB_RED_ZONE - 在对象中加入红色区域
SLAB_POISON - 在释放后的对象中添加指定内容
SLAB_STORE_USER - 存储最后的对象属主
SLAB_TRACE - 用于跟踪对象的分配和释放
SLAB_DEBUG_OBJECTS - ***************************
SLAB_NOLEAKTRACE - 避免kmemleak进行记录
SLAB_KASAN - KASAN使用
- ctor - 对象的构造函数
kmem_cache_create()执行时,首先调用kmem_cache_zalloc()函数从上面创建的kmem_cache缓存中分配一个对象用来管理当前要创建的slab,并对其进行初始化。通过调用calculate_sizes()计算对象+对齐+调试元数据+pad所占内存的大小,赋值给kmem_cache的size成员。然后调用init_kmem_cache_nodes()分配kmem_cache_node缓存中的对象,用来以node为单位对slab部分满链表进行管理。紧接着调用alloc_kmem_cache_cpus()分配per cpu变量,用于快速分配对象的管理。完整过程大概如下图所示:
通用缓存的创建
通用缓存也是在内核初始化时调用kmem_cache_init()完成的,它调用create_kmalloc_caches()函数完成实际的创建过程,所用的kmalloc缓存保存在kmalloc_caches二维数组中,该数组定义如下:
struct kmem_cache *
kmalloc_caches[NR_KMALLOC_TYPES][KMALLOC_SHIFT_HIGH + 1] __ro_after_init =
{ /* initialization for gas warning: ignoring changed section attributes */ };
EXPORT_SYMBOL(kmalloc_caches);
kmalloc_caches数组的第一维表示kmalloc缓存的类型数量,几种类型值定义如下:
enum kmalloc_cache_type {
/* 普通的kmalloc */
KMALLOC_NORMAL = 0,
#ifndef CONFIG_ZONE_DMA
/* 系统中不包含DMA区时,DMA专用的通用缓存类型 */
KMALLOC_DMA = KMALLOC_NORMAL,
#endif
#ifndef CONFIG_MEMCG_KMEM
KMALLOC_CGROUP = KMALLOC_NORMAL,
#else
/* 用于cgroup记账的kmalloc */
KMALLOC_CGROUP,
#endif
/* 可回收的kmalloc */
KMALLOC_RECLAIM,
#ifdef CONFIG_ZONE_DMA
/* 系统中有DMA区时,DMA专有的通用缓存类型 */
KMALLOC_DMA,
#endif
/* kmalloc的类型数量,也就是kmalloc_caches第一维数组个数 */
NR_KMALLOC_TYPES
};
数组的第二维是2的幂次,最小值为KMALLOC_SHIFT_LOW,在slub中是3;最大值为KMALLOC_SHIFT_HIGH,在slub中是PAGESHIFT+1,x86中PAGESHIFT为12,则KMALLOC_SHIFT_HIGH的值是13, 所以kmalloc缓存的字节数取值范围是2^3 ~ 2^13字节。在内核使用slub作为slab时,如果分配大于8096字节的slab缓存时则直接从伙伴系统接进行分配,不归slub管理。其中kmalloc的size信息可以通过kmalloc_info数组查找,其定义如下:
#define INIT_KMALLOC_INFO(__size, __short_size) \
{ \
.name[KMALLOC_NORMAL] = "kmalloc-" #__short_size, \
.name[KMALLOC_RECLAIM] = "kmalloc-rcl-" #__short_size, \
KMALLOC_CGROUP_NAME(__short_size) \
KMALLOC_DMA_NAME(__short_size) \
.size = __size, \
}
/*
* kmalloc_info[] is to make slub_debug=,kmalloc-xx option work at boot time.
* kmalloc_index() supports up to 2^25=32MB, so the final entry of the table is
* kmalloc-32M.
*/
const struct kmalloc_info_struct kmalloc_info[] __initconst = {
INIT_KMALLOC_INFO(0, 0),
INIT_KMALLOC_INFO(96, 96),
INIT_KMALLOC_INFO(192, 192),
INIT_KMALLOC_INFO(8, 8),
INIT_KMALLOC_INFO(16, 16),
INIT_KMALLOC_INFO(32, 32),
INIT_KMALLOC_INFO(64, 64),
INIT_KMALLOC_INFO(128, 128),
INIT_KMALLOC_INFO(256, 256),
INIT_KMALLOC_INFO(512, 512),
INIT_KMALLOC_INFO(1024, 1k),
INIT_KMALLOC_INFO(2048, 2k),
INIT_KMALLOC_INFO(4096, 4k),
INIT_KMALLOC_INFO(8192, 8k),
INIT_KMALLOC_INFO(16384, 16k),
INIT_KMALLOC_INFO(32768, 32k),
INIT_KMALLOC_INFO(65536, 64k),
INIT_KMALLOC_INFO(131072, 128k),
INIT_KMALLOC_INFO(262144, 256k),
INIT_KMALLOC_INFO(524288, 512k),
INIT_KMALLOC_INFO(1048576, 1M),
INIT_KMALLOC_INFO(2097152, 2M),
INIT_KMALLOC_INFO(4194304, 4M),
INIT_KMALLOC_INFO(8388608, 8M),
INIT_KMALLOC_INFO(16777216, 16M),
INIT_KMALLOC_INFO(33554432, 32M)
};
通过代码可知,通用缓存的创建和kmem_cache缓存一样,也是本质上也是通过create_boot_cache()函数完成的。
删除slab缓存
彻底删除一个slab缓存是通过kmem_cache_destroy()函数完成的,一般内核模块在被卸载的时候要删除它所创建的slab缓存,该函数原型如下:
void kmem_cache_destroy(struct kmem_cache *s);
参数s指向要删除的缓存。
删除一个slab缓存时会先把cpu_slab指向的slab归还给node,然后尝试释放所有slab中的对象。如果所有对象都被释放的话,则释放page到buddy系统中。如图所示:
2. object/对象
2.1 对象布局
object在slub中的布局分为两种,一种是已分配出去的object布局,每个size大小的内存块中保存对象内容+其他数据;另一种是未分配状态下的布局,每个size大小的内存中放置FP指针值 + 其他数据。其他数据包括,设置SLAB_POISON标志时,在free状态下Payload部分要放poison内容,包括0x5a,0x6b和0xa5;设置SLAB_RED_ZONE标志时,RedZone存储0xbb或者0xcc,根据对象分配与否而定。设置SLAB_STORE_USER标志时,Tracking跟踪分配和释放的信息。
打开CONFIG_SLUB_DEBUG并且设置了某些标志的对象内容布局。实时上该选项默认打开。
关闭CONFIG_SLUB_DEBUG选项时对象内存布局:
FP(Free Pointer)放在对象之后的情形:
- 设置了SLAB_TYPESAFE_BY_RCU标志,也就是通过RCU方式释放对象。
- 设置了SLAB_POISON标志,释放对象以后payload的部分要填充预期的内容。
- 设置了SLAB_RED_ZONE标志,并且object的大小小于机器字长所占用的字节数。
- 该slab中需要通过构造函数进行初始化,说明某个模块会在object中填充指定的内容。
如果不属于上面任何一种情况,则FP放在对象Payload大概中间的位置,下面是缓存中offset成员的计算方法:
static int calculate_sizes(struct kmem_cache *s, int forced_order)
{
......
if ((flags & (SLAB_TYPESAFE_BY_RCU | SLAB_POISON)) ||
((flags & SLAB_RED_ZONE) && s->object_size < sizeof(void *)) ||
s->ctor) {
/*
* Relocate free pointer after the object if it is not
* permitted to overwrite the first word of the object on
* kmem_cache_free.
*
* This is the case if we do RCU, have a constructor or
* destructor, are poisoning the objects, or are
* redzoning an object smaller than sizeof(void *).
*
* The assumption that s->offset >= s->inuse means free
* pointer is outside of the object is used in the
* freeptr_outside_object() function. If that is no
* longer true, the function needs to be modified.
*/
s->offset = size;
/* FP放在对象对齐之后的位置 */
size += sizeof(void *);
} else {
/*
* Store freelist pointer near middle of object to keep
* it away from the edges of the object to avoid small
* sized over/underflows from neighboring allocations.
*/
/* FP放在payload接近中间的位置 */
s->offset = ALIGN_DOWN(s->object_size / 2, sizeof(void *));
}
......
}
2.2 对象的存储
slab中未分配的对象通过每个对象中的FP(freepointer)形成一个链表。链表的建立是在allocate_slab()函数中分配内存页后建立的,下面代码是allocate_slab()的实现:
static struct slab *allocate_slab(struct kmem_cache *s, gfp_t flags, int node)
{
......
//分配内存
slab = alloc_slab_page(s, alloc_gfp, node, oo);
......
/* 在打开CONFIG_SLAB_FREELIST_RANDOM的情况下建立随机空闲对象链表 */
shuffle = shuffle_freelist(s, slab);
/* 否则从低地址开始依次链接slab中的空闲对象 */
if (!shuffle) {
start = fixup_red_left(s, start);
/* 填充对象中的debug内存空间,例如poison,redzone等 */
start = setup_object(s, slab, start);
/* freelist指向第一个空闲对象 */
slab->freelist = start;
for (idx = 0, p = start; idx < slab->objects - 1; idx++) {
next = p + s->size;
/* 填充对象中的debug内存空间,例如poison,redzone等 */
next = setup_object(s, slab, next);
/* 把当前对象中的FP指向下一个对象的起始位置 */
set_freepointer(s, p, next);
p = next;
}
/* 设置最后一个空闲对象的FP为NULL */
set_freepointer(s, p, NULL);
}
......
return slab;
}
所以在分配新的slab时,假设不使能CONFIG_SLAB_FREELIST_RANDOM,则page中的空闲对象组织如下图:
分配对象的时候直接取slab->freelist或cpu_slab->freelist的值,然后freelist赋值为当前对象中的FP的值,也就是下一个空闲对象。
2.3 对象的分配
内核通过下面函数完成对象的分配和释放:
/* 分配slab中的一个空闲对象 */
void *kmem_cache_alloc(struct kmem_cache *s, gfp_t gfpflags);
/* 在指定node上分配slab中的一个空闲对象 */
void *kmem_cache_alloc_node(struct kmem_cache *s, gfp_t gfpflags, int node)
/* 释放对象 */
void kmem_cache_free(struct kmem_cache *s, void *x);
/* 分配指定size的通用对象 */
static __always_inline __alloc_size(1) void *kmalloc(size_t size, gfp_t flags)
/* 在指定node上分配指定size的通用对象 */
static __always_inline __alloc_size(1) void *kmalloc_node(size_t size, gfp_t flags, int node)
/* 释放通用对象 */
void kfree(const void *x)
kmem_cache_alloc()实际是调用slab_alloc()函数完成对象的分配,而slab_alloc()调用slab_alloc_node()完成真正的分配过程。slub中分配对象的大致流程如下图:
2.4 对象的释放
我们通过上文可以知道,内核通过kmem_cache_free()或kfree()函数释放对象到slab中,它们最终都调用slab_free()完成对象的释放。然而slab_free()只是一个封装函数,实际是do_slab_free()真正完成对象的释放过程。大致的过程如下:
2.5 有可能触发slab缓存的释放
在释放一个对象的时候,如果发现对象所在的slab中inuse成员为0,也就是所有对象都是空闲状态,并且当前node中的nr_partial大于等于kmem_cache中的min_partial,也就是该类slab缓存已经有至少min_partial个slab,那么可以把当前对象所在的slab所占用的页返还给buddy系统,这是通过discard_slab()函数完成的。大概流程如下:
结合"2.4 对象的释放"一节,可以完整 了解该过程。
2.6 kfence分配对象
未完待续...
3. slub的部分满链表
cpu_slab部分满slab的管理
cpu_slab中的部分满slab使用partial成员作为链表头,主要用于当cpu_slab->slab所指向的slab没有空闲对象的时候,会从cpu_slab->partial链表中取出一个部分满slab完成对象的分配。该部分满链表通过struct slab结构体成员next指向下一个slab的地址,如图:
kmem_cache_node中的部分满slab的管理
每个node中的部分满slab的链表组织方式与cpu_slab中有所不同,在cpu_slab的部分满链表是用next指针指向下一个slab的,它是单向链表。而node中是用struct slab中的list_head类型的slab_list成员组织成双向循环链表,如下图所示:
4. slab的回收
slab的内存回收通过两个途径完成,一个是通过kmem_cache_shrink()函数,另外一个是通过shrink_slab()函数进行回收。
kmem_cache_shrink()的调用主要是在sysfs中通过写入字符'1'到每个缓存的shrink文件中。例如,用户要回收dentry的slab缓存,则执行下面命令:
echo 1 > /sys/kernel/slab/dentry/shrink
然而,该文件的读写只能通过超级用户root完成,所以普通用户无法使用该途径。kmem_cache_shrink()的工作流程大概如下图所示:
结合“2.5 有可能触发slab缓存的释放”一节中的流程图可以得到整个slab释放的过程。
而shrink_slab()的触发有两条途径,一个是通过执行 "echo 2 > /proc/sys/vm/drop_caches"调用该函数,另一个是内核主动回收内存时会调用到shrink_slab()。但是,shrink_slab()的运行原理是通过内核组件注册的struct shrinker实例完成的,而shrinker并不是专门用于slab内存回收,所以shrink_slab()并不保证回收slab缓存所占用的内存。通过drop_caches的slab回收的流程大概如下图所示:
内核进行内存回收时调用shrink_slab()的可能的原因有两个:一个是内核某个子系统分配页时资源不够,则触发内存回收机制;第二个是内核线程kswapd进行内存交换时触发。
5. 寻找相似的slab
在某内核组件创建新的缓存时,slub可以为该请求寻找已经存在的、对象大小相似的、flags相似的slab作为请求的slab,如果匹配的话则降低了管理成本。可以通过__kmem_cache_alias()函数实现该功能,其原型为:
struct kmem_cache *
__kmem_cache_alias(const char *name, unsigned int size, unsigned int align,
slab_flags_t flags, void (*ctor)(void *))
参数的含义如下:
- name - 要创建的缓存名称
- size - 对象大小
- align - 对象对齐字节数
- flags - slab标志
- ctor - 对象构造函数
该函数通过find_mergeable()函数查找可以共用的缓存,如果匹配,则返回找到的slab缓存,否则返回NULL。这里需要注意的是对象大小size要小于相似缓存的对象大小,否则内存空间不够存放用户所请求的对象的内容。
目前__kmem_cache_alias()的唯一用户是kmem_cache_create_usercopy()函数,它是用于创建支持安全检查的slab缓存。usercopy的含义是在用户空间和内核空间进行内存拷贝的时候执行安全检查,防止过多的内核内存数据暴露给用户空间。
6. 调试slub
通过内核命令行参数slub_debug进行调试
slub调试功能通过CONFIG_SLUB_DEBUG选项打开,但是在有用户选择使用slub(对于slab, slob而言)作为小块内存分配管理器时,CONFIG_SLUB_DEBUG是默认打开的。所以一般在不重新编译内核的情况下,通过命令行参数slub_debug就可以进行slub调试。目前slub中有如下几个运行时调试选项:
- F - sanity检查,主要是检查slab本身、对象redzone、对象poison、FP和对象地址等各方面在分配和释放对象 时的合法性。打开该选项时slab flags会添加SLAB_CONSISTENCY_CHECKS标志。
- Z - redzone检查,也就是检查object的数据是否超过规定的大小。打开该选项时slab flags会添加SLAB_RED_ZONE标志。它是'F'选项的一个子功能。
- P - poison检查,检查object是否被正确使用。该选项对应SLAB_POISON标志。它是'F'选项的一个子功能。
- U - 用户跟踪。记录分配/释放对象的一些信息,例如调用kmem_cache_alloc函数的虚拟地址,pid等信息。该选项对应SLAB_STORE_USER标志。
- T - 分配或释放对象时打印slab的状态。该选项对应SLAB_TRACE标志。
- A - 未看到代码中有什么实际用途。该选项对应SLAB_FAILSLAB标志。
- O - 当对象的尺寸+ 调试信息的尺寸超过了slab所需要的原有page的order,则关闭调试。我猜应该是避免内存浪费。
- '-' - 关闭上面所有的调试选项。
slabtop
slabtop工具可以实时查看slab缓存的状态,类似top命令实时刷新。常用的选项如下:
- -d N 设置刷新间隔,以秒为单位
- -s S 是排序方式,可以使用下面列表的值:
character description header
a 活跃对象数量 ACTIVE
b 每个slab中对象数量 OBJ/SLAB
c 缓存大小 CACHE SIZE
l slab数量 SLABS
v 活跃slab数量 N/A
n slab名称 NAME
o slab中所有对象的数量 OBJS
p 每个slab中的页数 N/A
s 对象大小 OBJ SIZE
u 对象使用率 USE
- -o oneshot模式,也就是一次性显示slab状态。
- -h 帮助列表。
slabinfo
该工具的源码位于内核代码tools/vm/slabinfo.c。它用于显示slab缓存的统计信息。这些统计信息来自于/sys/kernel/slab目录。
slub in debugfs
在debugfs中,可以为slub提供slab的用户信息,该功能对应SLAB_STORE_USER标志。在使能CONFIG_DEBUG_FS选项后,系统运行时可以在/sys/kernel/debug/slab/<cache>/ 目录下查看对应slab缓存的相关用户信息,例如我们为anon_vma的slab缓存添加SLAB_STORE_USER标志,使其出现在debugfs中。我们可以通过在命令行设置slub_debug参数完成:
slub_debug=U,anon_vma
在运行时执行"cat /sys/kernel/debug/slab/anon_vma/alloc_traces":
~# cat /sys/kernel/debug/slab/anon_vma/alloc_traces
19 anon_vma_fork+0x59/0x140 age=221998/221998/221998 pid=136 cpus=0 nodes=0
76 __anon_vma_prepare+0xdd/0x160 age=6/196463/222989 pid=1-309 cpus=0-3 nodes=0-1
显示的内容包括对象的数量、分配对象的函数、最小/平均/最大的jiffies的值、分配该类对象的pid范围、cpu掩码和node掩码。
/sys/kernel/debug/slab/anon_vma/free_traces的输出信息类似。
7. 最新动态
在Linux 5.17的merge窗口,slab迎来一次重大更新。为了减少page实例在系统中占用过多内存并且提高struct page的可读性,Matthew Wilcox和slab的maintainer之一Vlastimil Babka合作将struct page中跟slab缓存管理相关的成员全部移除。他们创建了一个定义为struct slab的全新结构体用来对slab缓存进行管理,该结构体的定义如下:
struct slab {
unsigned long __page_flags;
#if defined(CONFIG_SLAB)
union {
struct list_head slab_list;
struct rcu_head rcu_head;
};
struct kmem_cache *slab_cache;
void *freelist; /* array of free object indexes */
void *s_mem; /* first object */
unsigned int active;
#elif defined(CONFIG_SLUB)
union {
struct list_head slab_list;
struct rcu_head rcu_head;
#ifdef CONFIG_SLUB_CPU_PARTIAL
struct {
struct slab *next;
int slabs; /* Nr of slabs left */
};
#endif
};
struct kmem_cache *slab_cache;
/* Double-word boundary */
void *freelist; /* first free object */
union {
unsigned long counters;
struct {
unsigned inuse:16;
unsigned objects:15;
unsigned frozen:1;
};
};
unsigned int __unused;
#elif defined(CONFIG_SLOB)
struct list_head slab_list;
void *__unused_1;
void *freelist; /* first free block */
long units;
unsigned int __unused_2;
#else
#error "Unexpected slab allocator configured"
#endif
atomic_t __page_refcount;
#ifdef CONFIG_MEMCG
unsigned long memcg_data;
#endif
};
结构体中每个成员的含义跟struct page中的slab管理相关成员的含义是基本对应的。主要的区别是为了改善结构体的可读性,用宏CONFIG_SLAB/CONFIG_SLUB/CONFIG_SLOB对slab/slub/slob所对应的不同成员的进行了编译分割。比如,在原先的struct page中,用于slub的cpu_slab部分满slab管理的匿名结构体如下:
struct { /* Partial pages */
struct page *next;
#ifdef CONFIG_64BIT
int pages; /* Nr of pages left */
#else
short int pages;
#endif
};
而在struct slab结构体中只有在CONFIG_SLUB_CPU_PARTIAL选项被设置的时候才定义:
#ifdef CONFIG_SLUB_CPU_PARTIAL
struct {
struct slab *next;
int slabs; /* Nr of slabs left */
};
#endif
具体细节参考:
上文中有的描述使用了page,有的描述使用了slab,其实它们的作用是一样的。
参考:
git://http://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git 5.17.0
Slab Allocator
The Slab Allocator in the Linux kernel
https://events.static.linuxfound.org/sites/events/files/slides/slaballocators.pdf
《linux技术内幕》罗秋明 著
Yann:Linux内存管理:slub分配器