Linux内存管理第八章 -- Slab Allocator (二)

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
最后一个问题就是用于管理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
如上图所示,当为on-slab的情况时,在slab开头出要保留足够的空间用于存放struct slab_t,其中有一个free字段,其实是一个unsigned int型用来存放下一个free object的index,在后续章节会详细讨论free字段是如何工作的。
再来看下off-slab的示意图:
off
从上图可以看出,当为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中
    CACHE_GROW

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数组的元素将会是下图中的样子:
1233
此时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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值