一.Linux系统内核内存管理简介

Linux采用“按需调页”算法,支持三层页式存储管理策略。将每个用户进程4GB长度的虚拟内存划分成固定大小的页面。其中0至3GB是用户态空间,由各进程独占;3GB到4GB是内核态空间,由所有进程共享,但只有内核态进程才能访问。

Linux将物理内存也划分成固定大小的页面,由数据结构page管理,有多少页面就有多少page结构,它们又作为元素组成一个数组mem_map[]。

slab:在操作系统的运作过程中,经常会涉及到大量对象的重复生成、使用和释放问题。对象生成算法的改进,可以在很大程度上提高整个系统的性能。在Linux系统中所用到的对象,比较典型的例子是inode、task_struct等,都又这些特点。一般说来,这类对象的种类相对稳定,每种对象的数量却是巨大的,并且在初始化与析构时要做大量的工作,所占用的时间远远超过内存分配的时间。但是这些对象往往具有这样一个性质,即他们在生成时,所包括的成员属性值一般都赋成确定的数值,并且在使用完毕,释放结构前,这些属性又恢复为未使用前的状态。因此,如果我们能够用合适的方法使得在对象前后两次背使用时,在同一块内存,或同一类内存空间,且保留了基本的数据结构,就可以大大提高效率。slab算法就是针对上述特点设计的。

slab算法思路中最基本的一点被称为object-caching,即对象缓存。其核心做法就是保留对象初始化状态的不变部分,这样对象就用不着在每次使用时重新初始化(构造)及破坏(析构)。

面向对象的slab分配中有如下几个术语:

l         缓冲区(cache):一种对象的所有实例都存在同一个缓存区中。不同的对象,即使大小相同,也放在不同的缓存区内。每个缓存区有若干个slab,按照满,半满,空的顺序排列。在slab分配的思想中,整个内核态内存块可以看作是按照这种缓存区来组织的,对每一种对象使用一种缓存区,缓存区的管理者记录了这个缓存区中对象的大小,性质,每个slab块中对象的个数以及每个slab块大小。

l         slab块:slab块是内核内存分配与页面级分配的接口。每个slab块的大小都是页面大小的整数倍,有若干个对象组成。slab块共分为三类:

完全块:没有空闲对象。

部分块:只分配了部分对象空间,仍有空闲对象。

空闲块:没有分配对象,即整个块内对象空间均可分配。

在申请新的对象空间时,如果缓冲区中存在部分块,那么首先查看部分块寻找空闲对象空间,若未成功再查看空闲块,如果还未成功则为这个对象分配一块新的slab块。

l         对象:将被申请的空间视为对象,使用构造函数初始化对象,然后由用户使用对象。

 

二.内存池的数据结构

Linux内存池是在2.6版内核中加入的,主要的数据结构定义在mm/mempool.c中。

typedef struct mempool_s {

       spinlock_t lock;

       int min_nr;             /* elements数组中的成员数量 */

       int curr_nr;            /* 当前elements数组中空闲的成员数量 */

       void **elements;    /* 用来存放内存成员的二维数组,其长度为min_nr,宽度是上述各个内存对象的长度,因为对于不同的对象类型,我们会创建相应的内存池对象,所以每个内存池对象实例的element宽度都是跟其内存对象相关的 */

 

       void *pool_data;     /* 内存池与内核缓冲区结合使用(上面的简介当中提到了,Linux采用slab技术预先为每一种内存对象分配了缓存区,每当我们申请某个类型的内存对象时,实际是从这种缓存区获取内存),这个指针一般是指向这种内存对象对应的缓存区的指针 */

       mempool_alloc_t *alloc; /* 用户在创建一个内存池对象时提供的内存分配函数,这个函数可以用户自行编写(因为对于某个内存对象如何获取内存,其开发者完全可以自行控制),也可以采用内存池提供的分配函数 */

       mempool_free_t *free;   /* 内存释放函数,其它同上 */

       wait_queue_head_t wait;/* 任务等待队列 */

} mempool_t;

 

三.内核缓存区和内存池的初始化

上面提到,内存池的使用是与特定类型的内存对象缓存区相关联的。例如,在系统rpc服务中,系统初始化时,会为rpc_buffers预先分配缓存区,调用如下语句:

rpc_buffer_slabp = kmem_cache_create("rpc_buffers",

                                        RPC_BUFFER_MAXSIZE,

                                        0, SLAB_HWCACHE_ALIGN,

                                        NULL, NULL);

调用kmem_cache_create函数从系统缓存区cache_cache中获取长度为RPC_BUFFER_MAXSIZE的缓存区大小的内存,作为rpc_buffer使用的缓存区。而以后对rpc操作的所有数据结构内存都是从这块缓存区申请,这是linux的slab技术的要点,而内存池也是基于这段缓存区进行的操作。

一旦rpc服务申请到了一个缓存区rpc_buffer_slabp以后,就可以创建一个内存池来管理这个缓存区了:

rpc_buffer_mempool = mempool_create(RPC_BUFFER_POOLSIZE,

                                       mempool_alloc_slab,

                                       mempool_free_slab,

                                       rpc_buffer_slabp);

mempool_create函数就是内存池创建函数,负责为一类内存对象构造一个内存池,传递的参数包括,内存池大小,定制的内存分配函数,定制的内存析构函数,这个对象的缓存区指针。下面是mempool_create函数的具体实现:

/**

 * mempool_create – 创建一个内存池对象

 * @min_nr:       为内存池分配的最小内存成员数量

 * @alloc_fn:       用户自定义内存分配函数

 * @free_fn:       用户自定义内存释放函数

 * @pool_data:     根据用户自定义内存分配函数所提供的可选私有数据,一般是缓存区指针

*/

mempool_t * mempool_create(int min_nr, mempool_alloc_t *alloc_fn,

                            mempool_free_t *free_fn, void *pool_data)

{

       mempool_t *pool;

       /*为内存池对象分配内存*/

       pool = kmalloc(sizeof(*pool), GFP_KERNEL);

       if (!pool)

              return NULL;

       memset(pool, 0, sizeof(*pool));

       /*根据内存池的最小长度为elements数组分配内存*/

       pool->elements = kmalloc(min_nr * sizeof(void *), GFP_KERNEL);

       if (!pool->elements) {

              kfree(pool);

              return NULL;

       }

       spin_lock_init(&pool->lock);

       /*初始化内存池的相关参数*/

       pool->min_nr = min_nr;

       pool->pool_data = pool_data;

       init_waitqueue_head(&pool->wait);

       pool->alloc = alloc_fn;

       pool->free = free_fn;

 

       /*首先为内存池预先分配min_nr个element对象,这些对象就是为了存储相应类型的内存对象的。数据结构形入:

 内核内存池管理技术实现分析【转】_内存分配

*/

       while (pool->curr_nr < pool->min_nr) {

              void *element;

 

              element = pool->alloc(GFP_KERNEL, pool->pool_data);

              if (unlikely(!element)) {

                     free_pool(pool);

                     return NULL;

              }

              /*将刚刚申请到的内存挂到elements数组的相应位置上,并修改curr_nr的值*/

              add_element(pool, element);

       }

       /*若成功创建内存池,则返回内存池对象的指针,这样就可以利用mempool_alloc和mempool_free访问内存池了。*/

       return pool;

}

 

四.内存池的使用

如果需要使用已经创建的内存池,则需要调用mempool_alloc从内存池中申请内存以及调用mempool_free将用完的内存还给内存池。

void * mempool_alloc(mempool_t *pool, int gfp_mask)

{

       void *element;

       unsigned long flags;

       DEFINE_WAIT(wait);

       int gfp_nowait = gfp_mask & ~(__GFP_WAIT | __GFP_IO);

 

repeat_alloc:

       /*这里存在一些不明白的地方,先将用户传递进来的gfp掩码标志去掉__GFP_WAIT 和 __GFP_IO 两个标志,试图调用用户自定义分配函数从缓存区申请一个内存对象,而不是首先从内存池从分配,如果申请不到,再从内存池中分配。*/

       element = pool->alloc(gfp_nowait|__GFP_NOWARN, pool->pool_data);

       if (likely(element != NULL))

              return element;

 

       /*如果池中的成员(空闲)的数量低于满时的一半时,需要额外从系统中申请内存,而不是从内存池中申请了。但是如果这段内存使用完了,则调用mempool_free将其存放到内存池中,下次使用就不再申请了。*/

       mb();