前言
前面的关于内存的部分分配是伙伴系统算法,这个是基于以页面为最小单位,一个页面还是蛮大的,像很多嵌入式设备对于内存的请求分配可能只需要几十个字节,这样如果给别人一个页,也就太浪费了。
进一步,就算你是个大方的好人,但是如果有很多这样的请求,那对于内存的浪费是不是很可怕。就必须要有一种分配更小内存的方法:slab机制。
1、slab机制的必要性和大概样子
当然可能你在这个内存的学习过程中,还遇到过slob、slub。这是由slab机制针对特殊场景演化出来的。
slub:大型服务器
slob:嵌入式系统
这样的原因是因为slab机制存在两个问题:
- 使用的元数据开销比较大,这个元数据是对于数据的管理成本。
- slab分配器的代码量和复杂度很高
(对于元数据,你可能需要有点认识,这个第一次看我是在学数据结构中见到的,这个元数据的含义我决定写一篇解读一下)
这里我们还是基于slab机制来讲解。
不知道你在面对这个小内存分配有没有想到什么招数?根据伙伴系统的联想,我们可以在这里构建一个以2的order字节大小的内存分配机制。这个是kmalloc的机制思想。
这里slab是以数据结构为对象进行内存池的构建,比如内核常见的struct mm_struct数据结构、进程控制块mm_struct。slab就建立一个mm_struct的对象缓存池,这样在需要的时候就直接拿出来,也不会存在多余的内存浪费,速度也快。你想想要是基于其他的,你给申请者一片内存,我还需要切割成小字节的,然后再使用,又浪费又慢。
(还有个问题,就是页面分配器去申请物理页面,会产生阻塞、睡眠的可能,对于这个速度不稳定)
所以slab机制,解决了内存浪费的问题,也保证了内存分配速度的问题。
2、slab分配的核心思想
从上面大概认识了slab就是个分配小内存的机制,同时这个分配不是基于内存大小的,而是基于数据机构对象的。知道这个是干啥的,但是对于这个具体分配的过程我们这里来看看。
最核心的就是在空闲时建立缓存对象池。(挺好奇这个空闲时创建的机制是怎么运转的),建立的这个对象缓存池包括本地也包括共享的对象缓存池。
2.1、本地缓存池
本地缓存池是在创建每个slab描述符时,就为每个cpu创建一个本地的缓存池。(这个描述符就是数据结构)本地可以减少多核cpu之间的锁竞争。
共享对象缓存池是所有共享的,本地没有时,就从共享缓存池中搬移一批对象到本地缓存池。(共享缓存池是多久建立的?)
其中涉及到三个概念
2.2、slab描述符
struct kmem_cache数据结构是slab分配器中的核心数据结构,我们把它称为slab描述符。
struct kmem_cache数据结构的定义如下。
[include/linux/slab_def.h] 0 /* 1 * kmem cache数据结构的核心成员 2 */ 3 struct kmem_cache { 4 struct array_cache __percpu *cpu_cache; 7 unsigned int batchcount; 8 unsigned int limit; 9 unsigned int shared; 11 unsigned int size; 12 struct reciprocal_value reciprocal_buffer_size; 15 unsigned int flags; 16 unsigned int num; 20 unsigned int gfporder; 23 gfp_t allocflags; 25 size_t colour; 26 unsigned int colour_off; 27 struct kmem_cache *freelist_cache; 28 unsigned int freelist_size; 31 void (*ctor)(void *obj); 34 const char *name; 35 truct list_head list; 36 int refcount; 37 int object_size; 38 int align; 41 struct kmem_cache_node *node[MAX_NUMNODES]; 42 }
每个slab描述符都由一个struct kmem_cache数据结构来抽象描述。
cpu_cache:一个Per-CPU的struct array_cache数据结构,每个CPU一个,表示本地CPU的对象缓冲池。
batchcount:表示当前CPU的本地对象缓冲池array_cache为空时,从共享的缓冲池或者slabs_partial/slabs_free列表中获取对象的数目。
limit:当本地对象缓冲池的空闲对象数目大于limit时,会主动释放batchcount个对象,便于内核回收和销毁slab。
shared:用于多核系统。
size:对象的长度,这个长度要加上align对齐字节。
flags:对象的分配掩码。
num:一个slab中最多可以有多少个对象。
gfporder:一个slab中占用2^gfporder个页面。
colour:一个slab中有几个不同的cache line。
colour_off:一个cache colour的长度,和L1缓存行大小相同。
freelist_size:每个对象要占用1字节来存放freelist。
name:slab描述符的名称。
object_size: 对象的实际大小。
align:对齐的长度。
node:slab节点,在NUMA系统中每个节点有一个struct kmem_cache_node数据结构。
在ARM Vexpress平台中,只有一个节点。
struct array_cache数据结构定义如下。
struct array_cache { unsigned int avail; unsigned int limit; unsigned int batchcount; unsigned int touched; void *entry[]; };
slab描述符给每个CPU都提供一个对象缓存池(array_cache)。
batchcount/limit:和struct kmem_cache数据结构中的语义一样。
avail:对象缓存池中可用的对象数目。
touched:从缓冲池移除一个对象时,将touched置1;而收缩缓存时,将touched置0。
entry:保存对象的实体。
2.3、slab
一个slab的组成如图7.17所示,它由1个或者多个(2的order次方)连续的物理页面组成。注意:这是连续的物理页面。
(注意这里是gforder)
一般根据缓存对象object大小、align大小等参数来统一计算出来究竟由多少个页面组成一个slab是最经济、最合适的。最后会计算出来,一个slab里面最多可以有多少个cache colour,这里cache colour指的cache着色区。这是在slab机制里特有的,但是slub里已经去掉了。着色区后面紧跟着freelist,用来管理后面的object的。freelist后面就是一个个object对象了。
2.4、slab机制
slab机制分两步完成。第一步是使用kmem_cache_create()函数创建一个slab描述符,使用struct kmem_cache数据结构来描述。struct kmem_cache数据结构里有几个主要的成员,一个是指向本地缓存池的指针,另一个是指向slab节点的node指针。每个内存节点有一个slab节点,通常ARM只有一个内存节点,这里就假设系统只有一个slab节点。其他是描述这个slab描述符的信息,比如这个slab的对象的大小、名字以及align等信息。
这个slab节点里有3个链表,分别是slab空闲链表、slab满链表和slab partial链表。这些链表的成员是slab,不是对象。另外,该节点里有一个指针指向一个共享缓存池,它和本地缓存池是相对的。
第二步是从这个slab描述符中去分配空闲对象。一个CPU要从这个slab描述符中分配对象,它首先去访问当前CPU对应的这个slab描述符里的本地缓存池。如果本地缓冲池里有空闲对象,就直接获取,没有其他 CPU 过来竞争。如果本地缓冲池里没有空闲对象,那么需要去共享缓冲池里查询是否有空闲对象。如果有,就从共享缓存池里搬移几个空闲对象到自己的缓存池中。
可是刚创建slab描述符时,本地缓冲池和共享对象缓冲池里都是空的,没有空闲对象,那slab是怎么建立的呢?
建立 slab 所使用的物理页面需要向页面分配器申请,这个过程可能会睡眠。如图 7.18所示,建好一个slab之后,会把这个slab添加到 slab节点的slab空闲链表里,所以slab中的3个链表的成员是slab,而不是对象。这里是通过slab的第一个页面的lru成员挂入链表中的。另外,空闲的对象会搬移到共享缓冲池和本地缓冲池,供分配器使用。
2.4、slab回收
slab回收就是slab运行的机制。当然,slab不能只分配,不用的slab还是会被回收的。如果一个slab描述符中有很多空闲对象,那么系统是否要回收一些空闲的缓存对象,从而释放内存归还系统呢?这是必须要考虑的问题,否则系统有大量的 slab 描述符,每个 slab 描述符还有大量不用的、空闲的slab对象。
slab系统有两种方式来回收内存。
使用kmem_cache_free释放一个对象。当发现本地和共享对象缓冲池中的空闲对象数目 ac->avail 大于等于缓冲池的极限值 ac->limit 时,系统会主动释放bacthcount个对象。当系统所有空闲对象数目大于系统空闲对象数目极限值,并且这个 slab没有活跃对象时,系统就会销毁这个slab,从而回收内存。
slab系统还注册了一个定时器,定时扫描所有的slab描述符,回收一部分空闲对象,达到条件的slab也会被销毁,实现函数为cache_reap()。
3、slab分配接口
上面知道了slab的分配机制,概念还是比较抽象。再结合api函数,以及其参数来看看。
创建slab描述符
struct kmem_cache *kmem_cache_create(const char *name, size_t size, size_t align,unsigned long flags, void (*ctor)(void *))
释放slab描述符
void kmem_cache_destroy(struct kmem_cache *s)
分配缓存对象
void *kmem_cache_alloc(struct kmem_cache *, gfp_t flags);
释放缓存对象
void kmem_cache_free(struct kmem_cache *, void *);
kmem_cache_create()函数中有如下参数。
name:slab描述符的名称。 size:缓存对象的大小。 align:缓存对象需要对齐的字节数。 flags:分配掩码。 ctor:对象的构造函数。
这个时候还记得描述符和对像是什么?
例如,在Intel显卡驱动中就大量使用了kmem_cache_create()创建自己的slab描述符。
[drivers/gpu/drm/i915/i915_gem.c]
voidi915_gem_load(struct drm_device *dev) { … dev_priv->slab =kmem_cache_create("i915_gem_object",sizeof(struct drm_i915_gem_object), 0,SLAB_HWCACHE_ALIGN,NULL); … }
void *i915_gem_object_alloc(struct drm_device *dev) { #分配缓存对象 return kmem_cache_zalloc(dev_priv->slab, GFP_KERNEL); }
4、kmalloc机制
5、小结
待解决
问题
元数据是什么?
参考资料
《奔跑吧 Linux内核》