首发于linux内核
非专业理解slub

非专业理解slub

1. slub的管理

slub overview

在slab中,struct kmem_cache是管理每种缓存的数据结构。linux为了实现kmem_cache的实例也由slab管理,对kmem_cache进行了巧妙的初始化,而不是简单的静态声明kmem_cache缓存。

我们先来看一下slub缓存数据结构的组织关系:

图1.1 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初始化的关键流程是:

  1. 定义全局指针kmem_cache和kmem_cache_node 。
  2. 定义局部静态变量boot_kmem_cache和boot_kmem_cache_node。
  3. 使两个全局指针指向两个静态局部变量。
  4. 初始化boot_kmem_cache_node和boot_kmem_cache。
  5. 从boot_kmem_cache_node和boot_kmem_cache两个缓存中分别分配实例,并把boot_kmem_cache_node和boot_kmem_cache的内存拷贝到两个动态分配的实例中。然后分别链接到slab_caches全局缓存链表中。也就是kmem_cache的第一个实例管理着自己的kmem_cache缓存,kmem_cache_node一样。
  6. 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变量,用于快速分配对象的管理。完整过程大概如下图所示:

图1.2 slab缓存的创建过程

通用缓存的创建

通用缓存也是在内核初始化时调用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系统中。如图所示:

图1.3 删除一中slab缓存的过程

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并且设置了某些标志的对象内容布局。实时上该选项默认打开。

图2.1 打开CONFIG_SLUB_DEBUG并且设置了某些标志的对象内容布局

关闭CONFIG_SLUB_DEBUG选项时对象内存布局:

图2.2 关闭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中的空闲对象组织如下图:

图2.3 分配一个新的cache后,空闲对象的组织

分配对象的时候直接取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 分配对象

2.4 对象的释放

我们通过上文可以知道,内核通过kmem_cache_free()或kfree()函数释放对象到slab中,它们最终都调用slab_free()完成对象的释放。然而slab_free()只是一个封装函数,实际是do_slab_free()真正完成对象的释放过程。大致的过程如下:

图2.5释放对象

2.5 有可能触发slab缓存的释放

在释放一个对象的时候,如果发现对象所在的slab中inuse成员为0,也就是所有对象都是空闲状态,并且当前node中的nr_partial大于等于kmem_cache中的min_partial,也就是该类slab缓存已经有至少min_partial个slab,那么可以把当前对象所在的slab所占用的页返还给buddy系统,这是通过discard_slab()函数完成的。大概流程如下:

图2.6 释放一个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的地址,如图:

图3.1 cpu部分满链表

kmem_cache_node中的部分满slab的管理

每个node中的部分满slab的链表组织方式与cpu_slab中有所不同,在cpu_slab的部分满链表是用next指针指向下一个slab的,它是单向链表。而node中是用struct slab中的list_head类型的slab_list成员组织成双向循环链表,如下图所示:

图3.2 node部分满链表

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()的工作流程大概如下图所示:

图4.1 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回收的流程大概如下图所示:

图4.2 shrink_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

具体细节参考:

Struct slab comes to 5.17

[GIT PULL] slab for 5.17

上文中有的描述使用了page,有的描述使用了slab,其实它们的作用是一样的。

参考:

git://git.kernel.org/pub/scm/ 5.17.0
Slab Allocator
The Slab Allocator in the Linux kernel
events.static.linuxfound.org
《linux技术内幕》罗秋明 著
Yann:Linux内存管理:slub分配器
编辑于 2022-03-07 22:00