一. 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分配的思想中,整个内核状态内存块可以看作是按照这样的缓存区域进行组织的,每个对象使用一种缓存区域,缓存区域的管理者根据该缓存区域内的对象的大小、性质、各slse
l slab块: slab块是内核内存分配和页面级分配的接口。 每个slab块的大小是页面大小的整数倍,由几个对象组成。 slab块共分为三类:
完整块:没有可用对象。
部分块:只分配了一部分对象空间,剩下空闲对象。
lymt :没有分配对象。 这意味着将分配块中的整个对象区域。
申请新的对象空间时,如果缓冲区有部分块,首先看部分块寻找空闲的对象空间,如果不好就看lymt,如果不好就给该对象分配新的slab块。
l对象:将申请的空间视为对象,使用构造函数初始化对象,用户使用对象。
2 .内存池数据结构
Linux内存池内置于2.6版内核中,主要数据结构在mm/mempool.c中定义。
typedef struct mempool_s {
spinlock_t lock;
int min_nr; /* elements数组的成员数*
int curr_nr; /*当前elements数组中的空闲成员数*
void * *元素; 用于以min_nr长度保存内存成员的二维数组。 宽度是上述每个内存对象的长度。 每个内存池对象实例的element宽度都与其内存对象相关,因为您将为每种对象类型创建相应的内存池对象。 */
void *pool_data; /*组合使用内存池和内核缓冲区(如上概述所述,Linux使用slab技术预先为每个内存对象分配缓存空间,每次请求某种类型的内存对象时,实际上
mempool_alloc_t *alloc; /*用户创建内存池对象时提供的内存分配函数。 此函数可以由用户自己创建,也可以使用内存池提供的分配函数。 */
mempool_free_t *free; /*内存释放函数,其他同上*
wait_queue_head_t wait; /*任务等待队列*
} mempool_t;
3 .初始化内核缓冲区和内存池
如上所述,内存池的使用与特定类型的内存对象缓存相关联。 例如,在系统rpc服务中,初始化系统时,会为rpc_buffers预分配缓存空间并调用以下语句:
RPC _ buffer _ slabp=kmem _ cache _ create (RPC _ buffers ),
RPC_BUFFER_MAXSIZE,
0,SLAB_HWCACHE_ALIGN,
NULL,空值;
调用kmem_cache_create函数以rpc_buf形式从系统缓冲区cache_cache中获取RPC_BUFFER_MAXSIZE的缓冲区大小的内存
fer使用的缓存区。而以后对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();
if ((gfp_mask & __GFP_FS) && (gfp_mask != gfp_nowait) &&
(pool->curr_nr <= pool->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);
/* We must not sleep in the GFP_ATOMIC case */
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 < 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);
}
这个函数十分简单,没有什么过多的分析了。
五.内存池实现总结
通过上面的分析,我们发现Linux内核的内存池实现相当简单。而C++STL中,实现了二级分配机制,初始化时将内存池按照内存的大小分成数个级别(每个级别均是8字节的整数倍,一般是8,16,24,…,128字节),每个级别都预先分配了20块内存。二级分配机制的基本思想是:如果用户申请的内存大于我们预定义的级别,则直接调用malloc从堆中分配内存,而如果申请的内存大小在128字节以内,则从最相近的内存大小中申请,例如申请的内存是10字节,则可以从16字节的组中取出一块交给申请者,如果该组的内存储量(初始是20)小于一定的值,就会根据一个算法(成为refill算法),再次从堆中申请一部分内存加入内存池,保证池中有一定量的内存可用。
而Linux的内存池实际上是与特定内存对象相关联的,每一种内存对象(例如task_struct)都有其特定的大小以及初始化方法,这个与STL的分级有点相似,但是内核主要还是根据实际的对象的大小来确定池中对象的大小。
内核内存池初始时从缓存区申请一定量的内存块,需要使用时从池中顺序查找空闲内存块并返回给申请者。回收时也是直接将内存插入池中,如果池已经满,则直接释放。内存池没有动态增加大小的能力,如果内存池中的内存消耗殆尽,则只能直接从缓存区申请内存,内存池的容量不会随着使用量的增加而增加。