Linux 内核 slab 分配器

slab 介绍

内存分配与释放向来是程序设计中的难点,在C语言中有malloc可以方便使用,但还是有更多的针对具体业务的实现。内存分配需要考虑分配效率和内存使用率的平衡,当用户申请一块内存,程序需要在空闲内存中查找合适的内存块返回,这里可以选择一个最快获取的算法(first-fit),或者最优选择(best-fit)。二者往往无法两全。另外需要考虑一个根本性的问题就是内存碎片,释放内存时往往与申请顺序不同,会在内存中留下大量的空洞,当有大量不连续的小内存即碎片出现时,便会产生空闲内存很多但是无法分配大块内存的情况。对于操作系统,需要长时间运行,内存碎片的问题便显得非常突出了。

Linux内核一种分配内存的方式是alloc_pages,使用伙伴算法,但这个只适应于按页分配,如果需要几个字节的内存都分配4k,显然会是极大的浪费,所以内核又提供了一种小内存的分配方式,称之为slab,另外还有两种可选的分配方式,slob和slub,slob是为了在嵌入式环境中使用,slub在一些大型设备中币slab有更优秀的性能。但作为程序开发者来说,无需关心内核选用了哪种分配方式,三者具有相同的接口,调用者可以忽略其中的差别。

因为在内核中需要频繁分配同类型的对象,比如频繁分配socket,task_struct等结构体对象,slab一个核心思想就是一个slab分配器只分配一种对象,这样做的好处便是每个slab只分配等大小的内存,能够很好的兼顾效率和内存碎片的问题,当然slab最终也是通过alloc_pages按页分配的内存。

直观认识

先来通过命令直观的了解一下slab的情况:

cat /proc/slabinfo
slabinfo - version: 2.1
# name                 : tunables    : slabdata   
kmalloc_dma-512        8      8    512    8    1 : tunables    0    0    0 : slabdata      1      1      0
UDPLITEv6              0      0    704   11    2 : tunables    0    0    0 : slabdata      0      0      0
UDPv6                 11     11    704   11    2 : tunables    0    0    0 : slabdata      1      1      0
...
kmalloc-8          13156  13312      8  512    1 : tunables    0    0    0 : slabdata     26     26      0
kmalloc-192          352    420    192   21    1 : tunables    0    0    0 : slabdata     20     20      0
kmalloc-96         14293  14322     96   42    1 : tunables    0    0    0 : slabdata    341    341      0

可以很直观的看到每种对象的slab分配情况,从名称不难看出每种slab的作用,从名字kmalloc-8等也能猜测出这是调用kmalloc时分配的内存,内存也是按照一定规则递增分配。

结构

slab分配器的结构如下:

slab allocator

每种具体的内存分配器都由kmem_cache表示,多个kmem_cache组成分配器链,kmem_cache又由三部分组成,slabs_full表示已经完全分配出去的slab,slabs_partial表示部分分配,slabs_empty表示未进行分配。具体的实现原理分析可以放到最后,这里可以查看一下如何使用:

// 定义一个slab实例
struct struct kmem_cache * test_cache;
// 创建一个新缓存
struct kmem_cache *kmem_cache_create(const char *, size_t, size_t,
                        unsigned long,
                        void (*)(void *));
// 销毁缓存
void kmem_cache_destroy(struct kmem_cache *); 
// 分配内存
void *kmem_cache_alloc(struct kmem_cache *, gfp_t);
// 释放内存
void kmem_cache_free(struct kmem_cache *, void *);

用法例子

接下来可以写一段代码来体验一下slab的用法,这里在 Linux字符设备框架 的基础上添加代码实现:

#include<linux/init.h> 
#include<linux/module.h> 
 
#include<linux/fs.h> 
#include<linux/types.h> 
#include<linux/cdev.h> 
#include<linux/mm.h> 
#include<linux/sched.h> 
#include<asm/io.h> 
#include<asm/uaccess.h> 
#include<asm/system.h> 
 
#include<linux/device.h> 
 
#define OBJECT_SIZE        27 
#define OBJECT_NUM        10 
dev_t devno; 
struct class * simple_class; 
static struct cdev cdev; 
 
struct kmem_cache * cppbreak_cache; 
 
struct cache_object { 
    void * object; 
    struct list_head list; 
}; 
 
LIST_HEAD(head); 
 
ssize_t simple_write(struct file * filp, 
    const char __user * buf, size_t size, loff_t * ppos) 
{ 
    char command[OBJECT_SIZE] = {}; 
    int num = 0; 
    int i = 0; 
    if (size >= OBJECT_SIZE || size < 3) { 
        return -EFAULT; 
    } 
    if (copy_from_user(command, buf, size)) 
        return -EFAULT; 
    command[size] = '0'; 
 
    printk("Cache name is %s\n", kmem_cache_name(cppbreak_cache)); 
    printk("Cache object size is %d\n", kmem_cache_size(cppbreak_cache)); 
 
    num = simple_strtol(command + 2, NULL, 10); 
 
    printk("command is %s, num=%d\n", command, num); 
    if (command[0] == 'i') { 
        for (i = 0; i < num; i++) { 
            struct cache_object * object = (struct cache_object *)kmem_cache_alloc(cppbreak_cache, GFP_KERNEL); 
            list_add_tail(&object->list, &head); 
        } 
    } 
    else if (command[0] == 'd') { 
        for (i = 0; i < num && !list_empty(&head); i++) { 
            struct cache_object * object = 
                (struct cache_object *)list_entry( 
                    head.next, 
                    struct cache_object, 
                    list); 
            list_del(&object->list); 
            kmem_cache_free(cppbreak_cache, object); 
        } 
    } 
    else { 
        return -EFAULT; 
    } 
    return size; 
} 
 
int simple_open(struct inode * pnode, struct file * pfile) 
{ 
    printk(KERN_INFO "open simple\n"); 
    return 0; 
} 
 
int simple_release(struct inode * pnode, struct file * pfile) 
{ 
    printk(KERN_INFO "close simple\n"); 
    return 0; 
} 
 
static struct file_operations simple_op =  
{ 
    .owner = THIS_MODULE, 
    .write = simple_write, 
    .open = simple_open, 
    .release = simple_release, 
}; 
 
static int __init initialization(void) 
{ 
    int result; 
 
    result = alloc_chrdev_region(&devno, 0, 1, "simple"); 
    if (result < 0) 
        return result; 
 
    cdev_init(&cdev, &simple_op); 
    result = cdev_add(&cdev, devno, 1); 
 
    simple_class = class_create(THIS_MODULE, "simple"); 
    device_create(simple_class, NULL, devno, NULL, "simple"); 
 
    printk(KERN_INFO " init simple\n"); 
 
    cppbreak_cache = kmem_cache_create("cppbreak", OBJECT_SIZE, 0, SLAB_HWCACHE_ALIGN, NULL); 
    if (cppbreak_cache == NULL) { 
        printk(KERN_INFO " kmem_cache_create failed\n"); 
        return -10; 
    } 
    printk(KERN_INFO " kmem_cache_create succ\n"); 
    return result; 
} 
 
static void __exit cleanup(void) 
{ 
    if (cppbreak_cache) { 
        kmem_cache_destroy(cppbreak_cache); 
        printk(KERN_INFO " kmem_cache_destroy succ\n"); 
    } 
    device_destroy(simple_class, devno); 
    class_destroy(simple_class); 
 
    cdev_del(&cdev); 
    unregister_chrdev_region(devno, 1); 
    printk(KERN_INFO " cleanup simple\n"); 
} 
 
module_init(initialization); 
module_exit(cleanup); 
 
MODULE_AUTHOR("cppbreak@gmail.com"); 
MODULE_DESCRIPTION("slab tester"); 
MODULE_VERSION("V0.1"); 
MODULE_LICENSE("Dual BSD/GPL"); 

这里的代码比较简单,modprobe 模块时创建一个文件系统节点和slab分配器,之后便可在echo “i n” > /dev/simple 来分配n个object,echo “d n” > /dev/simple 来释放n个object。关于内核链表的应用,可以查看 内核数据结构-链表。

使用modprobe加载内核之后,便可看到 /proc/slabinfo 增加了 cppbreak_slab 节点,可以看到object_size信息,之后便可通过添加节点删除节点来查看响应的分配数值变化。

[root@localhost module]# insmod simple.ko
[root@localhost module]# cat /proc/slabinfo | grep cppbreak
cppbreak               0      0     32  112    1 : tunables  120   60    0 : slabdata      0      0      0
[root@localhost module]# echo "i 10" > /dev/simple 
[root@localhost module]# cat /proc/slabinfo | grep cppbreak
cppbreak              10    112     32  112    1 : tunables  120   60    0 : slabdata      1      1      0
[root@localhost module]# echo "d 10" > /dev/simple 
[root@localhost module]# cat /proc/slabinfo | grep cppbreak
cppbreak               5    112     32  112    1 : tunables  120   60    0 : slabdata      1      1      0
[root@localhost module]#

这里便介绍了slab的调用方法,具体的细节和实现过程需要下篇分解了。

slab 实现

slab 除了作为内存分配器以外,还有一个重要的功能便是做CPU的高速缓存,slab会把当前cpu释放的对象保存在对应cpu的数组上,这样如果下次还是这个cpu分配相同的对象,直接从对应数组返回,这里利用了cpu的告诉缓存,cpu刚释放的空间仍然存在于它的高速缓存中的概率会很大。

分析slab的具体实现。首先可以浏览一下关键数据结构 kmem_cache 的实现(注意这里结构体的定义有三个分别是slab,slub,slob的实现,slab的实现位于文件 include/linux/slab_def.h)

第一部分:kmem_cache 结构体

根据内核代码注释,kmem_cache分为6个部分,第一部分是 Cache tunables

struct kmem_cache {
/* 1) Cache tunables. Protected by cache_chain_mutex */
        unsigned int batchcount;
        unsigned int limit;
        unsigned int shared;

        unsigned int buffer_size;
        u32 reciprocal_buffer_size;
		...
};

batchcount 表示在per-CPU列表为空的情况下,从缓存的slab获取对象的数目。limit指定per-CPU列表中保存的对象最大数目,如果超过该值,内核会将batchcount个对象返回到slab。这里的参数便是与cpu高速缓存相关的控制参数。

buffer_size表示缓存管理对象的长度。

第二部分:

struct kmem_cache {
		...
/* 2) touched by every alloc & free from the backend */
        unsigned int flags;             /* constant flags */
        unsigned int num;               /* # of objs per slab */
		...
};

num 是每个slab中的object数量。

第三部分:

struct kmem_cache {
		...
/* 3) cache_grow/shrink */
        /* order of pgs per slab (2^n) */
        unsigned int gfporder;

        /* force GFP flags, e.g. GFP_DMA */
        gfp_t allocflags;

        size_t colour;                  /* cache colouring range */
        unsigned int colour_off;        /* colour offset */
        struct kmem_cache *slabp_cache;
        unsigned int slab_size;
        unsigned int dflags;            /* dynamic flags */

        /* constructor func */
        void (*ctor)(void *obj);
		...
};

这里于分配相关的参数,主要的参数是颜色范围。

第四部分:

struct kmem_cache {
		...
/* 4) cache creation/removal */
        const char *name;
        struct list_head list;
        int refcount;
        int object_size;
        int align;
		...
};

这部分存储了缓存的名称和缓存的list_head节点。

第五部分

主要是统计之用,开启了CONFIG_DEBUG_SLAB宏才会生效,与slab分配算法无关。

struct kmem_cache {
		...
/* 5) statistics */
        unsigned long num_active;
        unsigned long num_allocations;
        unsigned long high_mark;
        unsigned long grown;
        unsigned long reaped;
        unsigned long errors;
        unsigned long max_freeable;
        unsigned long node_allocs;
        unsigned long node_frees;
        unsigned long node_overflow;
        atomic_t allochit;
        atomic_t allocmiss;
        atomic_t freehit;
        atomic_t freemiss;

        /*
         * If debugging is enabled, then the allocator can add additional
         * fields and/or padding to every object. size contains the total
         * object size including these internal fields, the following two
         * variables contain the offset to the user object and its size.
         */
        int obj_offset;
		...
};

第六部分

struct kmem_cache {
		...
/* 6) per-cpu/per-node data, touched during every alloc/free */
        /*
         * We put array[] at the end of kmem_cache, because we want to size
         * this array to nr_cpu_ids slots instead of NR_CPUS
         * (see kmem_cache_init())
         * We still use [NR_CPUS] and not [1] or [0] because cache_cache
         * is statically defined, so we reserve the max number of cpus.
         */
        struct kmem_list3 **nodelists;
        struct array_cache *array[NR_CPUS];
		...
};

最后这部分是per-CPU相关变量,array很容易理解,既是每个cpu对应的数组。另外一个成员nodelists存储了 slabs_partial,slabs_fullslabs_free链表,并记录链表空闲节点等信息,这个结构体便是给per-CPU数组提供object的缓存池,注意这里是一个指针的指针,是 kmem_list3 的二维数组,其中第一个维度是与cpu最接近的内存块索引。

创建过程

接下来看kmem_cache节点的创建过程,kmem_cache_create 分为几个部分:

一、检查参数的合理性

if (!name || in_interrupt() || (size < BYTES_PER_WORD) ||             size > KMALLOC_MAX_SIZE) {
        printk(KERN_ERR "%s: Early error in slab %s\n", __func__,
                        name);
        BUG();
}

查看名字是否为空,分配大小是否大于分配的最大值KMALLOC_MAX_SIZE,是否小于CPU字长等,之后进行了去重检测,防止创建相同名称的缓存。

二、计算对齐

if (size & (BYTES_PER_WORD - 1)) {
        size += (BYTES_PER_WORD - 1);
        size &= ~(BYTES_PER_WORD - 1);
}

按照处理器字长对其。之后根据flag 的ARCH参数计算对齐。

三、分配缓存结构

cachep = kmem_cache_zalloc(&cache_cache, gfp);
if (!cachep)
        goto oops;

cache_cache 是缓存的slab分配器,这里创建出一个新的cache实例,存入cachep,这里就会出现一个问题,便是mem_cache的创建需要调用alloc,但是cache_cache是如何分配出来的呢,这里涉及到slab的初始化问题,之后再讨论。

四、确定slab头的存储位置

if ((size >= (PAGE_SIZE >> 3)) && !slab_early_init &&
    !(flags & SLAB_NOLEAKTRACE))
        /*
         * Size is large, assume best to place the slab management obj
         * off-slab (should allow better packing of objs).
         */
        flags |= CFLGS_OFF_SLAB;

如果object大小大于页帧的八分之一,则将管理数据放到slab之外。

五、calculate_slab_order

现在已经确定缓存对象的size,现在需要估算slab的长度,函数calculate_slab_order通过对象size,align和flags计算slab应该使用多少页帧,如果页帧过少则存储的对象过少,增加分配管理的开销,如果页帧过多,则空间过于浪费。

for (gfporder = 0; gfporder

函数循环中gfporder为页帧数量,对指定的页帧,通过函数cache_estimate计算能够存储的数量num以及浪费的空间remainder,计算的公式为: PAGE_SIZE « gfporder = head + num * size + left_over。 如果对象存储在slab之外,则head(mgmt_size)为0,如果对象存储于slab之内,则 head = sizeof(struct slab) + num*sizeof(kmem_bufctl_t)。接着calculate_slab_order之后便是循环退出的各种条件。

六、着色

cachep->colour_off = cache_line_size();
/* Offset must be a multiple of the alignment. */
if (cachep->colour_off < align)                 cachep->colour_off = align;
cachep->colour = left_over / cachep->colour_off;

colour_off 计算颜色偏移,colour颜色数量,紧接着初始化函数对cache的变量赋值。

七、setup_cpu_cache

调用enable_cpucache初始化per-CPU变量。

八、加入全局链表

函数的最后加入全局链表cache_chain

接下来看缓存分配过程,kmem_cache_alloc,函数调用__cache_alloc -> ____cache_alloc 完成主要功能:

ac = cpu_cache_get(cachep);
if (likely(ac->avail)) {
        STATS_INC_ALLOCHIT(cachep);
        ac->touched = 1;
        objp = ac->entry[--ac->avail];
}

如果per-CPU缓存能够使用,则直接从cpu缓存中返回对象。否则通过函数 cache_alloc_refill 从slab中分配。cache_alloc_refill 的作用主要从slab中获取batchcount个对象放入per-CPU缓存。

这里涉及到另外一个问题,如果当前slab空闲缓存不足,需调用 cache_grow 函数重新分配新的slab节点。 cache_grow 执行时首先计算颜色值,颜色值加一,如果超过最大值,则返回从0开始,这里的颜色值主要是一个偏移。

slab所需的内存使用是kmem_getpages从伙伴系统分配的,如果slab头存储在slab之外,则调用 alloc_slabmgmt函数分配所需要的空间。

最后对slab对象初始化之后添加到nodelists的free链表中。

分配完毕如果对象对象释放,使用函数 kmem_cache_free,释放过程如果per-CPU中尚有剩余位置,讲对象放入对应数组中,如果没有,则释放到对应的nodelists中。

Built with Hugo
主题 StackJimmy 设计