[Linux操作系统分析]内存分配算法-伙伴和slab

文章目录

  • 内部碎片与外部碎片
  • 伙伴,别名Buddy(较好的解决外部碎片问题)
      • 申请过程
      • 释放过程
  • slab算法(解决内部碎片)
    • slab有两种,一种是专用cache,如下分析:
      • **总结一下基本思想:将内核中经常使用的对象(比如进程描述符,文件描述符),他们也是需要内存的,所以一开始就直接分配好,按照所占用的内存大小分门别类放在下文讲的 kmem_cache (其实就和伙伴一摸一样),下次用到直接拿去用,用完在放回原位置就行了**
      • 如何创建新 slab 缓存、向缓存中增加内存、销毁缓存的应用程序接口(API)以及 slab 中对对象进行分配和释放操作的函数?
          • 创建新 slab 缓存 kmem_cache_create (flags参数->缓存行)
          • 销毁缓存(kmem_cache_destroy)
          • slab 中对对象进行分配 kmem_cache_alloc
          • slab 中对对象进行释放 kmem_cache_free
    • 第二种:通用 cache (会使用`kmalloc` 函数来申请)

先行阅读:Linux 内存管理机制(内核空间层面分析)

内部碎片与外部碎片

在内存管理中

  • 内部碎片是已经被分配出去(能明确指出属于哪个进程)的内存空间,却不能被利用的内存空间.(就是已经被分配出去的内存空间大于请求所需的内存空间,而导致有些内存自己不使用,别的也不能使用
  • 外部碎片是指还没有分配出去,但是由于大小太小而无法分配给申请空间的新进程的内存空间空闲块。

伙伴,别名Buddy(较好的解决外部碎片问题)

唉,我原先以为这两个不一样,面试的时候就说只知道Buddy,不知道伙伴,原来这两个是一样的,尴尬尴尬啊!!!!

关于这个算法,我前面的文章有讲过可以先看看,这里我也就简单总结一下即可.

解决的问题是频繁地请求和释放不同大小的一组连续页框,必然导致在已分配页框的块内分散了许多小块的空闲页面,由此带来的问题是,即使有足够的空闲页框可以满足请求,但要分配一个大块的连续页框可能无法满足请求。
在这里插入图片描述
比如申请5个.

伙伴算法:
[Linux操作系统分析]内存分配算法-伙伴和slab_第1张图片
算法将所有空闲的页面分组划分为MAX_ORDER(11)个页面块链表进行管理,其中MAX_ORDER定义:
/include/linux/mmzone.h

/* Free memory management - zoned buddy allocator.  */
#ifndef CONFIG_FORCE_MAX_ZONEORDER
#define MAX_ORDER 11
#else
#define MAX_ORDER CONFIG_FORCE_MAX_ZONEORDER
#endif
#define MAX_ORDER_NR_PAGES (1 << (MAX_ORDER - 1))

   1:  struct zone{
   2:     ....
   3:     struct free_area    free_area[MAX_ORDER];  
   4:     ....
   5:  }

地址连续,双向链表组织,最小4K,最大4M(2^10).

Linux2.6为每个管理区使用不同的伙伴系统,内核空间分为三种区,DMA,NORMAL,HIGHMEM,对于每一种区,都有对应的伙伴算法

每一条链表的第一个页框的物理地址是该块大小的整数倍。例如,大小为 16个页框的块,其起始地址是 16 * 2^12 (2^12 = 4096,这是一个常规页的大小)的倍数。

//对应于每一个链表
struct free_area {
	struct list_head	free_list[MIGRATE_TYPES];//空闲块双向链表
	unsigned long		nr_free;//空闲块的数目
};

申请过程

看链接

释放过程

页框块在释放时,会主动将两个连续大小的页框块合并为一个较大的页框块。
满足以下条件的两个块称为伙伴:

  • 两个块的大小相同
  • 两个块的物理地址连续

伙伴算法把满足以上条件的两个块合并为一个块,该算法是迭代算法,如果合并后的块还可以跟相邻的块进行合并,那么该算法就继续合并。

深入分析的话有:链表的一些操作和位图(本来不想再深入了,但表示想了一下还是 再往下研究一下吧)

slab算法(解决内部碎片)

slab有两种,一种是专用cache,如下分析:

它的基本思想是将内核中经常使用的对象放到高速缓存中,并且由系统保持为初始的可利用状态。比如进程描述符,文件描述符等,内核中会频繁对此数据进行申请和释放

需要注意的是slab分配器只管理内核的常规地址空间(直接被映射到内核地址空间的ZONE_NORMAL和ZONE_DMA)。

总结一下基本思想:将内核中经常使用的对象(比如进程描述符,文件描述符),他们也是需要内存的,所以一开始就直接分配好,按照所占用的内存大小分门别类放在下文讲的 kmem_cache (其实就和伙伴一摸一样),下次用到直接拿去用,用完在放回原位置就行了

采用了slab分配器后,在释放内存时,slab分配器将释放的内存块保存在一个列表中,而不是返回给伙伴系统。在下一次内核申请同样类型的对象时,会使用该列表中的内存。slab分配器分配的优点:

  • 可以提供小块内存的分配支持
  • 不必每次申请释放都和伙伴系统打交道,提供了分配释放效率
  • 如果在slab缓存的话,其在CPU高速缓存的概率也会较高。
  • 伙伴系统的操作对系统的数据和指令高速缓存有影响,slab分配器降低了这种副作用
  • 伙伴系统分配的页地址都页的倍数,这对CPU的高速缓存的利用有负面影响,页首地址对齐在页面大小上使得如果每次都将数据存放到从伙伴系统分配的页开始的位置会使得高速缓存的有的行被过度使用,而有的行几乎从不被使用。slab分配器通过着色使得slab对象能够均匀的使用高速缓存,提高高速缓存的利用率

缺点:

  • 对于微型嵌入式系统,它显得比较复杂,这是可以使用经过优化的slob分配器,它使用内存块链表,并使用最先适配算法
  • 对于具有大量内存的大型系统,仅仅建立slab分配器的数据结构就需要大量内存,这时候可以使用经过优化的slub分配器

OK,以上就是其基本思想了.呼~终于理解了!!!下面来介绍一下其内部实现:

图 1 给出了 slab 结构的高层组织结构。在最高层是 cache_chain,这是一个 slab 缓存的链接列表。可以用来查找最适合所需要的分配大小的缓存(遍历列表)。cache_chain 的每个元素都是一个 kmem_cache 结构的引用(称为一个 cache)。它定义了一个要管理的给定大小的对象池(固定大小,即分门别类)。(kmem_cache == 对象池
[Linux操作系统分析]内存分配算法-伙伴和slab_第2张图片
每个缓存都包含了一个 slabs 列表,这是一段连续的内存块(通常都是页面)。存在 3 种 slab:

  • slabs_full 完全分配的 slab
  • slabs_partial 部分分配的 slab
  • slabs_empty 空 slab,或者没有对象被分配

slab 列表中的每个 slab 都是一个连续的内存块(一个或多个连续页),它们被划分成一个个对象。这些对象是从特定缓存中进行分配和释放的基本元素。注意 slab 是 slab 分配器进行操作的最小分配单位,通常来说,每个 slab 被分配为多个对象。
[Linux操作系统分析]内存分配算法-伙伴和slab_第3张图片
由于对象是从 slab 中进行分配和释放的,因此单个 slab 可以在 slab 列表之间进行移动。例如,当一个 slab 中的所有对象都被使用完时,就从 slabs_partial 列表中移动到 slabs_full 列表中

如何创建新 slab 缓存、向缓存中增加内存、销毁缓存的应用程序接口(API)以及 slab 中对对象进行分配和释放操作的函数?

创建新 slab 缓存 kmem_cache_create (flags参数->缓存行)
/*kmem_cache 结构包含了每个中央处理器单元(CPU)的数据、
一组可调整的(可以通过 proc 文件系统访问)参数、
统计信息和管理 slab 缓存所必须的元素。*/

struct kmem_cache *
kmem_cache_create(const char *name, 
		unsigned int size, 
		unsigned int align,
		slab_flags_t flags, 
		void (*ctor)(void *))
{
	return kmem_cache_create_usercopy(name, size, align, flags, 0, 0,
					  ctor);
}

name 参数定义了缓存名称,proc 文件系统(在 /proc/slabinfo 中)使用它标识这个缓存。 size 参数指定了为这个缓存创建的对象的大小, align 参数定义了每个对象必需的对齐。 flags 参数指定了为缓存启用的选项。这些标志如表 1 所示。
[Linux操作系统分析]内存分配算法-伙伴和slab_第4张图片
后面的参数ctor定义了一个对象构造器。构造器和析构器是用户提供的回调函数。当从缓存中分配新对象(kmem_cache_alloc函数)时,可以通过构造器进行初始化。

在创建缓存之后, kmem_cache_create 函数会返回对它的引用。注意这个函数并没有向缓存分配任何内存。一切的分配添加都是通过kmem_cache_alloc函数执行的.

销毁缓存(kmem_cache_destroy)
void kmem_cache_destroy(struct kmem_cache *s)
{
	int err;

	if (unlikely(!s))
		return;

	flush_memcg_workqueue(s);

	get_online_cpus();
	get_online_mems();

	mutex_lock(&slab_mutex);

	s->refcount--;
	if (s->refcount)
		goto out_unlock;

	err = shutdown_memcg_caches(s);
	if (!err)
		err = shutdown_cache(s);

	if (err) {
		pr_err("kmem_cache_destroy %s: Slab cache still has objects\n",
		       s->name);
		dump_stack();
	}
out_unlock:
	mutex_unlock(&slab_mutex);

	put_online_mems();
	put_online_cpus();
}
slab 中对对象进行分配 kmem_cache_alloc

kmem_cache_zalloc函数就是会memset操作一下

void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags) 

这个函数从缓存中返回一个对象。注意如果缓存目前为空,那么这个函数就会调用 cache_alloc_refill 向缓存中增加内存。

slab 中对对象进行释放 kmem_cache_free
void kmem_cache_free( struct kmem_cache *cachep, void *objp );

第二种:通用 cache (会使用kmalloc 函数来申请)

如果不涉及到特定类型的内存,而只是普通类型的内存,可以使用kmalloc和kfree来申请和释放缓存。

循环遍历可用缓存来查找可以满足大小限制的缓存。找到之后,就(使用 __kmem_cache_alloc)分配一个对象。要使用 kfree 释放对象,从中分配对象的缓存可以通过调用 virt_to_cache 确定。这个函数会返回一个缓存引用,然后在 __cache_free 调用中使用该引用释放对象。

过程实例:

kmem_cache_create
kmem_cache_alloc
kmem_cache_destroy //调用者必须确保在执行销毁操作过程中,不要从缓存中分配对象。

参考:https://www.ibm.com/developerworks/cn/linux/l-linux-slab-allocator/index.html

你可能感兴趣的:(操作系统)