文章目录
Linux内存管理 – Slab Allocator (二)
Slabs
本段落将描述slab如何被结构化和被管理的。相比cache描述符,用来描述slab的结构体要简单很多。但是slab如何被安置要更复杂些。slab描述符定义如下:
struct slab {
struct list_head list;
unsigned long colouroff;
void *s_mem; /* including colour offset */
unsigned int inuse; /* num of objs active in slab */
kmem_bufctl_t free;
};
其字段含义如下:
- list:连接该slab的链表。该链表是cache中的slab_full,slab_paritial,slab_free三者中的一个。
- colouroff :该字段表示在slab中,从slab的起始地址开始到放置第一个object的位置之间的偏移,这个偏移叫做着色偏移。如果slab的起始地址为s_mem,那么该slab中第一个object的地址为s_mem + colouroff
- s_mem:slab中第一个object的基地址
- inuse:slab中active object的个数
- free:该字段是一个kmem_bufctl_t类型的数组,用来存放free object的位置。详细细节见后续章节。
至此读者可能就会发现对于一个已知slab或者一个slab中已知的object来说,没有一种很明显的方式来决定slab属于哪个cache或者object属于哪个slab。而实际的实现方式是通过每个struct page中的list字段来组装成一个cache。SET_PAGE_CACHE()和SET_PAGE_SLAB()使用page->list中的next和prev字段来追踪slab属于哪个cache和object属于哪个slab。为了能从struct page中获取到slab或者cache的描述符,有GET_PAGE_CACHE()和GET_PAGE_SLAB()两个宏可以使用。它们的关系如下图:
最后一个问题就是用于管理slab的struct要放置在哪里。slab manager要么在slab内要么在slab外,可通过CFLGS_OFF_SLAB来进行标示。将slab manager放置在哪里是由在创建cache时object的大小决定的。要重点说明下在上图中,struct slab_t可以被放置在一个物理页框的的头部,尽管上图中暗示struct slab_t与物理页框是分离的。
Storing the Slab Descriptor
如果一个object的size大于一个阈值(x86上是512字节), CFGS_OFF_SLAB将会在cache flags中被置位,那么slab描述符将存放在通用的cache中二独立于slab之外。被选中的通用cache有足够大的空间来存放struct slab_t,在需要将slab_t放置在slab之外时,可kmem_cache_slabmgmt()函数用来分配struct slab_t。一个slab中存放object的数量是由有限的是因为bufctls的空间是有限的。但是这不是很重要因为通常objects size很大因此在单个slab中object的数量并不会有很多。
下图为on-slab的示意图:
如上图所示,当为on-slab的情况时,在slab开头出要保留足够的空间用于存放struct slab_t,其中有一个free字段,其实是一个unsigned int型用来存放下一个free object的index,在后续章节会详细讨论free字段是如何工作的。
再来看下off-slab的示意图:
从上图可以看出,当为off-slab时,struct slab_t是从通用cache Size-N中分配空间,而当分配一个page给到slab,该page内全部存放的是object而没有struct slab_t.
Slab Creation
至此,我们已经知道如何创建一个cache。但是在cache刚被创建时,它仅仅是一个空cache,slab_full,slab_partial,slab_free都是空链表。当需要在cache中分配新的slab时,可以调用cache_grow()函数。该函数调用时发生在当slabs_partial链表中没有可用的object并且slab_free链表中没有空闲的slab的时候。该函数的执行步骤如下:
- 执行一些基础的sanity检查来避免错误调用
- 计算当前slab中object的colour offset
- 为slab分配空间并获取一个slab描述符
- 将slab空间对应的page,通过page->lru中next和prev字段分别指向当前page所属的cache和slab
- 初始化slab中的object
- 将slab加入到cache中
Tracking Free Objects
slab allocator必须拥有一种快速而简单的方法追踪到非全满的slab中的free object。系统中通过使用明教kmem_buffctl_t的unsigned int型的数据来实现查找free object。在每一个slab manager都有一个这样的数组,因此slab manager知道free object在哪里。
从历史上看,最开始根据描述slab allocator的论文可得知,kmem_bufctl_t是一个将object链接起来的链表。但是在Linux2.2.x中,该结构是一个union,有三个元素:一个指向下一个free object的指针,一个指向slab manager的指针,一个指向当前object的指针。这些指针的值取决于object的状态。
当前,一个object属于哪个slab和属于哪个cache可以通过struct page来找到。而kmem_buffctl_t是一个简单的object索引数组。数组中元素的个数与slab中object的个数一致。
typedef unsigned int kmem_bufctl_t;
因为该数组紧跟在slab描述符后面,因此没有一个指针直接指向它的第一个元素,因此有提供下面的函数来访问获取该数组的地址:
static inline kmem_bufctl_t *slab_bufctl(struct slab *slabp)
{
return (kmem_bufctl_t *)(slabp+1);
}
也就是通过slab描述符的地址就可以得到kmem_bufctl_t 整型数组的首地址。而slab_t.free作为kmem_bufctl_t 整型数组的index指向下一个当前slab 中下一个free object。slab_t.free的值随着slab的分配和释放,不断的在变化。
Initialising the kmem_bufctl_t Array
当一个cache增长后,slab中所有的object和kmem_bufctl_t数组都已经被初始化。kmem_bufctl_t数组中填充的值为每一个object索引编号,从1开始,BUFCTL_END结束。如果一个slab有5个object,kmem_bufctl_t数组的元素将会是下图中的样子:
此时slab_t->free中的值为0,因为第0个object是第一个可以使用的free object。假设给定一个object n,那么下一个free object的index将存储在 kmem_bufctl_t[n]中。从上面这个数组中可以看出object 0的下一个object是object 1,object 1的下一个是object 2依次类推。因为是使用数组,因此free object安排顺序将会把数组当做是一个LIFO。
Finding the Next Free Object
当分配一个object的时候,kmem_cache_alloc()通过调用cache_alloc_refill()函数来进行真正的更新kmem_bufctl_t数组的操作。因为slab_t->free拥有第一个free object的index,而下一个free object的index为kmem_bufctl_t[slab_t->free],因此流程码就像如下的代码:
currFreeObj = slabp->s_mem + slabp->free * cachep->slab_size;
slabp->free = slab_bufctl(slabp)[slabp->free];
Updating kmem_bufctl_t
kmem_bufctl_t数组只有当一个object被释放之后才会更新。更新实现就像下面的代码块:
unsigned int objnr = (objp - slabp->s_mem)/cachep->objsize;
slab_bufctl(slabp)[objrn] = slabp->free;
slabp->free = objnr;
其中objp是指即将要被释放的object地址,首先算出该object的真实index为objnr,然后将kmem_bufctl_t[objnr]更新为当前slabp->free的值。然后再将slabp->free更新为objnr。
换句话说,当前slabp–>free指向object A,要释放的为object B,释放object B之后,即object B为第一个free object,而object A为object B的下一个free object。
Calculating the Number of Objects on a Slab
在创建cache的过程当中,函数cache_estimate()被调用用来计算单个slab中可能存放多少个object,进而计算出slab描述符必须是on-slab还是off-slab,再来计算用于追踪free object的kmem_bufctl_t 数组需要多大空间,其返回可以存储的object个数和浪费的字节数。如果要使用cache着色,要浪费的字节数就相当重要了。
其计算过程如下:
- 将wastpage初始化为slab的total size。如:PAGE_SIZEgfp_order, slab大小通常为1个page。
- 减去slab描述符所需要的空间大小
- 计算出可以存放object的个数。如果是on-slab,那么slab中还包含kmem_bufctl_t 数组的大小。
- 返回object个数以及浪费的字节数
在kmem_cache_create()中,如果object的大小大于等于PAGE_SIZE>>3,就使用off-slab.
/* Determine if the slab management is 'on' or 'off' slab. */
if (size >= (PAGE_SIZE>>3))
/*
* Size is large, assume best to place the slab management obj
* off-slab (should allow better packing of objs).
*/
flags |= CFLGS_OFF_SLAB;
下面来看下cache评估函数的源码:
/* Cal the num objs, wastage, and bytes left over for a given slab size. */
static void cache_estimate (unsigned long gfporder, size_t size, size_t align,
int flags, size_t *left_over, unsigned int *num)
{
int i;
size_t wastage = PAGE_SIZE<<gfporder; //slab total size
size_t extra = 0;// off-slab case下 extra = 0
size_t base = 0;// off-slab case下 base = 0
if (!(flags & CFLGS_OFF_SLAB)) {//on-slab 的case
base = sizeof(struct slab); //slab 描述符的大小
extra = sizeof(kmem_bufctl_t);// unsigned int 大小
}
i = 0;
while (i*size + ALIGN(base+i*extra, align) <= wastage)
i++; //递增object的个数,直到总大小大于 wastage为止
if (i > 0)
i--; //因为在while循环总size已经超过了slab total,所以要减掉一个object
if (i > SLAB_LIMIT)
i = SLAB_LIMIT;
*num = i; //单个slab中可以存放object的个数
wastage -= i*size; //减掉i个object所占的空间
wastage -= ALIGN(base+i*extra, align);//减掉base加上i个unsigned int的对齐后的字节数,即为要浪费的字节数
*left_over = wastage;//要浪费的字节数
}
Slab Destroying
当一个cache需要被收缩或者被销毁时,slabs将会被删除。因为object可能有析构函数,因此他们必须被调用,因此slab_destroy()销毁slab的步骤如下:
- 如果可能,调用slab中每一个object的析构函数
- 如果是debug模式,检查red mark和poison pattern
- 释放slab所占用的pages
Objects
本段将会覆盖slab中的object如何被管理。至此,大多数真正难的工作已经被完成如:cache或者slab manager
Initialising Objects in a Slab
当一个slab被创建,slab中的所有object都为初始状化态。如果构造函数存在,每一个object的构造函数将被调用,期望得到的结果是所有free object是初始化状态。概念上初始化非常简单,遍历所有的object并调用构造函数和初始化kmen_bufctl。cache_init_objs()函数就是来负责初始化object。
Object Allocation
函数kmem_cache_alloc()负责分配一个object给调用者。在Linux2.6中它的实现函数为kmem_cache_alloc() --> cache_alloc_refill()其中有四个基本步骤:
- first,做一些基本检查
- second,选择从哪一个slabs list中分配object,slabs_parital或者slabs_free
- third,如果slabs_free中没有slabs,那么就需要增长cache,在slabs_free中创建新的slab
- forth,从被选中的slab中分配一个object
如果是SMP case,将会多一个步骤要做:在分配object之前,要先检查是否可以先从per-CPU cache中分配object,如果per-CPU cache总有可用的object,优先使用per-CPU cache中的object。如果没有,则需要一次移动batchcount个object到per-CPU cache中。
Object Freeing
kmem_cache_free()函数被用来释放objects。如果per-CPU cache中可用的object大于limit值,则需要将per-CPU中batchcount个object启动到slab中,再来检查释放后的slab是否全部空闲,如果全部空闲,则销毁掉该slab,再把要释放的object放入到per-CPU list中。如果per-CPU cache中的object个数小于limit值,则将要释放的object释放到per-CPU cache中。
通用cache – Sizes Cache
Linux包含有两组cache用于小块内存分配,由于块太小不适合buddy allocator来分配。其中一组cache供DMA设备使用,另外一组供常规内核中小块内存的常规分配。这些cache的名称分别为size-N cache和size-N(DMA)cache,可用过/proc/slabinfo来进行查看。每个特定大小的cache存放在struct cache_sizes中,其被type defined为cache_sizes_t,其内部结构如下:
struct cache_sizes {
size_t cs_size;
kmem_cache_t *cs_cachep;
kmem_cache_t *cs_dmacachep;
};
结构体中字段含义如下:
- cs_size:cache中内存块的大小
- cs_cachep:常规分配的cache
- cs_dmacachep:DMA分配使用的cache
因为存在于系统中的这些cache的数量是有限的,因此一个静态的数组malloc_sizes在编译阶段被初始化。
如果是page size为4KB的机器,最小的cache 内存块从25字节开始到217字节
如果page size大于4KB,则从26字节开始到217字节
struct cache_sizes malloc_sizes[] = {
#define CACHE(x) { .cs_size = (x) },
#include <linux/kmalloc_sizes.h>
{ 0, }
#undef CACHE
};
#if (PAGE_SIZE == 4096)
CACHE(32)
#endif
CACHE(64)
#if L1_CACHE_BYTES < 64
CACHE(96)
#endif
CACHE(128)
#if L1_CACHE_BYTES < 128
CACHE(192)
#endif
CACHE(256)
CACHE(512)
CACHE(1024)
CACHE(2048)
.................
kmalloc()/kfree()
由于通用size-N cache的存在,slab allocator因此会提供一个新的分配函数kmalloc()用于分配小块内存。当分配需求到来,会通过要求分配的内存大小来选择相应的cache,然后从该cache中分配一个object。
而kfree()是用来释放从通用cache size-N中分配的object。
Per-CPU Object Cache
slab allocator的一个重要任务就是致力于提升硬件缓存的使用率。通常高性能计算的一个目标就是尽可能长时间的使用同一CPU的数据。Linux通过使用per-CPU cache来尽力保证object在相同的CPU cache中来达到高性能计算的目的。
当分配或者释放object时,这些object会先放入到cpucache中。当cpucache中没有空闲的object,将会重新放入batchcount个object。当cpucache中object太多,则将一般数量的object删除并放入到global cache中。这种方式下硬件cache将会尽可能长时间的被相同的CPU使用。
第二个重要的好处是这种方法会减少spinlock的持有次数,因为CPU pool中的数据会被保证不会被其他CPU访问。这一点很重要,如果没有CPU cache,每次内存的分配和释放都将要持有spinlock,这是不必要的消耗。
Describing the Per-CPU Object Cache
每一个slab cache描述符中都有一个指向cpucaches数组的指针。其定义如下:
struct array_cache *array[NR_CPUS];
struct array_cache {
unsigned int avail;
unsigned int limit;
unsigned int batchcount;
unsigned int touched;
};
其中字段含义如下:
- avail:表示该cpucache中free object的数量
- limit:cpucache中可以拥有free object的最大个数
- batchcount:slab与cpucache每次转换object的数量
Draining a Per-CPU Cache
当一个cache要被收缩,其第一步就是通过调用drain_cpu_caches()清除cpucache中可能持有的任何object。目的是为了使得slab allocator拥有一个清晰的视觉来检查哪些slab可以被释放哪些slab不能被释放。这一点非常重要,如果slab中只有一个object在per-cpu cache中,那么整个slab都不能被释放。如果系统的内存很紧张,节省几微秒的分配时间的优先级是很低的。
Slab Allocator Initialisation
此处我们将描述slab allocator如何初始化它自己。当slab allocator创建一个cache时,它需要从cache_cache中分配cache描述符struct kmem_cache_t。这很明显是一个鸡生蛋和蛋生鸡的问题。因此cache_cache需要静态地初始化:
static kmem_cache_t cache_cache = {
.lists = LIST3_INIT(cache_cache.lists),
.batchcount = 1,
.limit = BOOT_CPUCACHE_ENTRIES,
.objsize = sizeof(kmem_cache_t),
.flags = SLAB_NO_REAP,
.spinlock = SPIN_LOCK_UNLOCKED,
.name = "kmem_cache",
#if DEBUG
.reallen = sizeof(kmem_cache_t),
#endif
};
静态定义的所有字段在编译阶段就提前计算好了的,在start_kernel()函数中调用kmem_cache_init()来初始化该结构体中剩余的字段。
参考文献:
https://www.kernel.org/doc/gorman/html/understand/understand011.html