物理内存分配与回收(3) 之slab分配机制

时间:2022-01-24 19:33:28

      采用伙伴算法分配内存时,每次至少分配一个页面。但当请求分配的内存大小为几十字节或几百字节时又应如何处理?如何在一个页面中分配小的内存区?小内存区的分配所产生的内碎片又如何解决?

      内存管理的开发者采用了一种叫做Slab的分配模式,Slab分配器是基于对象进行管理,相同类型的对象归为一类(如进程描述符就是一类)。当要申请这样一个对象时,Slab分配器就从Slab列表中分配一个这样大小的的单元出去,当要释放时,将其重新保存在该列表中。当以后要请求新对象时,就可以从内存直接获取而不用重复初始化。

Slab的提出基于以下几点考虑:

(1)内核对内存区的分配取决于所存放数据的类型。例如,当给用户进程分配页面时,内核调用__get_free_pages()函数,并用0填充所分配的页面。而给内核数据结构分配页面时,则需要一定的操作,要对数据结构所在的内存初始化,不用的时候要收回内存区。因此,Slab中引入了对象的概念,所谓对象就是存放一组数据结构的内存区,其方法则是用构造或析构函数(初始化与回收内存区)。为了避免从父初始化对象,Slab分配模式并不丢弃已分配的对象,而是释放但把他们依然保留在内存中。当以后又要调用分配用一对象时,就可以直接在内存获取而不用进行初始化。

      Linux中对Slab分配模式有所改进,它对内存区的处理并不需要进行初始化或回收。处于效率的考虑,Linux并不调用对象的构造或析构函数,而是把指向这两个函数的指针都置为空。Linux中引用了Slab的主要目的是为了减少对伙伴算法的调用次数。

(2)实际上,内核经常反复时用某一内存区。例如,只要内核创建一个新的进程,就要为该进程相关的数据结构(PCB,打开文件对象等)分配内存区。当进程结束时,收回这些内存区。因为进程的创建和撤销都十分的频繁,因此,Linux的早期版本把大量的时间花费在反复分配或回收其内存。从Linux2.2开始,把那些频繁使用的页面保存在高速缓存区中并重新使用(在开机时创建好,需要时直接调用,不需要时从哪取的还回去)。

(3)可以根据内存区的使用频率对其分类。对于预期频繁使用的内存区,可以创建一组特定大小的专用缓冲区来进行处理,以避免内碎片的产生。对于较少使用的内存区,可以创建一组通用缓冲区来处理,即使这种处理模式产生碎片,也对整个系统的性能影响不大。

(4)硬件高速缓存的使用,又为尽量减少对伙伴算法调用提供了另一个理由,因为对伙伴算法的每次调用都会弄脏硬件高速缓存。因此,这就增加了对内存的平均访问次数。

      Slab分配模式把对象分组放进缓冲区(尽管英文中使用了Cache这个词,但实际上指的是内存中的区域,而不是指硬件高速缓存)。因为缓冲区的组织和管理与硬件高速缓存的命中率密切相关,因此,Slab缓冲区并非由各个对象直接构成,而是由一连串的“大块(Slab)“构成。而每个大块中则包含了若干个同类型的对象,每个大块中则包含了若干个同类型的对象。

物理内存分配与回收(3) 之slab分配机制

      实际上,缓冲区就是主存中的一片区域,把这片区域划分为多个块,每块就是一个Slab,每个Slab由一个或多个页面组成,每个Slab中存放的就是对象。


1.Slab专用缓冲区的建立和释放

      专用缓冲区主要用于频繁使用的数据结构。在开机时建立一定数量的Slab专用缓冲区,当特有的进程需要时,直接从专用缓冲区取出来直接用,用完后还给缓冲区,等待下一个特定的进程使用。缓冲区用kmem_cache_t类型描述的,通过keme_cache_create()来建立的,函数原型为:

     keme_cache_t *keme-cache_create(const char *name. size_t size, size_t offset, unsigned long c_flags,

                                    void(*ctor )(void *objp, keme_cache_t *cachep, unsigned long flags),

                                    void(*dtor )(void *objp, keme_cache_t *cachep, unsigned long flags))       

(1)name:缓冲区名(19个字符)。

(2)size:对象大小。

(2)offset:在缓冲区内第一个对象的偏移,用来确定在页内进行对齐的位置,缺省为0,表示标准对齐。

(4)c_flags:对缓冲区的设置标志如下。

a.  SLAB_HWCACHE_ALIGN:表示与第一个缓冲区的缓冲行边界(16或32字节)对齐。

b.  SLAB_NO_PEAP:不允许系统回收内存。

c.  SLAB_CACHE_DMA:表示Slab使用的是DMA内存。

(5)ctor:构造函数(一般为NULL)。

(6)dtro:析构函数(一般为NULL)。

(7)objp:指向对象的指针。

(8)cachep:指向缓冲区。

      但是,函数kmem_cache_create()所创建的缓冲区还没有包含任何Slab,因此,也没有空闲的对象,只有以下两种的条件都为真时,才给缓冲区分配slab。

(1)已发出一个分配新对象的请求。

(2)缓冲区不包含任何空闲对象。(不包含对象才需要分配)

      当从内核卸载一个模块时,同时需要撤销这个模块中的数据结构所建立的缓冲区,这是通过kmem_cache_destroy()函数完成的‘

      创建缓冲区后,就可以通过下列函数从中获取对象:

      void *kmem_cache_alloc(kmem_cache_t *cachep, int flags)

      该函数从给定的缓冲区cachep中返回一个指向对象的指针。如果缓冲区中所有的Slab中都没有空闲的对象,那么Slab必须调用__get_free_pages()获取新的页面,flags是传地给该函数的值,一般应该是GFP_KERNEL或GFP_ATOMIC。

      最后一个释放对象,并把它返回给原先的Slab,可以用下面这个函数:

      void kmem_cache_free(keme_cache_T *cachep, void *objp)

       该函数将对象还给缓存,并让其标记为空闲状态。


2.Slab分配举例

      下面举一个例子,这个例子是用task_struct 结构(进程控制块),代码取自kernel/fork.c

      keme_cache_t  *task_struct_cachep;    //首先,用一个全局变量存放指向task_struct 缓冲区的指针,内核初始化期间,在fork_init()中会创建缓冲区

      task_struct_cachep = kmem_cache_create("task_struct", sizeof(struct task_struct), 0, SLAB_HWCACHE_ALIGN,NULL,NULL);  //这样就创建了一个名为task_strutc_cachep的缓冲区,其中存放的就是类型为 struct task_struct 的对象。

      if(!task_struct-cachep)     //这种情况下,如果内核不能创建task_struct_cachep缓冲区,它就会陷入混乱,因为这时系统操作一定要用到该缓冲区(没有进程控制块,操作系统自然不能正常运行)。

              printk("fork_init():cannot create task_struct SLAB cache");

      struct task_struct *tsk;

      tsk = kmem-cache_alloc(task_struct_cachep, GFP_KERNEL);

      if(!tsk) {

     /*不能分配进程控制块,清除,并返回错误代码*/

       ...

       return NULL;

      }

      keme_cache_free(task_struct _cachep, tsk);  //进程执行完,如果没有子进程在等待,它的进程控制块就会被释放,并返会给task_struct_cachep slab缓冲区。

      int err;

      err  =  kmem_cache_destroy(task_struct_cachep);  //销毁该缓冲区

       if(err)

        /* 出错,撤销缓冲区*/

       如果要频繁创建很多相同类型的对象,那么就应该考虑使用Slab缓冲区。确切地说不要自己去实现空闲链表。


通用缓冲区在下篇讲到。