Linux 内存池【金沙官网线上】【转】

内存池(Memery Pool)技术是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是尽量避免了内存碎片,使得内存分配效率得到提升。
不仅在用户态应用程序中被广泛使用,同时在Linux内核也被广泛使用,在内核中有不少地方内存分配不允许失败。作为一个在这些情况下确保分配的方式,内核开发者创建了一个已知为内存池(或者是 "mempool" )的抽象,内核中内存池真实地只是相当于后备缓存,它尽力一直保持一个空闲内存列表给紧急时使用,而在通常情况下有内存需求时还是从公共的内存中直接分配,这样的做法虽然有点霸占内存的嫌疑,但是可以从根本上保证关键应用在内存紧张时申请内存仍然能够成功。
下面看下内核内存池的源码,内核内存池的源码在中,实现上非常简洁,描述内存池的结构;
mempool_t在头文件中定义,结构描述如下:

转自:http://blog.chinaunix.net/uid-23634108-id-2393485.html

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;

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

内存池的创建函数mempool_create的函数原型如下:

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

mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn,
                mempool_free_t *free_fn, void *pool_data)
{
    return mempool_create_node(min_nr,alloc_fn,free_fn, pool_data,-1);
}

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

函数原型指定内存池可以容纳元素的个数、申请元素的方法、释放元素的方法,以及一个可选的内存源(通常是一个cache),内存池对象创建完成后会自动调用alloc方法从pool_data上分配min_nr个元素用来填充内存池。
内存池的释放函数mempool_destory函数的原型很简单,应该也能猜到是依次将元素对象从池中移除,再释放给pool_data,最后释放池对象,如下:

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

void mempool_destroy(mempool_t *pool)
{
    while (pool->curr_nr) {
        void *element = remove_element(pool);
        pool->free(element, pool->pool_data);
    }
    kfree(pool->elements);
    kfree(pool);
}

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

值得注意的是内存池分配和回收对象的函数:mempool_allocmempool_freemempool_alloc的作用是从指定的内存池中申请/获取一个对象,函数原型如下:

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

void * mempool_alloc(mempool_t *pool, gfp_t gfp_mask){
......
    element = pool->alloc(gfp_temp, 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);
        /* paired with rmb in mempool_free(), read comment there */
        smp_wmb();
        return element;
    }
......
}

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

函数先是从pool_data中申请元素对象,当从pool_data无法成功申请到时,才会从池中提取对象使用,因此可以发现内核内存池mempool其实是一种后备池,在内存紧张的情况下才会真正从池中获取,这样也就能保证在极端情况下申请对象的成功率,单也不一定总是会成功,因为内存池的大小毕竟是有限的,如果内存池中的对象也用完了,那么进程就只能进入睡眠,也就是被加入到pool->wait的等待队列,等待内存池中有可用的对象时被唤醒,重新尝试从池中申请元素:

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

init_wait(&wait);
prepare_to_wait(&pool->wait, &wait, TASK_UNINTERRUPTIBLE);
spin_unlock_irqrestore(&pool->lock, flags);
io_schedule_timeout(5*HZ);
finish_wait(&pool->wait, &wait);

完全块:没有空闲对象。

内存池回收对象的函数mempool_free的原型如下:

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

void mempool_free(void *element, mempool_t *pool)
{
    if (pool->curr_nr < pool->min_nr) {
        spin_lock_irqsave(&pool->lock, flags);
        if (pool->curr_nr < pool->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);
}

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

其实原则跟mempool_alloc是对应的,释放对象时先看池中的可用元素是否充足(pool->curr_nr

pool->min_nr),如果不是则将元素对象释放回池中,否则将元素对象还给pool->pool_data。
此外mempool也提供或者说指定了几对alloc/free函数,及在mempool_create创建池时必须指定的alloc和free函数,分别适用于不同大小或者类型的元素的内存池,具体如下:

void *mempool_alloc_slab(gfp_t gfp_mask, void *pool_data)
{
    struct kmem_cache *mem = pool_data;
    return kmem_cache_alloc(mem, gfp_mask);
}
void mempool_free_slab(void *element, void *pool_data)
{
    struct kmem_cache *mem = pool_data;
    kmem_cache_free(mem, element);
}

void *mempool_kmalloc(gfp_t gfp_mask, void *pool_data)
{
    size_t size = (size_t)pool_data;
    return kmalloc(size, gfp_mask);
}
void mempool_kfree(void *element, void *pool_data)
{
    kfree(element);
}

void *mempool_alloc_pages(gfp_t gfp_mask, void *pool_data)
{
    int order = (int)(long)pool_data;
    return alloc_pages(gfp_mask, order);
}
void mempool_free_pages(void *element, void *pool_data)
{
    int order = (int)(long)pool_data;
    __free_pages(element, order);
}

总体上来讲mempool的实现很简约,但是不简单,而且非常轻便易用,这也是内核奥妙之所在。

在申请新的对象空间时,如果缓冲区中存在部分块,那么首先查看部分块寻找空闲对象空间,若未成功再查看空闲块,如果还未成功则为这个对象分配一块新的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;

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

本文由金沙官网线上发布于操作系统,转载请注明出处:Linux 内存池【金沙官网线上】【转】

您可能还会对下面的文章感兴趣: