linux内存管理-内核缓冲区的管理

可想而知,内核在运行中常常会需要使用一些缓冲区。例如,当要建立一个新的进程时就要增加一个task_struct结构,而当进程撤销时就要释放本进程的task结构。这些小块存储空间的使用并不局限于某一个子程序,否则就可以作为整个子程序的局部变量而使用堆栈空间了。另外,这些小块存储空间又是动态变化的,不像用于内存管理的page结构那样,有多大的内存就有多少个page结构,构成一个静态的阵列。由于事先根本无法预测运行中各种不同数据结构对缓冲区的需求,不适合为每一种可能用到的数据结构建立一个缓冲池,因为那样的话很可能会出现有些缓冲池已经用尽而有些缓冲池中却有大量缓冲区空闲的局面。因此,只能采用更具全局性的方法。

那么,用什么样的方法呢?如果采用像用户空间的malloc那样的动态分配办法,从一个统一的存储空间堆(heap)中,需要用多少就切下来多大一块,不用了就归还,则有几个缺点需要考虑改进:

  • 久而久之,会使存储碎片化,以致虽然存储堆中空闲空间的总量足够大、却无法分配所需大小的连续空间。为此,一般都采用按2^N的大小来分配空间,以缓解碎片化。
  • 每次分配得到所需大小的缓冲区以后,都要进行初始化。内核中频繁地使用一些数据结构,这些数据结构中相当一部分成分需要某些特殊的初始化(例如队列头部等)而并非简单的清成全0。如果释放的数据结构可以在下次分配时重用而无需初始化,那就可以提高内核的效率。
  • 缓冲区的组织和管理是密切相关的。在有高速缓存的情况下,这些缓冲区的组织和管理方式直接影响到高速缓存中的命中率,进而影响到运行时的效率。试想,假定我们运用最先符合(first fit)的方法,从一个由存储空间片段构成的队列中分配缓冲区。在这样的过程中,当一个片段不能满足要求而顺着指针往下看下一个片段的数据结构时,如果该数据结构每次都在不同的页面中,因而每次都不能命中,而要从内存装入到高速缓存,那么可想而知,其效率显然就要大打折扣了。
  • 不适合多处理器共用内存的情况。

实际上,如何有效地管理缓冲区空间,很久以来就是一个热门的研究课题。90年代前期,在Solaris2.4操作系统(Unix的一个变种)中,采用了一种称为slab的缓冲区分配和管理方法(slab的原意是大块的混泥土),在相当程度上克服了上述的缺点。而linux,也在其内核中采用了这种方法,并作了改进。

从存储器分配的角度讲,slab与为各种数据结构分别建立缓冲池相似,也与以前我们看到过的按大小划分管理区(zone)的方法相似,但是也有重要的不同。

在slab方法中,每种重要的数据结构都有自己专用的缓冲区队列,每种数据结构都有相应的构造constructor和析构destructor函数。同时,还借用面向对象程序设计技术中的名词,不再称结构而称为对象(object)。缓冲区队列中的各个对象在建立时用其构造函数进行初始化,所以一经分配立即就能使用,而在释放时则恢复成原状。例如,对于其中的队列头成分来说(读者可参看page数据结构的定义,结构中有两个struct list_head成分),当将其从队列中摘除时自然就恢复了原状。每个队列中对象的个数是动态变化的,不够时可以增添。同时,又定期地检查,将由富余的队列加以精简。我们在kswapd的do_try_to_free_pages中曾经看到,调用函数kmem_cache_reap,为的就是从富余的队列回收物理页面,只是当时我们没有细讲。其实,定期地检查和处理这些缓冲区队列,也是kswapd的一项功能。

此外,slab管理方法还有一个特点,每种对象的缓冲区队列并非由各个对象直接构成,而是由一连串的大块(slab)构成,而每个大块中则包含了若干同种的对象。一般而言,对象分两种,一种是大对象,一种是小对象。所谓小对象,是指在一个页面中可以容纳下好几个对象的那一种。例如,一个inode的大小约300多个字节,因此一个页面中可以容纳8个以上的inode,所以inode是小对象。内核中使用的大多数数据结构都是这样的小对象,所以,我们先来看对小对象的组织和管理以及相应的slab结构。先看用于某种假象小对象的一个slab块的结构示意图:

此处先对上列示意图做几点说明,详细情况则随着代码的阅读再逐步深入:

  • 一个slab可能由1个、2个、4个、...最多32个连续的物理页面构成。slab的具体大小因对象的大小而异,初始化时通过计算得出最合适的大小。
  • 在每个slab的前端是该slab的描述结构slab_t。用于同一种对象的多个slab通过描述结构中的队列头形成一条双向队列。每个slab双向队列在逻辑上分成三截,第一截是各个slab上所有的对象都已分配使用的;第二截是各个slab上的对象已经部分地分配使用;最后一截是各个slab上的全部对象都处于空闲状态。
  • 每个slab上都有一个对象区,这是个对象数据结构的数组,以对象的序号为下标就可得到具体对象的起始地址。
  • 每个slab上还有个对象链接数组,用来实现一个空闲对象链。
  • 同时,每个slab的描述结构中都有一个字段,表明该slab上的第一个空闲对象。这个字段与对象链接数组结合在一起形成了一条空闲对象链。
  • 在slab描述结构中还有一个已经分配使用的对象的计数器,当将一个空闲的对象分配使用时,就将slab控制结构中的计数器加1,并将该对象从空闲队列中脱链。
  • 当释放一个对象时,只需要调整链接数组中的相应元素以及slab描述结构中的计数器,并且根据该slab的使用情况而调整其在slab队列中的位置(例如,如果slab上所有的对象都已经分配使用,就要将该slab从第二截转移到第一截去)。
  • 每个slab的头部有一个小小的区域是不使用的,称为着色区(coloring area)。着色区的大小使slab中的每个对象的起始地址都按高速缓存中的缓存行(cache line)大小(80386的一级高速缓存中缓存行大小为16个字节,Pentium为32个字节)对齐。每个slab都是从一个页面边界开始的,所以本来就自然按高速缓存的缓存行对齐,而着色区的设置只是将第一个对象的起始地址往后推到另一个与缓存行对齐的边界。同一个对象的缓冲队列中的各个slab的着色区的大小尽可能地安排成不同的大小,使得不同slab上同一相对位置的对象的起始地址在高速缓存中互相错开,这样可以改善高速缓存的效率。
  • 每个slab上最后一个对象以后也有一个小小的废料区是不用的,这是对着色区大小的补偿,其大小取决于着色区的大小以及slab与其每个对象的相对大小。但该区域与着色区的总和对于同一种对象的各个slab是个常数。
  • 每个对象的大小基本上是所需数据结构的大小。只有当数据结构的大小不与高速缓存中的缓存行对齐时,才增加若干字节使其对齐。所以,一个slab上的所有对象的起始地址都必然是按高速缓存中的缓冲行对齐的。

下面就是slab描述结构slab_t的定义:

/*
 * slab_t
 *
 * Manages the objs in a slab. Placed either at the beginning of mem allocated
 * for a slab, or allocated from an general cache.
 * Slabs are chained into one ordered list: fully used, partial, then fully
 * free slabs.
 */
typedef struct slab_s {
	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;
} slab_t;

这里的队列头list用来将一块slab链入一个专用缓冲区队列,colouroff为本地slab上着色区的大小,指针s_mem指向对象区的起点,inuse是已分配对象的计数器。最后,free的值指明了空闲对象链中的第一个对象,其实是个整数。

/*
 * kmem_bufctl_t:
 *
 * Bufctl's are used for linking objs within a slab
 * linked offsets.
 *
 * This implementaion relies on "struct page" for locating the cache &
 * slab an object belongs to.
 * This allows the bufctl structure to be small (one int), but limits
 * the number of objects a slab (not a cache) can contain when off-slab
 * bufctls are used. The limit is the size of the largest general cache
 * that does not use off-slab slabs.
 * For 32bit archs with 4 kB pages, is this 56.
 * This is not serious, as it is only for large objects, when it is unwise
 * to have too many per slab.
 * Note: This limit can be raised by introducing a general cache whose size
 * is less than 512 (PAGE_SIZE<<3), but greater than 256.
 */

#define BUFCTL_END 0xffffFFFF
#define	SLAB_LIMIT 0xffffFFFE
typedef unsigned int kmem_bufctl_t;

在空闲对象链接数组中,链内每一个对象所对应元素的值为下一个对象的序号,最后一个对象所对应元素的值为BUFCTL_END。

为每种对象建立的slab队列都有个队列头,其控制结构为kmem_cache_t。该数据结构中除用来维持slab队列的各种指针外,还记录了适用于队列中每个slab的各种参数,以及两个函数指针;一个是对象的构造函数,另一个是析构函数,有趣的是,像其他数据结构一样,每种对象的slab队列头也是在slab上。系统中有个总的slab队列,其对象是各个其他对象的slab队列头,其队头则也是一个kmem_cache_t结构,称为cache_cache。

这样,就形成了一种层次式的树形结构:

  • 总根cache_cache是一个kmem_cache_t结构,用来维持第一层slab队列,这些slab上的对象都是kmem_cache_t数据结构。
  • 每个第一层slab上的每个对象,即kmem_cache_t数据结构都是队头,用来维持一个第二层slab队列。
  • 第二层slab队列基本上都是为某种对象,即数据结构专用的。
  • 每个第二层slab上都维持着一个空闲对象队列。

总体的组织如下图所示:

从图中可以看出,最高的层次是slab队列cache_cache,队列中的每个slab载有若干个kmem_cache_t数据结构。而每个这样的数据结构又是某种数据结构(例如inode、vm_area_struct、mm_struct,乃至ip网络信息包等等)缓冲区的slab队列的头部。这样,当要分配一个某种数据结构的缓冲区时,就只要指明是从哪一个队列中分配,而不需要说明缓冲区的大小,并且不需要初始化了。具体的函数是:

void * kmem_cache_alloc (kmem_cache_t *cachep, int flags)
void kmem_cache_free (kmem_cache_t *cachep, void *objp)

所以,当需要分配一个具体专用slab队列的数据结构时,应该通过kmem_cache_alloc分配。例如,我们曾经看到过的mm_struct、vm_area_struct、file、dentry、inode等常用的数据结构,就都有专用的slab队列,而应通过kmem_cache_alloc分配。                              

当数据结构比较大,因而不属于小对象时,slab的结构略有不同。不同之处是不将slab的控制结构放在它所代表的slab上,而是将其游离出来,集中放在另外的slab上。由于在slab的控制结构slab_t中有一个指针指向相应slab上的第一个对象,所以逻辑上是一样的。其实,这就是将控制结构与控制对象相分离的一般模式。打个比方,载有小对象的slab就好像是随身携带的户口本而载有大对象的slab就好像是将户口本集中存放在派出所里或者某个代理机构里。此外,当对象的大小恰好是物理页面的1/2、1/4或1/8时,将依附于每个对象的链接指针紧挨着放在一起会造成slab空间上的巨大浪费,所以在这些特殊情况下,将连接指针也从slab上游离出来集中存放,以提高slab的空间使用率。

不过,并非内核中使用的所有数据结构都有必要拥有专用的缓冲区队列,一些不太常用、初始化开销也不大的数据结构还是可以合用一个通用的缓冲区分配机制。所以,linux内核中还有一种既类似于物理页面分配中采用的按大小分区,又采用slab方式管理的通用缓冲池,称为cache_sizes。

cache_sizes的结构与cache_cache大同小异,只不过其顶层不是一个队列而是一个结构数组(cache_sizes相对来说比较静态),数组中的每一个元素指向一个不同的slab队列。这些slab队列的不同之处仅在于所载对象的大小。最小的是32,然后依次为64、128、...直至128K(也就是32个页面)。从通用缓冲池中分配和释放缓冲区的函数为:

void * kmalloc (size_t size, int flags);
void kfree (const void *objp);

所以。当需要分配一个不具有专用slab队列的数据结构而又不必为之使用整个页面时,就应该通过kmalloc分配。这一般都是些细小而又不常用的数据结构,例如在文件系统中安装文件系统时使用的vfsmount数据结构就是这样。如果数据结构的大小接近于一个页面,则也可以干脆就通过alloc_pages为之分配一个页面。

顺便提一下,内核中还有一组与内存分配有关的函数vmalloc和vfree:

static inline void * vmalloc (unsigned long size);
void vfree(void * addr);

函数vmalloc从内核的虚存空间(3GB以上)分配一块虚存以及相应的物理内存,类似于系统调用brk。不过brk是由进程在用户空间启动并从用户空间分配的,而vmalloc则是在系统空间,也就是在内核中启动,从内核空间中分配的。由vmalloc分配的空间不会被kswapd换出,因为kswapd只扫描各个进程的用户空间,而根本就看不到通过vmalloc分配的页面表项。至于通过kmalloc分配的数据结构,则kswapd只是从各个slab队列中寻找和收集空闲不用的slab,并释放所占用的页面,但是不会将尚未在使用中的slab所占用的页面换出。由于vmalloc与我们后面要将的ioremap非常相似,这里就不讲了。

在讲解内核缓冲区的分配之前,我们先介绍缓冲区队列的建立。

专用缓冲区队列的建立

本来,虚存区间结构vm_area_struct的专用缓冲区队列是一个很好的实例,读者都已经熟悉了这个数据结构的使用。但是,到现在为止,linux内核中多数专用缓冲区的建立都用NULL作为构造函数的指针,也就是说并没有充分利用slab管理机制所提供的好处(相对来说,slab是比较新的技术),似乎不够典型。所以,我们从内核的网络驱动子系统中选择了一个例子,这是在net/core/skbuff.c中定义的:

void __init skb_init(void)
{
	int i;

	skbuff_head_cache = kmem_cache_create("skbuff_head_cache",
					      sizeof(struct sk_buff),
					      0,
					      SLAB_HWCACHE_ALIGN,
					      skb_headerinit, NULL);
	if (!skbuff_head_cache)
		panic("cannot create skbuff cache");

	for (i=0; i<NR_CPUS; i++)
		skb_queue_head_init(&skb_head_pool[i].list);
}

从代码中可以看到,skb_init所作的事情实际上就是为网络驱动子系统建立一个sk_buff数据结构的专用缓冲区队列,其名称为skbuff_head_cache。读者可以用命令cat /proc/slabinfo来观察这些队列的使用情况。每个缓冲区,或者说对象的大小是sizeof(struct sk_buff)。调用参数offset为0,表示对第一个缓冲区在slab中的位移并无特殊要求。但是参数flags为SLAB_HWCACHE_ALIGN,表示要求与高速缓存中的缓存行边界(16字节或32字节)对齐。对象的构造函数为skb_headerinit,而析构函数为NULL,也就是说在拆除或者释放一个slab时无需对各个缓冲区进行特殊的处理。

函数kmem_cache_create所做的事情过于专门,过于冷僻,这里就不深入到其代码中去了,只是把它的内容概要介绍如下。

首先,要从cache_cache中分配一个kmem_cache_t结构,作为sk_buff数据结构slab队列的控制结构,数据结构类型kmem_cache_t定义如下:


struct kmem_cache_s {
/* 1) each alloc & free */
	/* full, partial first, then free */
	struct list_head	slabs;
	struct list_head	*firstnotfull;
	unsigned int		objsize;
	unsigned int	 	flags;	/* constant flags */
	unsigned int		num;	/* # of objs per slab */
	spinlock_t		spinlock;
#ifdef CONFIG_SMP
	unsigned int		batchcount;
#endif

/* 2) slab additions /removals */
	/* order of pgs per slab (2^n) */
	unsigned int		gfporder;

	/* force GFP flags, e.g. GFP_DMA */
	unsigned int		gfpflags;

	size_t			colour;		/* cache colouring range */
	unsigned int		colour_off;	/* colour offset */
	unsigned int		colour_next;	/* cache colouring */
	kmem_cache_t		*slabp_cache;
	unsigned int		growing;
	unsigned int		dflags;		/* dynamic flags */

	/* constructor func */
	void (*ctor)(void *, kmem_cache_t *, unsigned long);

	/* de-constructor func */
	void (*dtor)(void *, kmem_cache_t *, unsigned long);

	unsigned long		failures;

/* 3) cache creation/removal */
	char			name[CACHE_NAMELEN];
	struct list_head	next;
#ifdef CONFIG_SMP
/* 4) per-cpu data */
	cpucache_t		*cpudata[NR_CPUS];
#endif
#if STATS
	unsigned long		num_active;
	unsigned long		num_allocations;
	unsigned long		high_mark;
	unsigned long		grown;
	unsigned long		reaped;
	unsigned long 		errors;
#ifdef CONFIG_SMP
	atomic_t		allochit;
	atomic_t		allocmiss;
	atomic_t		freehit;
	atomic_t		freemiss;
#endif
#endif
};

在kmem_cache_s的基础上,又定义了kmem_cache_t:

typedef struct kmem_cache_s kmem_cache_t;

结构中的队列头slabs用来维持一个slab队列,指针firstnotfull则指向队列中第一个含有空间对象(即缓冲区)的slab,也就是指向队列的第二段。当这个指针指向队列头slabs时就表明队列中不存在含有空闲对象slab。

结构中还有个队列头next,则是用来在cache_cache中建立一个专用缓冲区slab队列的队列,也就是slab队列控制结构的队列。当slab的描述结构与对象不在同一slab上时,即对大对象slab,指针slabp_cache指向对方队列的控制结构。

除这些队列头和指针以外,还有一些重要的成分;objsize是原始的数据结构(对象)的大小,在这个情景中就是sizeof(struct sk_buff);num表示每个slab上有几个缓冲区;gfporder则表示每个slab的大小。每个slab都是由2^N个页面构成的,而gfporder就是N。

前面讲过,在每个slab的前部保留了一小块区域空着不用,那就是着色区(coloring area),其作用是使同一slab队列中不同slab上对象区的起始地址互相错开,这样有利于改善高速缓冲的效率。所以,如果当前slab的颜色为1,则下一个slab的颜色将是2,使下一个slab中的第一个缓冲区更往后推一些。但是,不同颜色的数量是有限的,它取决于一块slab分割成若干缓冲区(对象)以及所需的其他空间以后剩余,以及高速缓存中每个缓存行(cache line)的大小。所以,对每个slab队列都要计算出它的颜色数量,这个数量就保存在color中,而下一个slab将要使用的颜色则保存在color_next中。当color_next达到最大值colour时,就又从0开始,如此周而复始,着色区的大小可以根据(colour_off*colour)算得。

分配了一个kmem_cache_t结构以后,kmem_cache_create就进行一系列的计算,以确定最佳的slab构成。包括:每个slab由几个页面组成,划分成多少个缓冲区(即对象);slab的控制结构kmem_cache_t应该在slab外面集中存放还是就放在每个slab的尾部;每个缓冲区的链接指针应该在slab外面集中存放还在slab上与相应的缓冲区紧挨着放在一起;还有颜色的数量等等。并根据调用参数和计算的结果设置队列头kmem_cache_t结构中的各个参数,包括两个函数指针ctor和dtor。

最后,将队列头kmem_cache_t结构链入cache_cache的next队列中(注意,不是它的slab队列中)。

函数kmem_cache_create只是建立了所需的专用缓冲区队列的基础设施,所形成的的slab队列是个空队列。而具体slab的创建则要等需要分配缓冲区时,却发现队列中并无空闲的缓冲区可供分配时,再通过kmem_cache_grow来进行。

缓冲区的分配与释放

在建立了一种缓冲区的专用队列以后,就可以通过kmem_cache_alloc来分配缓冲区了。就上面建立的skbuff_head_cache队列来说,是这样进行分配的:


struct sk_buff *alloc_skb(unsigned int size,int gfp_mask)
{
	struct sk_buff *skb;
	u8 *data;

	if (in_interrupt() && (gfp_mask & __GFP_WAIT)) {
		static int count = 0;
		if (++count < 5) {
			printk(KERN_ERR "alloc_skb called nonatomically "
			       "from interrupt %p\n", NET_CALLER(size));
 			BUG();
		}
		gfp_mask &= ~__GFP_WAIT;
	}

	/* Get the HEAD */
	skb = skb_head_from_pool();
	if (skb == NULL) {
		skb = kmem_cache_alloc(skbuff_head_cache, gfp_mask);
		if (skb == NULL)
			goto nohead;
	}
......
}

函数alloc_skb是具体设备驱动程序对kmem_cache_alloc的包装,在此基础上建立起自己的缓冲区管理机制,包括一个sk_buff数据结构的缓冲池,以加快分配数据结构的速度,并防止因具体驱动程序分配释放缓冲区不当引起问题。这样,就把具体的设备启动程序与kmem_cache_alloc分隔开了。

要分配一个sk_buff数据结构,先通过skb_head_from_pool试试缓冲池。如果在缓冲池中得不到,那就要进一步通过kmem_cache_alloc分配,这就是我们所关心的。其代码如下:

alloc_skb=>kmem_cache_alloc


/**
 * kmem_cache_alloc - Allocate an object
 * @cachep: The cache to allocate from.
 * @flags: See kmalloc().
 *
 * Allocate an object from this cache.  The flags are only relevant
 * if the cache has no available objects.
 */
void * kmem_cache_alloc (kmem_cache_t *cachep, int flags)
{
	return __kmem_cache_alloc(cachep, flags);
}

alloc_skb=>kmem_cache_alloc=>__kmem_cache_alloc


static inline void * __kmem_cache_alloc (kmem_cache_t *cachep, int flags)
{
	unsigned long save_flags;
	void* objp;

	kmem_cache_alloc_head(cachep, flags);
try_again:
	local_irq_save(save_flags);
#ifdef CONFIG_SMP
	{
		cpucache_t *cc = cc_data(cachep);

		if (cc) {
			if (cc->avail) {
				STATS_INC_ALLOCHIT(cachep);
				objp = cc_entry(cc)[--cc->avail];
			} else {
				STATS_INC_ALLOCMISS(cachep);
				objp = kmem_cache_alloc_batch(cachep,flags);
				if (!objp)
					goto alloc_new_slab_nolock;
			}
		} else {
			spin_lock(&cachep->spinlock);
			objp = kmem_cache_alloc_one(cachep);
			spin_unlock(&cachep->spinlock);
		}
	}
#else
	objp = kmem_cache_alloc_one(cachep);
#endif
	local_irq_restore(save_flags);
	return objp;
alloc_new_slab:
#ifdef CONFIG_SMP
	spin_unlock(&cachep->spinlock);
alloc_new_slab_nolock:
#endif
	local_irq_restore(save_flags);
	if (kmem_cache_grow(cachep, flags))
		/* Someone may have stolen our objs.  Doesn't matter, we'll
		 * just come back here again.
		 */
		goto try_again;
	return NULL;
}

首先,alloc_skb中的指针skbuff_head_cache是个全局变量,指向相应的slab队列(这里是sk_buff结构的slab队列)的队头,因而这里的参数cachep也是指向这个队列。

程序中的kmem_cache_alloc_head是为调试而设的,在实际运行的系统中是空函数。我们在这里也不关心多处理器SMP结构,所以这里关键性的操作就是kmem_cache_alloc_one,这是一个宏操作,定义如下:

/*
 * Returns a ptr to an obj in the given cache.
 * caller must guarantee synchronization
 * #define for the goto optimization 8-)
 */
#define kmem_cache_alloc_one(cachep)				\
({								\
	slab_t	*slabp;					\
								\
	/* Get slab alloc is to come from. */			\
	{							\
		struct list_head* p = cachep->firstnotfull;	\
		if (p == &cachep->slabs)			\
			goto alloc_new_slab;			\
		slabp = list_entry(p,slab_t, list);	\
	}							\
	kmem_cache_alloc_one_tail(cachep, slabp);		\
})

上面__kmem_cache_alloc的代码一定要和这个宏定义结合起来看才能明白。从定义中可以看到,第一步是通过slab队列头中的指针firstnotfull,找到该队列中第一个含有空闲对象的slab。如果这个指针指向slab队列的链头(不是链中的第一个slab),那就表明队列中已经没有含有空闲对象的slab,所以就转到__kmem_cache_alloc中的标号alloc_new_slab处(1324行),进一步扩充该slab队列。

如果找到了含有空闲对象的slab,就调用kmem_cache_alloc_one_tail分配一个空闲对象并返回其指针:

alloc_skb=>kmem_cache_alloc=>__kmem_cache_alloc=>kmem_cache_alloc_one_tail


static inline void * kmem_cache_alloc_one_tail (kmem_cache_t *cachep,
							 slab_t *slabp)
{
	void *objp;

	STATS_INC_ALLOCED(cachep);
	STATS_INC_ACTIVE(cachep);
	STATS_SET_HIGH(cachep);

	/* get obj pointer */
	slabp->inuse++;
	objp = slabp->s_mem + slabp->free*cachep->objsize;
	slabp->free=slab_bufctl(slabp)[slabp->free];

	if (slabp->free == BUFCTL_END)
		/* slab now full: move to next slab for next alloc */
		cachep->firstnotfull = slabp->list.next;
#if DEBUG
	if (cachep->flags & SLAB_POISON)
		if (kmem_check_poison_obj(cachep, objp))
			BUG();
	if (cachep->flags & SLAB_RED_ZONE) {
		/* Set alloc red-zone, and check old one. */
		if (xchg((unsigned long *)objp, RED_MAGIC2) !=
							 RED_MAGIC1)
			BUG();
		if (xchg((unsigned long *)(objp+cachep->objsize -
			  BYTES_PER_WORD), RED_MAGIC2) != RED_MAGIC1)
			BUG();
		objp += BYTES_PER_WORD;
	}
#endif
	return objp;
}

如前所述,数据结构slab_t中的free记录着下一次可以分配的空闲对象的序号,而s_mem则指向slab中的对象区,所以根据这些数据和本身专用队列的对象大小,就可以计算出空闲对象的起始地址。然后,就通过宏操作slab_bufctl改变字段free的值,使它指明下一个空闲对象的序号。

#define slab_bufctl(slabp) \
	((kmem_bufctl_t *)(((slab_t*)slabp)+1))

这个宏操作返回一个kmem_bufctl_t数组的地址,这个数组就在slab中数据结构slab_t的上方,紧挨着数据结构slab_t。该数组以当前对象的序号为下标,而数组元素的值则表明下一个空闲对象的序号。改变了slab_t的free字段的值,就隐含着当前对象已被分配。

如果达到了slab的末尾BUFCTL_END,就要调整该slab队列的指针firstnotfull,使它指向队列中的下一个slab。

不过,我们假定slab队列中已经不存在含有空闲对象的slab,所以要转到前面代码中的标号alloc_new_slab处,通过kmem_cache_grow来分配一块新的slab,使缓冲区的队列生长起来。函数kmem_cache_grow的代码如下:

alloc_skb=>kmem_cache_alloc=>__kmem_cache_alloc=>kmem_cache_grow


/*
 * Grow (by 1) the number of slabs within a cache.  This is called by
 * kmem_cache_alloc() when there are no active objs left in a cache.
 */
static int kmem_cache_grow (kmem_cache_t * cachep, int flags)
{
	slab_t	*slabp;
	struct page	*page;
	void		*objp;
	size_t		 offset;
	unsigned int	 i, local_flags;
	unsigned long	 ctor_flags;
	unsigned long	 save_flags;

	/* Be lazy and only check for valid flags here,
 	 * keeping it out of the critical path in kmem_cache_alloc().
	 */
	if (flags & ~(SLAB_DMA|SLAB_LEVEL_MASK|SLAB_NO_GROW))
		BUG();
	if (flags & SLAB_NO_GROW)
		return 0;

	/*
	 * The test for missing atomic flag is performed here, rather than
	 * the more obvious place, simply to reduce the critical path length
	 * in kmem_cache_alloc(). If a caller is seriously mis-behaving they
	 * will eventually be caught here (where it matters).
	 */
	if (in_interrupt() && (flags & SLAB_LEVEL_MASK) != SLAB_ATOMIC)
		BUG();

	ctor_flags = SLAB_CTOR_CONSTRUCTOR;
	local_flags = (flags & SLAB_LEVEL_MASK);
	if (local_flags == SLAB_ATOMIC)
		/*
		 * Not allowed to sleep.  Need to tell a constructor about
		 * this - it might need to know...
		 */
		ctor_flags |= SLAB_CTOR_ATOMIC;

	/* About to mess with non-constant members - lock. */
	spin_lock_irqsave(&cachep->spinlock, save_flags);

	/* Get colour for the slab, and cal the next value. */
	offset = cachep->colour_next;
	cachep->colour_next++;
	if (cachep->colour_next >= cachep->colour)
		cachep->colour_next = 0;
	offset *= cachep->colour_off;
	cachep->dflags |= DFLGS_GROWN;

	cachep->growing++;
	spin_unlock_irqrestore(&cachep->spinlock, save_flags);

	/* A series of memory allocations for a new slab.
	 * Neither the cache-chain semaphore, or cache-lock, are
	 * held, but the incrementing c_growing prevents this
	 * cache from being reaped or shrunk.
	 * Note: The cache could be selected in for reaping in
	 * kmem_cache_reap(), but when the final test is made the
	 * growing value will be seen.
	 */

	/* Get mem for the objs. */
	if (!(objp = kmem_getpages(cachep, flags)))
		goto failed;

	/* Get slab management. */
	if (!(slabp = kmem_cache_slabmgmt(cachep, objp, offset, local_flags)))
		goto opps1;

	/* Nasty!!!!!! I hope this is OK. */
	i = 1 << cachep->gfporder;
	page = virt_to_page(objp);
	do {
		SET_PAGE_CACHE(page, cachep);
		SET_PAGE_SLAB(page, slabp);
		PageSetSlab(page);
		page++;
	} while (--i);

	kmem_cache_init_objs(cachep, slabp, ctor_flags);

	spin_lock_irqsave(&cachep->spinlock, save_flags);
	cachep->growing--;

	/* Make slab active. */
	list_add_tail(&slabp->list,&cachep->slabs);
	if (cachep->firstnotfull == &cachep->slabs)
		cachep->firstnotfull = &slabp->list;
	STATS_INC_GROWN(cachep);
	cachep->failures = 0;

	spin_unlock_irqrestore(&cachep->spinlock, save_flags);
	return 1;
opps1:
	kmem_freepages(cachep, objp);
failed:
	spin_lock_irqsave(&cachep->spinlock, save_flags);
	cachep->growing--;
	spin_unlock_irqrestore(&cachep->spinlock, save_flags);
	return 0;
}

函数kmem_cache_grow根据队列头中的参数gfporder分配若干连续的物理内存页面,并将这些页面构成slab,链入给定的slab队列。对参数进行了一些检查以后,就计算出下一块slab应有的着色区大小。然后,通过kmem_getpages分配用于具体对象缓冲区的页面,这个函数最终调用alloc_pages分配空闲页面。分配了用于对象本身的内存页面后,还要通过kmem_cache_slabmgmt建立起slab的管理信息。其代码如下:

alloc_skb=>kmem_cache_alloc=>__kmem_cache_alloc=>kmem_cache_grow=>kmem_cache_slabmgmt


/* Get the memory for a slab management obj. */
static inline slab_t * kmem_cache_slabmgmt (kmem_cache_t *cachep,
			void *objp, int colour_off, int local_flags)
{
	slab_t *slabp;
	
	if (OFF_SLAB(cachep)) {
		/* Slab management obj is off-slab. */
		slabp = kmem_cache_alloc(cachep->slabp_cache, local_flags);
		if (!slabp)
			return NULL;
	} else {
		/* FIXME: change to
			slabp = objp
		 * if you enable OPTIMIZE
		 */
		slabp = objp+colour_off;
		colour_off += L1_CACHE_ALIGN(cachep->num *
				sizeof(kmem_bufctl_t) + sizeof(slab_t));
	}
	slabp->inuse = 0;
	slabp->colouroff = colour_off;
	slabp->s_mem = objp+colour_off;

	return slabp;
}

如前所述,小对象的slab控制结构slab_t与对象本身共存于同一slab上,而大对象的控制结构则游离于slab之外。但是,大对象的控制结构也是slab_t,存在于为这种数据结构专设的slab上,也有其专用的slab队列。所以,如果是大对象就通过kmem_cache_alloc分配一个slab_t,否则就用小对象slab低端的一部分空间用作其控制结构,不过在此之前要空出一小块着色区。注意这里1012行和1017行所引用的colour_off不是同一个数值,这个变量的值已在1013行作了调整,在原来的数值上增加了对象链接数组的大小以及控制结构slab_t的大小。所以,slabp->s_mem总是指向slab上对象区的起点。

对分配用于slab的每个页面的page数据结构,要通过宏操作SET_PAGE_CACHE和SET_PAGE_SLAB,设置其链接指针prev和next,使它们分别指向所属的slab和slab队列。同时,这要把page结构中的PG_slab标志位设成1,以表明该页面的用途。

最后,通过kmem_cache_init_objs进行slab的初始化:

alloc_skb=>kmem_cache_alloc=>__kmem_cache_alloc=>kmem_cache_grow=>kmem_cache_init_objs


static inline void kmem_cache_init_objs (kmem_cache_t * cachep,
			slab_t * slabp, unsigned long ctor_flags)
{
	int i;

	for (i = 0; i < cachep->num; i++) {
		void* objp = slabp->s_mem+cachep->objsize*i;
#if DEBUG
		if (cachep->flags & SLAB_RED_ZONE) {
			*((unsigned long*)(objp)) = RED_MAGIC1;
			*((unsigned long*)(objp + cachep->objsize -
					BYTES_PER_WORD)) = RED_MAGIC1;
			objp += BYTES_PER_WORD;
		}
#endif

		/*
		 * Constructors are not allowed to allocate memory from
		 * the same cache which they are a constructor for.
		 * Otherwise, deadlock. They must also be threaded.
		 */
		if (cachep->ctor)
			cachep->ctor(objp, cachep, ctor_flags);
#if DEBUG
		if (cachep->flags & SLAB_RED_ZONE)
			objp -= BYTES_PER_WORD;
		if (cachep->flags & SLAB_POISON)
			/* need to poison the objs */
			kmem_poison_obj(cachep, objp);
		if (cachep->flags & SLAB_RED_ZONE) {
			if (*((unsigned long*)(objp)) != RED_MAGIC1)
				BUG();
			if (*((unsigned long*)(objp + cachep->objsize -
					BYTES_PER_WORD)) != RED_MAGIC1)
				BUG();
		}
#endif
		slab_bufctl(slabp)[i] = i+1;
	}
	slab_bufctl(slabp)[i-1] = BUFCTL_END;
	slabp->free = 0;
}

这里的初始化包括了对具体对象构造函数的调用。对于sk_buff数据结构,这个函数就是skb_headerinit。此外,代码中的1060行是对链接数组中各个元素的初始化。

缓冲区队列成长了一些以后,就一定有空闲缓冲区可供分配了。所以转回标号try_again处再试一遍(见__kmem_cache_alloc中的1334行)。

这样,就构成了一个多层次的缓冲区分配机制。位于最高层的是缓冲区的分配,在我们这个情景中就是alloc_skb,具体则实现通过skb_head_from_pool,从缓冲池,即已经分配的slab块中分配。如果失败的话,就往下跑一层从slab队列中通过kmem_cache_alloc分配,要是slab队列中已经没有载有空闲缓冲区的slab,那就要再往下跑一层,通过kmem_cache_grow,分配若干页面而构筑出一个slab块。

那么,缓冲区队列是否单调递增而不缩小?我们在以前提到过,kswapd定时地调用kmem_cache_reap来收割。也就是说,依次检查若干专用缓冲区slab队列,看看你是否有完全空闲的slab存在。有的话就将这些slab占用的内存页面释放。

再来看专用缓冲区的释放,这是由kmem_cache_free完成的。其代码如下:


/**
 * kmem_cache_free - Deallocate an object
 * @cachep: The cache the allocation was from.
 * @objp: The previously allocated object.
 *
 * Free an object which was previously allocated from this
 * cache.
 */
void kmem_cache_free (kmem_cache_t *cachep, void *objp)
{
	unsigned long flags;
#if DEBUG
	CHECK_PAGE(virt_to_page(objp));
	if (cachep != GET_PAGE_CACHE(virt_to_page(objp)))
		BUG();
#endif

	local_irq_save(flags);
	__kmem_cache_free(cachep, objp);
	local_irq_restore(flags);
}

显然,操作的主体是__kmem_cache_free,这里只是在操作期间把中断暂时关闭。

kmem_cache_free=>__kmem_cache_free


/*
 * __kmem_cache_free
 * called with disabled ints
 */
static inline void __kmem_cache_free (kmem_cache_t *cachep, void* objp)
{
#ifdef CONFIG_SMP
	cpucache_t *cc = cc_data(cachep);

	CHECK_PAGE(virt_to_page(objp));
	if (cc) {
		int batchcount;
		if (cc->avail < cc->limit) {
			STATS_INC_FREEHIT(cachep);
			cc_entry(cc)[cc->avail++] = objp;
			return;
		}
		STATS_INC_FREEMISS(cachep);
		batchcount = cachep->batchcount;
		cc->avail -= batchcount;
		free_block(cachep,
					&cc_entry(cc)[cc->avail],batchcount);
		cc_entry(cc)[cc->avail++] = objp;
		return;
	} else {
		free_block(cachep, &objp, 1);
	}
#else
	kmem_cache_free_one(cachep, objp);
#endif
}

我们在这里不关心多处理器SMP结构,而函数kmem_cache_free_one的代码也在同一个文件中:

kmem_cache_free=>__kmem_cache_free=>kmem_cache_free_one


static inline void kmem_cache_free_one(kmem_cache_t *cachep, void *objp)
{
	slab_t* slabp;

	CHECK_PAGE(virt_to_page(objp));
	/* reduces memory footprint
	 *
	if (OPTIMIZE(cachep))
		slabp = (void*)((unsigned long)objp&(~(PAGE_SIZE-1)));
	 else
	 */
	slabp = GET_PAGE_SLAB(virt_to_page(objp));

#if DEBUG
	if (cachep->flags & SLAB_DEBUG_INITIAL)
		/* Need to call the slab's constructor so the
		 * caller can perform a verify of its state (debugging).
		 * Called without the cache-lock held.
		 */
		cachep->ctor(objp, cachep, SLAB_CTOR_CONSTRUCTOR|SLAB_CTOR_VERIFY);

	if (cachep->flags & SLAB_RED_ZONE) {
		objp -= BYTES_PER_WORD;
		if (xchg((unsigned long *)objp, RED_MAGIC1) != RED_MAGIC2)
			/* Either write before start, or a double free. */
			BUG();
		if (xchg((unsigned long *)(objp+cachep->objsize -
				BYTES_PER_WORD), RED_MAGIC1) != RED_MAGIC2)
			/* Either write past end, or a double free. */
			BUG();
	}
	if (cachep->flags & SLAB_POISON)
		kmem_poison_obj(cachep, objp);
	if (kmem_extra_free_checks(cachep, slabp, objp))
		return;
#endif
	{
		unsigned int objnr = (objp-slabp->s_mem)/cachep->objsize;

		slab_bufctl(slabp)[objnr] = slabp->free;
		slabp->free = objnr;
	}
	STATS_DEC_ACTIVE(cachep);
	
	/* fixup slab chain */
	if (slabp->inuse-- == cachep->num)
		goto moveslab_partial;
	if (!slabp->inuse)
		goto moveslab_free;
	return;

moveslab_partial:
    	/* was full.
	 * Even if the page is now empty, we can set c_firstnotfull to
	 * slabp: there are no partial slabs in this case
	 */
	{
		struct list_head *t = cachep->firstnotfull;

		cachep->firstnotfull = &slabp->list;
		if (slabp->list.next == t)
			return;
		list_del(&slabp->list);
		list_add_tail(&slabp->list, t);
		return;
	}
moveslab_free:
	/*
	 * was partial, now empty.
	 * c_firstnotfull might point to slabp
	 * FIXME: optimize
	 */
	{
		struct list_head *t = cachep->firstnotfull->prev;

		list_del(&slabp->list);
		list_add_tail(&slabp->list, &cachep->slabs);
		if (cachep->firstnotfull == &slabp->list)
			cachep->firstnotfull = t->next;
		return;
	}
}

代码中的CHECK_PAGE只用于程序的调试,在实际运行的系统中为空语句。根据待释放的对象的地址可以算出其所在的页面。进一步,如前所述(见kmem_cache_grow的1142行),页面的page结构中链头list内,原用于队列链接的指针prev,指向页面所属的slab,所以通过宏操作GET_PAGE_SLAB就可以得到这个slab的指针。找到了对象所在的slab,就可以通过其连接数组释放给定对象了(见1404-1407行)。同时,还要递减所属slab队列控制结构中非空闲对象的计数。递减以后有三种可能:

  • 原来slab上没有空闲对象,现在有了,所以要转到moveslab_partial处,把slab从队列中原来的位置转移到队列的第二截,即由指针firstnotfull所指的地方。
  • 原来slab上就有空闲对象,而现在所有对象都空闲了,所以要转到moveslab_free处,把slab从队列中原来的位置移到队列的第三截,即队列的末尾。
  • 原来slab上就有空闲对象,现在只不过是多了一个,但也并没有全部空闲,所以不需要任何改动。

可见,分配和释放专用缓冲区的开销都是很小的。这里还要指出,缓冲区的释放并不导致slab的释放,空闲slab的释放是由kswapd等内核线程周期性地调用kmem_cache_reap完成的。

看完了专用缓冲区的分配和释放,再看看通用缓冲区的分配。前面讲过,除各种专用的缓冲区队列外,内核中还有一个通用的缓冲池cache_sizes,里面根据缓冲区的大小而分成若干队列。用于通用缓冲区分配的函数kmalloc是在mm/slab.c中定义的:


/**
 * kmalloc - allocate memory
 * @size: how many bytes of memory are required.
 * @flags: the type of memory to allocate.
 *
 * kmalloc is the normal method of allocating memory
 * in the kernel.  The @flags argument may be one of:
 *
 * %GFP_BUFFER - XXX
 *
 * %GFP_ATOMIC - allocation will not sleep.  Use inside interrupt handlers.
 *
 * %GFP_USER - allocate memory on behalf of user.  May sleep.
 *
 * %GFP_KERNEL - allocate normal kernel ram.  May sleep.
 *
 * %GFP_NFS - has a slightly lower probability of sleeping than %GFP_KERNEL.
 * Don't use unless you're in the NFS code.
 *
 * %GFP_KSWAPD - Don't use unless you're modifying kswapd.
 */
void * kmalloc (size_t size, int flags)
{
	cache_sizes_t *csizep = cache_sizes;

	for (; csizep->cs_size; csizep++) {
		if (size > csizep->cs_size)
			continue;
		return __kmem_cache_alloc(flags & GFP_DMA ?
			 csizep->cs_dmacachep : csizep->cs_cachep, flags);
	}
	BUG(); // too big size
	return NULL;
}

这里通过一个for循环,在cache_sizes结构数组中由小到大扫描,找到第一个能满足缓冲区大小要求的队列,然后就调用函数__kmem_cache_alloc从该队列中分配一个缓冲区。而__kmem_cache_alloc的作用我们在前面已经简要的介绍过了。

最后,我们来看看空闲slab的收割,即对构成空闲slab的页面的回收。以前我们看到过,内核线程 kswapd在周期性的运行中会调用kmem_cache_reap回收这些页面。这个函数的代码如下:


/**
 * kmem_cache_reap - Reclaim memory from caches.
 * @gfp_mask: the type of memory required.
 *
 * Called from try_to_free_page().
 */
void kmem_cache_reap (int gfp_mask)
{
	slab_t *slabp;
	kmem_cache_t *searchp;
	kmem_cache_t *best_cachep;
	unsigned int best_pages;
	unsigned int best_len;
	unsigned int scan;

	if (gfp_mask & __GFP_WAIT)
		down(&cache_chain_sem);
	else
		if (down_trylock(&cache_chain_sem))
			return;

	scan = REAP_SCANLEN;
	best_len = 0;
	best_pages = 0;
	best_cachep = NULL;
	searchp = clock_searchp;
	do {
		unsigned int pages;
		struct list_head* p;
		unsigned int full_free;

		/* It's safe to test this without holding the cache-lock. */
		if (searchp->flags & SLAB_NO_REAP)
			goto next;
		spin_lock_irq(&searchp->spinlock);
		if (searchp->growing)
			goto next_unlock;
		if (searchp->dflags & DFLGS_GROWN) {
			searchp->dflags &= ~DFLGS_GROWN;
			goto next_unlock;
		}
#ifdef CONFIG_SMP
		{
			cpucache_t *cc = cc_data(searchp);
			if (cc && cc->avail) {
				__free_block(searchp, cc_entry(cc), cc->avail);
				cc->avail = 0;
			}
		}
#endif

		full_free = 0;
		p = searchp->slabs.prev;
		while (p != &searchp->slabs) {
			slabp = list_entry(p, slab_t, list);
			if (slabp->inuse)
				break;
			full_free++;
			p = p->prev;
		}

		/*
		 * Try to avoid slabs with constructors and/or
		 * more than one page per slab (as it can be difficult
		 * to get high orders from gfp()).
		 */
		pages = full_free * (1<<searchp->gfporder);
		if (searchp->ctor)
			pages = (pages*4+1)/5;
		if (searchp->gfporder)
			pages = (pages*4+1)/5;
		if (pages > best_pages) {
			best_cachep = searchp;
			best_len = full_free;
			best_pages = pages;
			if (full_free >= REAP_PERFECT) {
				clock_searchp = list_entry(searchp->next.next,
							kmem_cache_t,next);
				goto perfect;
			}
		}
next_unlock:
		spin_unlock_irq(&searchp->spinlock);
next:
		searchp = list_entry(searchp->next.next,kmem_cache_t,next);
	} while (--scan && searchp != clock_searchp);

	clock_searchp = searchp;

	if (!best_cachep)
		/* couldn't find anything to reap */
		goto out;

	spin_lock_irq(&best_cachep->spinlock);
perfect:
	/* free only 80% of the free slabs */
	best_len = (best_len*4 + 1)/5;
	for (scan = 0; scan < best_len; scan++) {
		struct list_head *p;

		if (best_cachep->growing)
			break;
		p = best_cachep->slabs.prev;
		if (p == &best_cachep->slabs)
			break;
		slabp = list_entry(p,slab_t,list);
		if (slabp->inuse)
			break;
		list_del(&slabp->list);
		if (best_cachep->firstnotfull == &slabp->list)
			best_cachep->firstnotfull = &best_cachep->slabs;
		STATS_INC_REAPED(best_cachep);

		/* Safe to drop the lock. The slab is no longer linked to the
		 * cache.
		 */
		spin_unlock_irq(&best_cachep->spinlock);
		kmem_slab_destroy(best_cachep, slabp);
		spin_lock_irq(&best_cachep->spinlock);
	}
	spin_unlock_irq(&best_cachep->spinlock);
out:
	up(&cache_chain_sem);
	return;
}

这个函数扫描slab队列的队列cache_cache,从中发现可供收割的slab队列。不过,并不是每次都扫描整个cache_cache,而只是扫描其中的一部分slab队列,所以需要有个全局变量来记录下一次扫描的起点,这就是clock_searchp:

/* Place maintainer for reaping. */
static kmem_cache_t *clock_searchp = &cache_cache;

找到了可以收割的slab队列,也不是把它所有空闲的slab都全部回收,而是回收其中的大约80%。对于要回收的slab,调用kmem_slab_destroy释放其各个页面,我们把这个函数留给读者自己阅读。

kmem_cache_reap=>kmem_slab_destroy


/* Destroy all the objs in a slab, and release the mem back to the system.
 * Before calling the slab must have been unlinked from the cache.
 * The cache-lock is not held/needed.
 */
static void kmem_slab_destroy (kmem_cache_t *cachep, slab_t *slabp)
{
	if (cachep->dtor
#if DEBUG
		|| cachep->flags & (SLAB_POISON | SLAB_RED_ZONE)
#endif
	) {
		int i;
		for (i = 0; i < cachep->num; i++) {
			void* objp = slabp->s_mem+cachep->objsize*i;
#if DEBUG
			if (cachep->flags & SLAB_RED_ZONE) {
				if (*((unsigned long*)(objp)) != RED_MAGIC1)
					BUG();
				if (*((unsigned long*)(objp + cachep->objsize
						-BYTES_PER_WORD)) != RED_MAGIC1)
					BUG();
				objp += BYTES_PER_WORD;
			}
#endif
			if (cachep->dtor)
				(cachep->dtor)(objp, cachep, 0);
#if DEBUG
			if (cachep->flags & SLAB_RED_ZONE) {
				objp -= BYTES_PER_WORD;
			}	
			if ((cachep->flags & SLAB_POISON)  &&
				kmem_check_poison_obj(cachep, objp))
				BUG();
#endif
		}
	}

	kmem_freepages(cachep, slabp->s_mem-slabp->colouroff);
	if (OFF_SLAB(cachep))
		kmem_cache_free(cachep->slabp_cache, slabp);
}

おすすめ

転載: blog.csdn.net/guoguangwu/article/details/120913791
おすすめ