Linux内核空间内存管理(三):slab内存分配机制剖析

版权声明:本文为博主整理文章,未经博主允许不得转载。 https://blog.csdn.net/ZYZMZM_/article/details/90707778


slab概述

分配和释放数据结构是所有内核中最普遍的操作之一。为了便于数据的频繁分配和回收,编程者常常会用到一个空闲链表。该空闲链表包含有可供使用的、已经分配好的数据结构块。当代码需要一个新的数据结构实例时,就可以从空闲链表中抓取一个,而不需要分配内存,再把数据放进去。以后,当不再需要这个数据结构的实例时,就把它放回空闲链表,而不是释放掉它。从这个意义上说,空闲链表相当于对象高速缓存以便快速存储频繁使用的对象类型。

在内核中,空闲链表面临的主要问题之一是不能全局控制。当可用内存变得紧缺时,内核无法通知每个空闲链表,让其收缩缓存的大小以便释放出一些内存来。实际上,内核根本就不知道存在任何空闲链表。为了弥补这一缺陷,也为了使代码更加稳固,Linux内核提供了slab层(也就是所谓的slab分配器)。slab分配器扮演了通用数据结构缓存层的角色。

slab 是 Linux 操作系统的一种内存分配机制。其工作是针对一些经常分配并释放的对象,如进程描述符等,这些对象的大小一般比较小,如果直接采用伙伴系统来进行分配和释放,不仅会造成大量的内存碎片,而且处理速度也太慢。而slab分配器是基于对象进行管理的,相同类型的对象归为一类(如进程描述符就是一类),每当要申请这样一个对象,slab分配器就从一个slab列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中,而不是直接返回给伙伴系统,从而避免这些内碎片slab分配器并不丢弃已分配的对象,而是释放并把它们保存在内存中。当以后又要请求新的对象时,就可以从内存直接获取而不用重复初始化

Slab分配器为了减少内存分配、初始化、销毁和释放的代价,通常会维护经常使用的内存区的一个现成的缓存区。这个缓存区维护看分配的、初始化的以及准备部署的内存区。当请求进程不再需要内存区时,就会把释放的内存送回缓存区中。

实际上,slab分配器由许多缓存组成,不同的缓存存储大小不同的内存区。缓存可以是专用的(specialized),也可能是通用的(general purpose),专用缓存存储保存特定对象的内存区,比如各种描述符,像进程描述符(task_struct)就存放在slab分配器维护的缓存中,该缓存存储的内存区的大小为 task_struct 结构体的大小sizeof (task_struct) 。同样,inode 和 dentry 数据结构也存放在缓存中。一般来讲,通用缓存是预定义大小的内存区组成,其大小可以是:32、64、128、256、512、1024、2048、4 096、8192、16 384、32768、65536 和131 072字节。

对象高速缓存的组织如下图所示,高速缓存的内存区被划分为多个slab每个slab由一个或多个连续的页框组成,这些页框中既包含已分配的对象,也包含空闲的对象。我们之后会对其进行详细讲解。

slab分配器有以下三个基本目标:

  • 减少伙伴算法在分配小块连续内存时所产生的内部碎片
  • 将频繁使用的对象缓存起来,减少分配、初始化和释放对象的时间开销。
  • 通过着色技术调整对象以更好的使用硬件高速缓存;

接下来我们详细讲解一下slab分配器的设计原则。


基本设计原则

slab分配器试图在几个基本原则之间寻求一种平衡:

  • 频繁使用的数据结构也会频繁分配和释放,因此应当缓存它们
  • 频繁分配和回收必然会导致内存碎片(难以找到大块连续的可用内存)。为了避免这种现象,空闲链表的缓存会连续地存放。因为已释放的数据结构又会放回空闲链表,因此不会导致碎片。
  • 回收的对象可以立即投入下一次分配,因此,对于频繁的分配和释放,空闲链表能够提高其性能。
  • 如果分配器知道对象大小、页大小和总的高速缓存的大小这样的概念,它会做出更明智的决策。
  • 如果让部分缓存专属于单个处理器(对系统上的每个处理器独立而惟一),那么,分配和释放就可以在不加SMP锁的情况下进行。
  • 设计对存放的对象进行着色(colored),以防止多个对象映射到相同的高速缓存行(cacheline)

Linux的slab层在设计和实现时充分考虑了上述原则。


slab层的设计

slab层把不同的对象划分为所谓高速缓存(cache)组,其中每个高速缓存都存放不同类型的对象。每种对象类型对应一个高速缓存。例如,一个高速缓存用于存放进程描述符(task_struct 结构的一个空闲链表),而另一个高速缓存存放索引节点对象(struct inode)。注意,kmalloc() 接口建立在slab层之上,使用了一组通用高速缓存。

然后,这些高速缓存又被划分为slab (这也是这个子系统名字的来由)slab由一个或多个物理上连续的页组成一般情况下,slab也就仅仅由一页组成。每个高速缓存可以由多个slab组成。

每个 slab 都包含一些对象成员,这里的对象指的是被缓存的数据结构。每个slab处于三种状态之一:

  • 部分满

一个满的 slab 没有空闲的对象(slab中的所有对象都已被分配)。一个空slab没有分配出任何对象(slab中的所有对象都是空闲的)。一个部分满的slab有一些对象已分配出去,有些对象还空闲着当内核的某一部分需要一个新的对象时,先从部分满的slab中进行分配。

如果没有部分满的 slab,就从空的slab中进行分配如果没有空的slab,就要创建一个slab了。显然,满的 slab 无法满足请求,因为它根本就没有空闲的对象。这种策略能减少碎片。

下面举一个简单的例子。

我们知道struct inode 结构是磁盘索引节点在内存中的体现,而这些数据结构经常会频繁地创建和释放,因此,用slab分配器来管理它们就很有必要。因而 struct inode 就由 inode_cachep 高速缓存(这是一种标准的命名规范)进行分配的。这种高速缓存由一个或多个slab组成 —— 由多个slab组成的可能性大一些,因为这样的对象数量很大。每个slab包含尽可能多的struct inode对象。当内核请求分配一个新的inode结构时,内核就从部分满的slab或空slab (如果没有部分满的slab)返回一个指向已分配但未使用的结构的指针。当内核用完inode对象后,slab分配器就把该对象标记为空闲

下图显示了高速缓存、slab及对象之间的关系。


高速缓存描述符

每一个缓存都有一个类型为struct kmem_cache_s的缓存描述符,描述符中包含缓存的许多信息。这些信息中大多数值都是由函数kmem_cache_create()在缓存创建时设置或计算获得的,该函数定义在(mm/slab.c)我们先来看看缓存描述符中的一些字段以及它们存储的信息。

/**
 * 高速缓存描述符
 */
struct kmem_cache_s {
	···
	struct kmem_list3	lists;
	
	unsigned int		objsize;
	unsigned int	 	flags;	/* constant flags */
	unsigned int		num;	/* # of objs per slab */
	
	/* 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		slab_size;
	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);
	const char		*name;
	struct list_head	next;
};

重要的字段解释如下:

  • list字段中包含三个链表头,其中每个链表头对应的slab所处的三种状态之一:部分、完全和空闲,缓存正是通过这个数据结构引用slab。list本身是有slab描述符的list字段维护者的双向链表。
  • Obsize:存放缓存中对象的大小(以字节为单位),该值取决于缓存创建时请求的大小以及对其的需要。
  • flag:存放标志掩码,掩码描述缓存的固有特性
  • num:该字段存放缓存中每个 slab 所包含的对象数目。
  • name:name存放高速缓存名称。在创建slab时指定的名称,要与链表中。其他slab的名称进行比较,看是否有重名。如果发现相同名称的salb,那么struct
  • next:指向缓存描述符单链表中下一个缓存描述的指针

这个结构相当的大,Slab Allocator中对每种”对象”都有一个对应的cache每个cache管理多个slabSlab才是真正存放”对象”的地方。管理这些”对象”的结构就是slab描述符。


通用缓存描述符

通用缓存总是成对出现,其中存放预定大小的对象。一个缓存从DMA内存区分配对象,另一个缓存从普通内存区中分配

DMA缓存是在ZONE_DMA区中,而标准缓存则来自ZONE_NORMAL区

struct cache_sizes 存放通用缓存大小的所有信息。

/* Size description struct for general caches. */
struct cache_sizes {
	size_t		 cs_size;
	kmem_cache_t	*cs_cachep;
	kmem_cache_t	*cs_dmacachep;
};
  • cs_size:该字段存放该缓存中所包含的内存对象的大小
  • cs_cachep:该字段存放指向普通内存缓存描述符的指针,这种缓存存放的对象分配自 ZONE_NORMAL内存区。
  • cs_dmacachep:该字段存放指向DMA内存缓存描述符的指针,DMA内存缓存存放的对象分配自ZONE_DMA。

那么缓存描述符本身存放在哪里?slab分配器有一个缓存专门预留来解决该问题。cache_cache 缓存便是专门用来存放缓存描述符对象的。slab缓存是在系统自举是被静态初始化,以确保缓存描述符存储空间可用。


slab 描述符

高速缓存中的每个slab都有自己的 struct slab_s(等价于slab_t类型)的描述符。slab描述符可以存放在两个可能的地方。

  • 外部slab描述符:存放在slab外面,这种slab处于由cache_sizes所指向的普通高速缓存中。
  • 内部slab描述符:存放在slab内部,位于分配给slab的第一个页框的起始位置。

当对象小于512字节时,或者当内碎片在slab内部为slab及对象描述符留下足够的空间时,slab 分配器选择第二种方案

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描述符的三个双向循环链表中的一个(在高速缓存描述符中的slabs_full、slabs_partial或 slabs_free链表)
  • clouroff:slab中第一个对象的偏移
  • s_mem:指向 slab 中第一个”对象”(或被分配或空闲)。
  • inuse:当前所分配的slab中的对象个数。对于满或者部分满的slab,该值为正值;对于空闲slab,该值为0。
  • free:指向slab中的第一个空闲对象(如果有)。kmem_bufctl_t 数据类型链接处于同一slab中的所有对象。

普通和专用高速缓存

高速缓存被分为两种类型:普通和专用普通高速缓存只由slab分配器用于自己的目的,而专用髙速缓存由内核的其余部分使用

普通高速缓存是:

  • 第一个高速缓存叫做 kmem_cache,包含由内核使用的其余高速缓存的高速缓存描述符。cache_cache变量包含第一个高速缓存的描述符。
  • 另外一些高速缓存包含用作普通用途的内存区。内存区大小的范围一般包括13个几何分布的内存区。一个叫做 malloc_sizes 的表(其元素类型为cache_sizes)分别指向26个高速缓存描述符,与其相关的内存区大小为32,64,128,256,512,1024,2048,4096,8192,16384,32768,65536 和 131072 字节。对于每种大小,都有两个高速缓存:一个适用于ISA DMA分配,另一个适用于常规分配

在系统初始化期间调用 kmem_cache_init() 和 kmem_cache_sizes_init() 来建立普通高速缓存。

专用高速缓存是由kmem_cache_create() 函数创建的。这个函数首先先根据参数确定处理新高速缓存的最佳方法(例如,是在slab的内部还是外部包含slab描述符)。然后它从 cache_cache 普通高速缓存中为新的高速缓存分配一个高速缓存描述符,并把这个描述符插入到高速缓存描述符的cache_chain链表中(当获得了用于保护链表避免被同时访问的cache_chain_sem信号量后,插入操作完成)。

还可以调用 kmem_cache_destroy() 撤销一个高速缓存并将它从cache_chain 链表上删除。这个函数主要用于模块中,即模块装入时创建自己的高速缓存,卸载时撤销高速缓存。为了避免浪费内存空间,内核必须在撤销高速缓存本身之前就撤销其所有的slab。kmem_cache_shrink() 函数通过反复调用 slab_destroy() 撤销高速缓存中所有的slab

所有普通和专用高速缓存的名字都可以在运行期间通过读取 proc/slabinfo 文件得到。这个文件也指明毎个高速缓存中空闲对象的个数和已分配对象的个数。


与slab分配器有关的全局变量

下面是一组与slab分配器相关的全局变量:

  • cache_chche:它是一个特殊缓存的缓存描述符,其特殊性体现在该缓存存放的是其他所有缓存描述符。这个缓存的的名称是kmem_cache (在/proc/slabs/中的名字)。该缓存描述符是唯一一个静态分配的缓存描述符。
  • cache_chain:用作指向缓存描述符链表的指针的链表元素
  • cache_chain_sem:对cache_chain进行控制访问的信号量。每次向链表中添加元素(新缓存描述符) 时,分別调用 down() 和 up() 函数获取和释放信号量。
  • malloc_sizes[]:该数组存放DMA区的缓存描述符和与通用缓存对应的非DMA区的缓存描述符

在slab分配器初始化之前,这些结构已经存在,下面是创建过程:

/* internal cache of cache description objs */
/**
 * 第一个普通高速缓存
 */
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",
};

cache_cache 缓存描述符中若设置了 SLAB_NO_REAP 标志,那么即使系统内存紧张,该缓存也会在内核运行期间驻留内存。注意,cache_chain 信号量仅被定义,并未初始化初始化是在系统初始化时由函数 kmem_cache_init() 完成的

我们来看看该函数的实现细节:


/* These are the default caches for kmalloc. Custom caches can have other sizes. */
/**
 * 指向26个高速缓存描述符的表。
 * 与其相关的内存区大小为32,64,128,256,512,1024,4096,8192,32768,131072个字节。
 * 对于每种大小,都有两个高速缓存:一个适用于ISA DMA分配,一个适用于常规分配。
 */
struct cache_sizes malloc_sizes[] = {
#define CACHE(x) { .cs_size = (x) },
#include <linux/kmalloc_sizes.h>
	{ 0, }
#undef CACHE
};

这段代码初始化 malloc_sizes[] 数组,并按照在include/linux/kmalloc_sizes.h文件中定义的值设置 cs_size 字段。缓存大小的取值范围是32字节 ~ 131072字节,具体值取决于内核配置。

依靠这些全局变量,内核通过调用文件 init/main.c 中的函数 kmem_cache_init() 来继续初始化 slab 分配器。该函数负责初始化缓存链表、信号量、通用缓存、kmem_cache 缓存,本质上也就是初始化管理slab的slab分配器用到的所有全局变量。之后,专用缓存方可被创建。创建缓存的函数为 kmem_cache_create()


创建缓存

创建缓存包含三个步骤:

  • 描述符的分配和初始化
  • 计算slab着色和对象大小
  • 在 cache_chain 链表中添加缓存

通用缓存是由函数的 kmem_cache_init() 在系统初始化时创建的专用缓存则由函数 kmem_cache_create() 创建的

接下来我们分别来看看这两个函数。


kmem_cache_init()

该函数创建 cache_chain 和通用缓存,它是在初始化过程中被调用的。注意该函数名前有一个_init前缀,_init修饰的函数载入内存后会自举和初始化过程结束后会被销毁。

我们接下来对该函数的主干部分做以剖析。

/**
 * 建立普通高速缓存。
 */
void __init kmem_cache_init(void)
{
	size_t left_over;
	struct cache_sizes *sizes;
	struct cache_names *names;

	/*
	 * Fragmentation resistance on low memory - only use bigger
	 * page orders on machines with more than 32MB of memory.
	 */
	/*
	 * 下述代码确定一个slab使用多少页。一个slab中的页面数完全取决于系统有
	 * 多少可用内存。
	 */
	if (num_physpages > (32 << 20) >> PAGE_SHIFT)
		slab_break_gfp_order = BREAK_GFP_ORDER_HI;

	/* 1) create the cache_cache */
	/* 初始化cache_chain所存放的信号量 ache_chain_sem */
	init_MUTEX(&cache_chain_sem);
	/* 初始化cache_chain链表,所有的缓存描述符都存放在该链表中 */
	INIT_LIST_HEAD(&cache_chain);
	/* 向cache_chain链表中加入cache_cache描述符 */
	list_add(&cache_cache.next, &cache_chain);
	cache_cache.colour_off = cache_line_size();
	/* 创建每个CPU的缓存 */
	cache_cache.array[smp_processor_id()] = &initarray_cache.cache;

	cache_cache.objsize = ALIGN(cache_cache.objsize, cache_line_size());
	
	/*
	 * 下述代码执行完整性检查,也就是检查在 cache_cache 中是否至少分配了
	 * 一个缓存描述符,而且该代码还设置了 cache_cache 描述符的num字段,
	 * 并且计算还有多少剩余空间。这主要为了slab着色
	 * slab着色是一种内核用于降低缓存对齐所带来的性能下降的方法。
	 */
	cache_estimate(0, cache_cache.objsize, cache_line_size(), 0,
				&left_over, &cache_cache.num);
	if (!cache_cache.num)
		BUG();

	cache_cache.colour = left_over/cache_cache.colour_off;
	cache_cache.colour_next = 0;
	cache_cache.slab_size = ALIGN(cache_cache.num*sizeof(kmem_bufctl_t) +
				sizeof(struct slab), cache_line_size());


	/* 2+3) create the kmalloc caches */
	sizes = malloc_sizes;
	names = cache_names;

	/*
	 * 下述循环验证我们是否已经到达了 sizes 数组的末尾,sizes数组的
	 * 最后一个元素总是被设置为0,因此当我们到达数组的最后一个元素时,
	 * 判断条件就为真。 
	 */
	while (sizes->cs_size) {
		
		/*
		 * 下面代码为常规内存分配请求创建下一个kmalloc缓存,并且验证该
		 * 缓存是否为空。
		*/
		sizes->cs_cachep = kmem_cache_create(names->name,
			sizes->cs_size, ARCH_KMALLOC_MINALIGN,
			(ARCH_KMALLOC_FLAGS | SLAB_PANIC), NULL, NULL);

		/* Inc off-slab bufctl limit until the ceiling is hit. */
		if (!(OFF_SLAB(sizes->cs_cachep))) {
			offslab_limit = sizes->cs_size-sizeof(struct slab);
			offslab_limit /= sizeof(kmem_bufctl_t);
		}

		/*
		 * 下面代码为DMA内存分配创建缓存。
		*/
		sizes->cs_dmacachep = kmem_cache_create(names->name_dma,
			sizes->cs_size, ARCH_KMALLOC_MINALIGN,
			(ARCH_KMALLOC_FLAGS | SLAB_CACHE_DMA | SLAB_PANIC),
			NULL, NULL);
		/* 
		 * 找到sizes和names数组中的下一个元素
		*/
		sizes++;
		names++;
	}
	······

kmem_cache_create()

当通用缓存所提供的内存不能满足需要时,会调用该函数来创建专用缓存。创建专用缓存的步骤与创建通用缓存有相似之处,依次包括:创建、分配、初始化缓存描述符、对齐对象、对齐slab描述符、添加缓存到缓存链表中。该函数没有_init前缀,因此它被调用后,可以使用永久内存。

/**
 * 建立专用高速缓存。
 */
kmem_cache_t *
kmem_cache_create (const char *name, size_t size, size_t align,
	unsigned long flags, 
	void (*ctor)(void*, kmem_cache_t *, unsigned long),
	void (*dtor)(void*, kmem_cache_t *, unsigned long))

我们首先来看看 kmem_cache_create 函数的参数:

  • name:用于标识缓存的名字,它存放在缓存描述符的name字段中。
  • size:该参数指定缓存中包含的对象大小(以字节为单位),该值存放在缓存描述符的 objsize 字段中。
  • align:该值决定对象在一个页面上的位置。
  • flags:flags与slab相关。可以参考缓存描述符中的flag字段。
  • ctor与dtor:这两个字段分别指向这个内存区中创建和销毁对象时调用的构造函数和析构函数。该函数执行大范围的调试与完整性检查。
/* Get cache's description obj. */
/**
 * 从cache_cache普通高速缓存中为新的高速缓存分配一个高速缓存描述符。
 * 并把这个描述符插入到高速缓存描述符的cache_chain链表中。
 */
cachep = (kmem_cache_t *) kmem_cache_alloc(&cache_cache, 
		SLAB_KERNEL);
if (!cachep)
	goto opps;
memset(cachep, 0, sizeof(kmem_cache_t));

/**
 * 这段代码段决定缓存中对象的数量。大部分工作由函数cache_estimate()
 * 完成,计算出的对象的数量最终会被存放在缓存描述符的num字段中。
 */
do {
	unsigned int break_flag = 0;
cal_wastage:
	cache_estimate(cachep->gfporder, size, align, flags,
				&left_over, &cachep->num);
···
} while (1);

if (!cachep->num) {
	printk("kmem_cache_create: couldn't create cache %s.\n", name);
	kmem_cache_free(&cache_cache, cachep);
	cachep = NULL;
	goto opps;
}
/**
 * 在此之前,slab已经与硬件缓存对齐进行了着色。slab描述符中的color和
 * color_off字段已被填充,下面代码段初始化缓存描述符中的各个字段,
 * 与我们在kmem_cache_init()函数中看到了类似
 */
···
cachep->flags = flags;
cachep->gfpflags = 0;
if (flags & SLAB_CACHE_DMA)
	cachep->gfpflags |= GFP_DMA;
spin_lock_init(&cachep->spinlock);
cachep->objsize = size;
/* NUMA */
INIT_LIST_HEAD(&cachep->lists.slabs_full);
INIT_LIST_HEAD(&cachep->lists.slabs_partial);
INIT_LIST_HEAD(&cachep->lists.slabs_free);

if (flags & CFLGS_OFF_SLAB)
	cachep->slabp_cache = kmem_find_general_cachep(slab_size,0);
cachep->ctor = ctor;
cachep->dtor = dtor;
cachep->name = name;
···

/**
 * 该代码设置下一次回收缓存的时间。
 */
cachep->lists.next_reap = jiffies + REAPTIMEOUT_LIST3 +
				((unsigned long)cachep)%REAPTIMEOUT_LIST3;

/* Need the semaphore to access the chain. */
/**
 * 下面的代码初始化缓存描述符,并计算和存放与缓存相关的所有信息
 * 现在我们可将新的缓存描述符加入到cache_chain()链表中。
 */
down(&cache_chain_sem);
{
	struct list_head *p;
	mm_segment_t old_fs;

	old_fs = get_fs();
	set_fs(KERNEL_DS);
	list_for_each(p, &cache_chain) {
		kmem_cache_t *pc = list_entry(p, kmem_cache_t, next);
		char tmp;
		/* This happens when the module gets unloaded and doesn't
		   destroy its slab cache and noone else reuses the vmalloc
		   area of the module. Print a warning. */
		if (__get_user(tmp,pc->name)) { 
			printk("SLAB: cache with size %d has lost its name\n", 
				pc->objsize); 
			continue; 
		} 	
		if (!strcmp(pc->name,name)) { 
			printk("kmem_cache_create: duplicate cache %s\n",name); 
			up(&cache_chain_sem); 
			unlock_cpu_hotplug();
			BUG(); 
		}	
	}
	set_fs(old_fs);
}

/* cache setup completed, link it into the list */
list_add(&cachep->next, &cache_chain);
up(&cache_chain_sem);
unlock_cpu_hotplug();
opps:
if (!cachep && (flags & SLAB_PANIC))
	panic("kmem_cache_create(): failed to create slab `%s'\n",
		name);
	return cachep;
}

创建slab 与 cache_grow()

当缓存刚被创建时,其中没有slab。事实上,只有当一个对象发出请求的确需要slab时才真正分配slab。这种情况出现在缓存描述符的 list. slabs_partial 和 lists.slabs_free 字段都为空时。此时我们并不关心对内存的请求如何转化成对特定缓存中对象的请求,而是假定这种转化已经发生,把关注点转移到slab分配器内的具体实现上。

函数 cache_grow() 创建 slab 并将其放在在缓存中。当我们创建slab时,不但分配和初始化其描述符,而且还需要分配物理内存,为此,我们需要与伙伴系统交互以请求页面。这个工作是由函数 kmem_getpages() 完成的


cache_grow()

函数cache_grow()用来在缓存中增加一个slab,当缓存中没有空闲对象可用时就调用该函数。这种情况发生在lilist. slabs_partial 和 lists.slabs_free都为空时。

/**
 * 给高速缓存分配一个新的slab。
 */
static int cache_grow (kmem_cache_t * cachep, 
int flags, int nodeid)
{
···

传给该函数的参数有下面两个:

  • cachep:它是需要扩充的缓存对应的缓存描述符。
  • flags:这些标志将用于创建slab。
/**
 * 下述代码禁用中断且锁定描述符,为操作缓存描述符的字段做准备。
 */
check_irq_off();
spin_lock(&cachep->spinlock);

/**
 * 下述代码解锁缓存描述符且重新启用中断。
 */
spin_unlock(&cachep->spinlock);
if (local_flags & __GFP_WAIT)
	local_irq_enable();
	/* Get mem for the objs. */
	/**
	 * 调用kmem_getpages从分区页框分配器获得一组页框来存放一个单独的slab
	 * 下述代码与伙伴系统交互以获取页面存放slab
	 */
	if (!(objp = kmem_getpages(cachep, flags, nodeid)))
		goto failed;

	/* Get slab management. */
	/**
	 * 获得一个新的slab描述符,把slab描述符存放在它该存放的地方,slab
	 * 描述符存放在slab自身或者第一个通用缓存中
	 */
	if (!(slabp = alloc_slabmgmt(cachep, objp, offset, 
	local_flags)))
		goto opps1;

	/**
	 * set_slab_attr扫描分配给新slab的页框的所有页描述符
	 * 并将高速缓存描述符和slab描述符的地址分别赋给页描述符中lru字段的next
	 * 和prev字段,这是不会出错的,因为只有当页框空闲时,伙伴系统的函数才会
	 * 使用lru字段,而只要涉及伙伴系统,slab分配器函数处理的页框就不空闲。
	 * 注意:这个字段因为也会被页框回收算法使用,所以包含了这些隐含的约定,
	 * 总会让人困惑,也许会带来一些意外的后果。
	 */
	set_slab_attr(cachep, slabp, objp);

	/**
	 * cache_init_objs将构造方法(如果有)应用到新的slab包含的所有对象上。
	 * 初始化slab中的所有对象
	 */
	cache_init_objs(cachep, slabp, ctor_flags);
	/**
	 * 由于我们打算访问并且改变描述符字段,所以需要禁用中断并锁定数据。
	 */
	if (local_flags & __GFP_WAIT)
		local_irq_disable();
	check_irq_off();
	spin_lock(&cachep->spinlock);

	/* Make slab active. */
	/**
	 * 添加新的slab描述符到缓存描述符的 lists.slabs_free字段中,
	 * 更新记录缓存大小的统计信息
	 */
	list_add_tail(&slabp->list, &(list3_data(cachep)->slabs_free));
	STATS_INC_GROWN(cachep);
	list3_data(cachep)->free_objects += cachep->num;

	/* 解开自旋锁并成功返回 */
	spin_unlock(&cachep->spinlock);
	return 1;
	
	/* 下述代码会在页请求出错时被调用,基本上是释放请求的页。*/
opps1:
	kmem_freepages(cachep, objp);
	
	/* 下述代码启用中断,这样就允许中断传出了 */
failed:
	if (local_flags & __GFP_WAIT)
		local_irq_disable();
	return 0;
}

slab的销毁:退还内存与kmem_cache_destroy()

缓存与slab都可被销毁。缓存可被缩减或者销毁以返还其占用的内存给空闲内存池。内核在系统内存过低时会调用析构函数。在任意一种情况下,slab也可被销毁且将与之对应的页面返还给伙伴系统,以便重复利用。函数 kmem_cache_destroy() 用来销毁缓存。其中与伙伴系统的交互是通过函数kmem_freepages()完成的。


kmem_cache_destroy()

如果要删除缓存,有几个实例可以使用。动态加载模块(假定载入和卸载过程中没有固定的永久内存预留)创建的缓存就必须在卸载时被销毁,从而可以释放内存,确保在下次模块载入时不出现重复缓存。因此,通常以这种方式销毁专用缓存

销毁缓存的步骤与创建缓存的步骤相逆。在销毁缓存时不用关心对齐问题,只需要删除缓存描述符并释放内存即可。销毁缓存步骤可概括如下:

  • 从缓存链表中删除缓存;
  • 删除slab描述符;
  • 删除缓存描述符。
/**
 * 撤销一个高速缓存并将它从cache_chain链表上删除
 * 主要用于模块中。
 * 该函数的参数cache是一个指针,指向待销毁缓存的描述符。
 */
int kmem_cache_destroy (kmem_cache_t * cachep)
{
	int i;
	
	/**
	 * 下述代码段完成完整性检查,其中包括确认程序当前不在中断上下文中,
	 * 并且缓存描述符不为空。
	 */
	if (!cachep || in_interrupt())
		BUG();
		
	/*
	 * 下述代码段获得cache_chain信号量,从缓存链中删除指定缓存,释放
	 * cache_chain
	 */
	down(&cache_chain_sem);
	list_del(&cachep->next);
	up(&cache_chain_sem);

	/*
	 * 该代码段负责释放未使用的slab。若函数__cache_shrink返回真,则表
	 * 明缓存中仍然存在有slabs,因此不可销毁缓存。这样要求我们按相反的顺
	 * 序执行前面的步骤,重新将缓存描述符插入cache_chain链表中,当然同
	 * 样需要先获取cache_chain信号量,操作完成后再释放它。
	 */
	if (__cache_shrink(cachep)) {
		slab_error(cachep, "Can't free all objects");
		down(&cache_chain_sem);
		list_add(&cachep->next,&cache_chain);
		up(&cache_chain_sem);
		unlock_cpu_hotplug();
		return 1;
	}

	······

	/* 最后,释放缓存描述符,结束操作。 */
	kmem_cache_free(&cache_cache, cachep);

	unlock_cpu_hotplug();

	return 0;
}

内存请求路径

到目前为止,虽然我们已经讨论了 slab分配器,但还未与任何实际的内存请求结合起来。除了缓存初始化函数外,我们还没有说明如何把这些函数配合在一起调用。因此,从现在开始我们来跟踪有关的内存请求的控制流程


kmalloc()

当内核必须获得字节大小分组的内存块时,就需要使用函数kmalloc(),它实际上会调用到函数kmem_getpages() 完成实际分配。调用路径如下所示:

void * __kmalloc (size_t size, int flags)
{
  • size : size是请求的字节数。
  • flag:flags指定内存请求的类型。这些标志将被传递给伙伴系统而不会影响kmalloc() 的行为。
	struct cache_sizes *csizep = malloc_sizes;
	
	/**
	 * 在查找缓存的过程中,找到所包含的对象大小大于所需大小的第一个缓存。
	 */
	for (; csizep->cs_size; csizep++) {
		if (size > csizep->cs_size)
			continue;
	
	/* 从flags参数指定的内存管理区中分配对象。 */
		return __cache_alloc(flags & GFP_DMA ?
			 csizep->cs_dmacachep : csizep->cs_cachep, flags);
	}
	return NULL;
}

kmem_cache_alloc()

kmem_cache_alloc()函数是对函数 __cache_alloc() 的包装。它实际不实现任何额外的功能,其参数按如下方式传递给函数:

/**
 * 获得新的slab对象
 * cachep-指向高速缓存描述符。新空闲对象必须从该高速缓存描述符获得。
 * flag-表示传递给分区页框分配器函数的标志。
 */
void * kmem_cache_alloc (kmem_cache_t *cachep, int flags)
{
	return __cache_alloc(cachep, flags);
}
  • cachep:cachep参数是我们的待分配对象的缓存描述符
  • flags:flags表示内存请求方式,它是按照函数kmalloc()的要求直接传递过来的。内核提供的函数kfree()接口用于释放由 kmalloc() 分配的字节大小的内存块,它接收的参数为指向由 kmalloc() 执行完毕所返回的内存的指针。

下图描述了由 kfree() 到kmem_free_pages() 的调用过程。

调用路径如下所示:

猜你喜欢

转载自blog.csdn.net/ZYZMZM_/article/details/90707778
今日推荐