Linux内核内存池管理技术实现分析

来源:互联网 发布:php多文件上传代码 编辑:程序博客网 时间:2024/06/06 17:54

一.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;        
    int curr_nr;        
    void **elements;  

    void *pool_data;  
    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_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));
   
    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;

   
    while (pool->curr_nr min_nr) {
        void *element;

        element = pool->alloc(GFP_KERNEL, pool->pool_data);
        if (unlikely(!element)) {
              free_pool(pool);
              return NULL;
        }
       
        add_element(pool, element);
    }
   
    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:
   
    element = pool->alloc(gfp_nowait|__GFP_NOWARN, pool->pool_data);
    if (likely(element != NULL))
        return element;

   
    mb();
    if ((gfp_mask & __GFP_FS) && (gfp_mask != gfp_nowait) &&
                  (pool->curr_nr min_nr/2)) {
        element = pool->alloc(gfp_mask, pool->pool_data);
        if (likely(element != NULL))
              return element;
    }

    spin_lock_irqsave(&pool->lock, flags);
   
    if (likely(pool->curr_nr)) {
        element = remove_element(pool);
        spin_unlock_irqrestore(&pool->lock, flags);
        return element;
    }
    spin_unlock_irqrestore(&pool->lock, flags);

   
    if (!(gfp_mask & __GFP_WAIT))
        return NULL;
   
    prepare_to_wait(&pool->wait, &wait, TASK_UNINTERRUPTIBLE);
    mb();
    if (!pool->curr_nr)
        io_schedule();
    finish_wait(&pool->wait, &wait);

    goto repeat_alloc;
}

如果申请者调用mempool_free准备释放内存,实际上是将内存对象重新放到内存池中。源码实现如下:
void mempool_free(void *element, mempool_t *pool)
{
    unsigned long flags;

    mb();
   
    if (pool->curr_nr min_nr) {
        spin_lock_irqsave(&pool->lock, flags);
        if (pool->curr_nr min_nr) {
             
              add_element(pool, element);
              spin_unlock_irqrestore(&pool->lock, flags);
              wake_up(&pool->wait);
              return;
        }
        spin_unlock_irqrestore(&pool->lock, flags);
    }
    pool->free(element, pool->pool_data);
}
这个函数十分简单,没有什么过多的分析了。

五.内存池实现总结
通过上面的分析,我们发现Linux内核的内存池实现相当简单。而C++STL中,实现了二级分配机制,初始化时将内存池按照内存的大小分成数个级别(每个级别均是8字节的整数倍,一般是8,16,24,…,128字节),每个级别都预先分配了20块内存。二级分配机制的基本思想是:如果用户申请的内存大于我们预定义的级别,则直接调用malloc从堆中分配内存,而如果申请的内存大小在128字节以内,则从最相近的内存大小中申请,例如申请的内存是10字节,则可以从16字节的组中取出一块交给申请者,如果该组的内存储量(初始是20)小于一定的值,就会根据一个算法(成为refill算法),再次从堆中申请一部分内存加入内存池,保证池中有一定量的内存可用。
而Linux的内存池实际上是与特定内存对象相关联的,每一种内存对象(例如task_struct)都有其特定的大小以及初始化方法,这个与STL的分级有点相似,但是内核主要还是根据实际的对象的大小来确定池中对象的大小。
内核内存池初始时从缓存区申请一定量的内存块,需要使用时从池中顺序查找空闲内存块并返回给申请者。回收时也是直接将内存插入池中,如果池已经满,则直接释放。内存池没有动态增加大小的能力,如果内存池中的内存消耗殆尽,则只能直接从缓存区申请内存,内存池的容量不会随着使用量的增加而增加。
0 0
原创粉丝点击