【Linux】Linux内核空间的slab分配模式

内核在运行时,经常需要在内核空间3G~3G+high_memory这个内存空间申请动态内存,以存放一些结构类型的数据。例如,在创建一个程序时,它就要为该程序控制块task_struct申请一段内存空间;在撤销这个程序时,又要释放这个空间。在内核中,由于这种结构体类型数据的数量相当大,而数据所占的内存空间又不可能刚好是一个或多个页框,所以在以页框为最小分配单位的分配方法里,这种数据产生的碎片就相当多,内存空间浪费比较惊人。

于是,这就促使人们在不破坏页管理机制的条件下,考虑更小的内存分配粒度。所以自从Linux2.2开始,设计者在Linux系统中采用了一个叫做slab的小对象分配模式。

这里区分一下外部碎片和内部碎片:

外部碎片:什么是外部碎片呢?我们通过一个图来解释:

假设这是一段连续的页框,阴影部分表示已经被使用的页框,现在需要申请一个连续的5个页框。这个时候,在这段内存上不能找到连续的5个空闲的页框,就会去另一段内存上去寻找5个连续的页框,这样子,久而久之就形成了页框的浪费。称为外部碎片。内核中使用伙伴算法的迁移机制很好的解决了这种外部碎片。

内部碎片:当我们申请几十个字节的时候,内核也是给我们分配一个页,这样在每个页中就形成了很大的浪费。称之为内部碎片。内核中引入了slab机制去尽力的减少这种内部碎片。

缓冲区和slab的概念

slab模式是20世纪90年代提出的一个为小数据分配内存空间的方法。在设计slab模式时,人们看到:内核的这些小数据虽然量很大,但是种类并不多,于是就提出了这样一个思想:把若干的页框合在一起形成一大存储块——slab,并在这个slab中只存储同一类数据,这样就可以在这个slab内部打破页的界限,以该类型数据的大小来定义分配粒度,存放多个数据,这样就可以尽可能地减少页内碎片了。在Linux中,多个存储同类数据的slab的集合叫做一类对象的缓冲区——cache。注意,这不是硬件的那个cache,只是借用这个名词而已。

采用slab模式的另一个考虑就是:一些内核数据不但需要为其分配内存,而且还经常需要对它们进行一些比较费时的初始化操作。这样,当这些数据在内核运行时频繁地创建和撤销,就消耗了大量的CPU时间。而slab模式恰好就能解决这个问题。

但是目前,Linux中的缓冲区只才在用了前一个功能,并没有使用构造函数和析构函数。

slab分配机制:slab分配器是基于对象进行管理的,所谓的对象就是内核中的数据结构(例如:task_struct、file_struct 等)。相同类型的对象归为一类,每当要申请这样一个对象时,slab分配器就从一个slab列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中,而不是直接返回给伙伴系统,从而避免外部碎片。

Linux在一个缓冲区中,用链表来管理多个slab,而且把这些slab分成三段:第一段是所有已经被对象充满的slab;第二段是半满的slab;第三段是空闲的slab。slab模式的示意图如下:

也就是说,Linux 的slab 可有三种状态:

  • 满的:slab 中的所有对象被标记为使用;
  • 空的:slab 中的所有对象被标记为空闲;
  • 部分:slab 中的对象有的被标记为使用,有的被标记为空闲。

slab 分配器首先从部分空闲的slab 进行分配。如没有,则从空的slab 进行分配。如没有,则从物理连续页上分配新的slab,并把它赋给一个cache ,然后再从新slab 分配空间。

总之,一个缓冲区是一个特殊的内存区,其中有多个slab,一个slab占用的内存空间必须为整数页并且是连续的,但在slab内部却没有页的概念;而一个slab所占用的页框的数目,则是在以保证产生的页内碎片最小为目标,由负责创建缓冲区的内核函数kmem_cache_create()调用slab分配器经过计算得出的。

在文件mm/slab.c中定义的slab的数据结构如下:

struct slab {
	struct list_head list;        //链表结构
	unsigned long colouroff;        //对象区的起点与slab起点之间的偏移
	void *s_mem;		/* 对象区在slab中的起点 */
	unsigned int inuse;	/* 记录已分配对象空间数目的计数器 */
	kmem_bufctl_t free;        //指向了对象链中的第一个空闲对象
	unsigned short nodeid;
};

同一个缓冲区的slab按对象空间已满、半满和全空顺序组成了链表。为了解slab中有多少对象空间已经被占用,在链表的结构中有成员inuse;为给内核新申请的对象分配空间,在结构中有成员free。

Linux用数据结构kmem_cache_t描述一个缓冲区,并管理该缓冲区中的slab链表。

slab的优点:

  • 内核通常依赖于对小对象的分配,它们会在系统生命周期内进行无数次分配。slab 缓存分配器通过对类似大小的对象进行缓存而提供这种功能,从而避免了常见的碎片问题;
  • slab 分配器还支持通用对象的初始化,从而避免了为同一目的而对一个对象重复进行初始化;
  • slab 分配器还可以支持硬件缓存对齐和着色,这允许不同缓存中的对象占用相同的缓存行,从而提高缓存的利用率并获得更好的性能。

slab的缺点:

  • 较多复杂的队列管理。在slab分配器中存在众多的队列,例如针对处理器的本地缓存队列,slab中空闲队列,每个slab处于一个特定状态的队列之中。所以,管理太费劲了。
  • slab管理数据和队列的存储开销比较大。每个slab需要一个struct slab数据结构和一个管理者kmem_bufctl_t型的数组。当对象体积较小时,该数组将造成较大的开销(比如对象大小为32字节时,将浪费1/8空间)。同时,缓冲区针对节点和处理器的队列也会浪费不少内存。
  • 缓冲区回收、性能调试调优比较复杂。

slab缓冲区有专用缓冲区和通用缓冲区之分。

专用缓冲区

slab专用缓冲区主要用于内核频繁使用的一些数据结构,例如task_struct、mm_struct、vm_area_struct、file、dentry、inode等。

调用内核函数kmem_cache_create()可以创建一个专用缓冲区。该函数的原型如下:

struct kmem_cache * kmem_cache_create (
            const char *name,             //缓冲区名称
            size_t size,                 //对象大小
            size_t offset,                //偏移量
	        unsigned long c_flags,         //标志
            void (*ctor)(void *objp,...),    //构造函数
            void (*ctor)(void *objp,...))    //析构函数
{
	...
}

前面提到,Linux没有对缓冲区中的对象进行构造和析构,因此在参数中的构造函数和析构函数都被置为NULL。

函数调用成功后,其返回值为创建的缓冲区的指针。

如果要在缓冲区中申请一个对象的控件,则需调用内核函数kmem_chche_alloc()。

一个对象在使用之后,还要调用内核函数kmem_cache_free()将其占有的空间归还缓冲区并由缓冲区来管理。注意,是归还缓冲区,使已经被释放的对象空间成为一个空闲对象空间,而不是归还内存。

当认为某一个缓冲区确实不需要使用了,则需调用内核函数kmem_cache_distory()把缓冲区的控件归还内存。但一般不需要这样做,因为内核的守护程序kswapd会定时对slab中的空闲对象进行必要的回收工作。

这样看来,缓冲区有点像内存的一个特区。

通用缓冲区

对于内核的一些初始化工作量不大的数据结构可以使用通用缓冲区。在Linux中通用缓冲区按可分配对象空间的大小,分为可以容纳32字节大小对象的缓冲区、64字节大小对象的缓冲区……128KB对象大小的缓冲区。自通用缓冲区申请对象空间的内核函数为kmalloc(),而释放对象空间的函数为kfree()。

因为在本文开头提到:在内核空间3G~3G+high_memory这个内存空间分配空间。

Linux内存管理的总貌

分页及页交换技术是实现虚拟内存的基础,页表及MMU是实现虚拟内存的保障。

Linux内存管理的总貌如下图所示:

交换模块swap

这个模块负责页的换入和换出,从物理内存中淘汰最近没有访问的页面,保存近来访问的页面。该模块由以下几个部分组成:

  • page_io.c:读写交换文件的功能函数;
  • swap_state.c:修改高速缓存的函数;
  • swapfile.c:完成换入换出的系统调用;
  • swap.c:定义交换所需要的数据结构和常量;
  • kswapd:一个内核程序,周期性地处理物理内存与虚拟内存之间的交换。

核心内存管理模块core

这个模块负责核心内存的管理,即对页的管理。该模块的管理功能函数可以被核心的其他子系统调用。该模块的功能函数有:

  • page_alloc.c:负责处理页的释放、回收和分配;
  • memory.c:负责申请页面。

结构特定的模块

这个模块是在不同的硬件平台上实现虚拟内存的基础,通过执行命令来改变硬件MMU的虚拟地址映射,在发生页错误时提供公有的方法来通知其他内核子系统。实现该模块的源程序有:

  • mm/fault.c:处理页异常;
  • mm/init.c:内存初始化。

猜你喜欢

转载自blog.csdn.net/qq_38410730/article/details/81138790