前一篇blog linux内核内存分配(一、基本概念)主要是分析linux内核内存的分配和物理页分配函数接口。但是在实际的操作中,不一定所有内存申请都需要一个物理页,很多只是需要分配几K大小的内存就可以。所以就需要更小的内存分配函数。刚开始看这个有点不懂,不过懂了就很简单了。哈哈。
摘抄《深入linux设备驱动程序内核机制》的一段话:slab分配器的基本思想是,先利用页面分配器分配出单个或者一组连续的物理页面,然后在此基础上将整块页面分割成多个相等的小内存单元,以满足小内存空间分配的需要。当然,为了有效的管理这些小的内存单元并保证极高的内存使用速度和效率。
首先看下一张图,这是一张非常经典的图(所以也是从别的地方截取过来的,哈哈)基本上讲slab的都会上这张图(有些图可能会有点点不同,不过都是大同小异)
这是有struct kmem_cache 和 struct slab构成的slab分配器;
从大方向来说,每个kmem_cache都是链接在一起形成一个全局的双向链表,由cache指向该链表,系统可以从Cache_chain开始扫描每个kmem_cache,来找到一个大小最合适的kmem_cache,然后从该kmem_cache中分配一个对象;
其中每个kmem_cache(有的地方也会叫这个kmem_cache为cache,原因是kmem_cache中的object有大有小(其实也是kmem_cache有大有小),当内存申请时,会有命中该kmem_cache的说法,和CPU中的cache命中是类似的意思,所以也会叫kmem_cache为cache(个人理解))有三条链表,slabs_full 表示该链表中每个slab的object对象都已经分配完了;slabs_partial 表示该链表中的slab的object对象部分分配完了;slabs_empty 表示该链表中的object对象全部没有分配出去;
其中每个slab都是一个或者多个连续的内存页组成,而每个slab被分成多个object对象。对象的分配和释放都是在slab中进行的,所以slab可以在三条链表中移动,如果slab中的object都分配完了,则会移到full 链表中;如果分配了一部分object,则会移到partial链表中;如果所有object都释放了,则会移动到empty链表中;其中当系统内存紧张的时候,slabs_empty链表中的slab可能会被返回给系统。
基本的概念就是这样,下面说说简单的代码实现;
首先所有的kmem_cache结构都是从cache_cache,这个内存是在系统还没有完全初始化好就创建了(这个结构我看到了两个版本,不过意思差不多):
static kmem_cache_t cache_cache = {
slabs_full: LIST_HEAD_INIT(cache_cache.slabs_full),
slabs_partial: LIST_HEAD_INIT(cache_cache.slabs_partial),
slabs_free: LIST_HEAD_INIT(cache_cache.slabs_free),
objsize: sizeof(kmem_cache_t),
flags: SLAB_NO_REAP,
spinlock: SPIN_LOCK_UNLOCKED,
colour_off: L1_CACHE_BYTES,
name: "kmem_cache",
};
先看下面两个结构体:
struct cache_size{
size_t cs_size;
struct kmem_cache *cs_cachep;
}
struct cache_size malloc_sizes[] = {
{.cs_size = 32},
{.cs_size = 64},
{.cs_size = 128},
{.cs_size = 256},
................
{.cs_size = ~0UL},
};
在系统初始化时,内核会调用kmem_cache_init函数对malloc_size数组进行遍历,对数组中的每个元素都调用kmem_cache_create()函数在cache_cache中分配一个struct kmem_cache 实例,并且把kmem_cache所在的地址赋值给cache_size中的cs_cachep指针(malloc_sizes[x]->cs_cachep);
void __init kmem_cache_init(void)
{
struct cache_size *sizes = malloc_sizes;//数组
struct cache_names *names = cache_names;//cache名称
.....
while(sizes->cs_size != ULONG_MAX){//从32到~0UL都遍历每个元素
if(!sizes->cs_cachep)//表示还没有被初始化
{
// kmem_cache_create就是创建一个kmem_cache
sizes->cs_cachep = kmem_cache_create(names->name, sizes->cs_size,
ARCH_KMALLOC_MINALIGN,
ARCH_KMALLOC_FLAGS|SLAB_PANIC,
NULL);
}
sizes++;
names++;
}
.....
}
初始化后的slab分配器如下图(图片来自于《深入linux设备程序机制》):
从上面的图可以看出,第一行为malloc_sizes[]数组的所有元素,由于malloc_size[]数组中存放的是 struct cache_size结构体元素,所以cache_size->cs_size 和 cache_size->cs_cachep; 其中每个cs_cachep指向一个kmem_cache,表示该kmem_cache中slab分配的对象大小为cs_size;这就很容易理解了;
注意,这是slab分配器只是个空壳子,kmem_cache下面是三条空的链表,也就是说kmem_cache下面没有一个slab也没有一个page更没有一个对象;这时候仅仅是定义了一个规则,表示该kmem_cache下的对象大小都固定为cs_size。至于什么时候创建slab,后面再讨论;
所谓手动分配就是在自己程序中分配,其实就是理解下kmem_cache_create()函数:
struct kmem_cache* kmem_cache_create(const char* name, size_t size, size_t align, unsigned long flags, void(*ctor)(void*));
参数 name 是指向字符串的指针,用来生成kmem_cache的名字,会在/proc/slabinfo中出现;生成的kmem_cache对象会用一个指针指向该name;所以要保证name在kmem_cache对象有效周期内都有效;
参数 size 是用来指定slab分配的对象大小;
参数 align 是表示数据对齐的,一般使用0就可以;
参数 flags 是创建kmem_cache标识位掩码,使用0,表示默认;
参数 void (*ctor)(void*) 是构造函数,当slab分配一个新页面时,会对该页面中的每个内存对象调用该构造函数;
返回值:从cache_cache中返回一个指向kmem_cache实例的*cachep指针;当然该kmem_cache对象也会被加入cache_chain链表中;
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);
其中cachep就是上面kmem_cache_create()函数返回的kmem_cache对象,该函数会返回cachep下的一个空闲的内存对象;flags 则是在没有空闲对象,需要从物理页分配一个新页时,才使用的到;
下面把kmalloc函数简单的分析下,代码把无关的东西删除掉了,主要是说明下kmalloc的工作原理;对照下面的图会很容易知道该函数做了些什么;
void * kmalloc(size_t size, int flags)
{
struct cache_size *csizep = malloc_sizes;//定义好大小的数组
struct kmem_cache *cachep;
while(size > csizep->cs_size)//这是关键,从malloc_sizes数组(其实也是从kmem_cache链表)中遍历,得到地一个大于等于size的cache
csizep++;
cachep = csizep->cs_cachep;
return kmem_cache_alloc(cachep, flags)//这里会发现正真分配对象的还是靠kmem_cache_alloc()函数
}
前面说了,kmem_cache_create()仅仅是从cache_cache中分配一个kmem_cache实例,并不会分配实际的物理页,当然也就没有slab了(也没有对象)。那什么时候会创建一个slab呢?
只有满足下面两个条件时,才会给kmem_cache分配Slab:
(1)已发出一个分配新对象的请求;(2)kmem_cache中没有了空闲对象;
其实本质就是:需要得到该kmem_cache下一个对象,而kmem_cache没有空闲对象,这时候就会给kmem_cache分配一个slab了。所以新分配的kmem_cache只有被要求分配一个对象时,才会调用函数去申请物理页;
具体的分配流程:
首先会调用kmem_cache_grow()函数给kmem_cache分配一个新的Slab。其中,该函数调用kmem_gatepages()从伙伴系统获得一组连续的物理页面;然后又调用kmem_cache_slabgmt()获得一个新的Slab结构;还要调用kmem_cache_init_objs()为新Slab中的所有对象申请构造方法(如果定义的话);最后,调用kmem_slab_link_end()把这个Slab结构插入到缓冲区中Slab链表的末尾。
从slab的分配可以知道,其实所有的内存最终还是要伙伴系统来分配,这里就可以知道,这些内存都是连续的物理页。
这是后面增加的(感觉非常有必要提下):在某些情况下内核模块可能需要频繁的分配和释放相同的内存对象,这时候slab可以作为内核对象的缓存,当slab对象被释放时,slab分配器并不会把对象占用的物理空间还给伙伴系统。这样的好处是当内核模块需要再次分配内存对象时,不需要那么麻烦的向伙伴系统申请,而是可以直接在slab链表中分配一个合适的对象;
转载地址:http://blog.csdn.net/yuzhihui_no1/article/details/47305361
注:如果需要详细理解可以看《深入linux设备程序机制》,我的资源里可以免费下载这本电子书