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分配器的结构如下:
每种具体的内存分配器都由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_partia
l,slabs_full
,slabs_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中。