使用Java进行开发的时候,不用过多地考虑内存的问题。而C/C++就不得不考虑了,比如每一次的内存申请(malloc),都不能忘了释放(free),不然会出现内存泄漏等等诸多问题。在网络服务程序里,每来一个请求或者链接,都会开辟一小块内存来对其进行相应的处理,如果每次都向操作系统申请内存的话,会使得网络服务程序的性能大打折扣。在这种情况里,是非常有必要使用内存池的。近期在做一个消息转发机制的轮子,其中需要使用内存池,所以参照了Apache的内存池进行简单的设计(修剪<( ̄︶ ̄)>)和在Linux下的实现。
内存池
内存池 (Memory Pool),又被称为固定大小区块规划 (fixed-size-blocks allocation),允许程序员以类似 C语言 的 malloc 或是 C++ 的 new 运算对象进行动态的内存规划。对于其它动态内存规划的实践来说,因为会变动内存区块大小导致的碎片问题,导致在实时系统上受限于性能因此,根本无法使用。内存池提供了一个更有效率的解决方案:预先规划一定数量的内存区块,使得整个程序可以在运行期规划 (allocate)、使用 (access)、归还 (free) 内存区块 —— [ 维基百科 ]
设计
主要参照Apache内存池进行设计,说到底就是Apache内存池的简化版。保留Apache的内存池节点和分配器,并且对其进行修剪。只设计内存池的核心操作,如内存池创建、内存池销毁、向内存池申请内存和归还内存。并且还为其设计(仿照Apache_(:зゝ∠)_)了内存池空间大小动态增长的特点,即内存池并不会一开始向操作系统申请一大块内存,初始状态时,内存池的大小为一个比较小的值,其随着用户内存的申请而增长。
内存池节点
内存池节点是内存池分配内存块的最小单元,其有5个区域,分别是:链表后继指针域、链表前驱指针域、index域、使用标志和用户使用内存块,如下图所示。其中,用户使用内存块的前四个区域为内存池节点头。
链表后继指针域(next)和 链表前驱指针域(pre) 使内存池节点可插入到使用链表或index对应的空闲链表。
index域 即可作为分配器空闲数组free[]的下标来索引空闲链表,也可描述内存池节点的大小。
使用标志(use_flg) 当为0时表明内存池节点空闲,为1时表明正在被用户使用。
用户使用内存块 是用户真正使用的内存。
啊,,,小伙伴们看到这里,是不是对index处于懵逼状态╮(╯▽╰)╭?本来想在这里就对其进行详细介绍,但在后面的内存分配器的介绍中会涉及到,所以就放在后面一块介绍吧,在此先作一个印象。
内存分配器
Apache中的大部分的内存的分配都是由内存分配器allocator完成的。在这,Apache的内存分配器被我作了很大修剪,并且用内存分配器来代表一个内存池,所有对内存池的操作都需要通过其来进行。内存分配器的数据结构图及其与内存池节点的关系如下图所示:
是的,本次设计的内存池所有数据结构都被上图给画完了,就是这么简单<( ̄ˇ ̄)/!通过上图,小伙伴们是否对Index窥见一斑?index有两个身份:第一,它可以描述内存池节点的大小size = (index + 1)*ALIGN,其中ALGIN为用户可以设置的对齐大小;第二,它可以作为内存分配器中free数组的索引,当用户需要向内存池申请内存时,计算得出合适的索引,并且通过该索引可以找到对应大小内存池节点的空闲链表,进而取出合适大小的内存池节点。
下面对内存分配器的各区域进行介绍:
max_free_index 描述内存池的最大空闲内存大小。因为本次设计的内存池的内存空间是动态增长的,随着用户内存的申请,内存池节点会增多,相对的,空闲内存池节点也会增多,如果没有对空闲内存池节点进行限制,内存池就会占用很大的空闲内存,造成系统内存资源的很大浪费。
current_free_index 描述目前内存池空闲内存大小(不包括使用中的内存大小)。
use_head 正在使用的内存池节点链表头。该链表是双向链表,之所以设计成双向链表,是因为只需提供要被移出链表的节点指针即可对节点进行移除。也即是当用户需要释放内存时,计算出用户使用内存块对应的内存池节点头的指针即可把内存池节点移出使用链表,进而插入到相对应的内存池节点空闲链表。
*free[MAX_INDEX + 1] 空闲链表数组。这是一个非常重要的数组,其每个元素保存着相应大小的空闲内存池节点链表首节点指针。除了free[MAX_INDEX]元素,每个元素free[index]保存着大小为size = (index + 1)*ALIGN的内存池节点链表的首节点指针。而free[MAX_INDEX]元素保存着大小为size > (MAX_INDEX + 1)*ALIGN的内存池节点链表首节点指针。用户每一次的内存申请,都会访问一次free[]数组,找到对应内存池节点链表,进而取出合适的内存池节点。用户的每一次释放,如果释放之后内存池空闲空间大小不超过限制,也会把要释放的内存池节点插入到free[]数组里某个对应大小的空闲内存池节点链表中。
mutex 锁,用来互斥访问内存分配器。因为内存池有大量的链表的插入和移除操作,在多线程中,如果不进行互斥访问,内存池将会变得非常不安全。
内存申请
当用户向内存池申请内存时,内存池的主要工作就是:根据用户需要申请内存的大小选择一个合适的空闲内存池节点,把该节点的用户使用内存块的首地址返回给用户。选择空闲内存池节点时,会遇到两种情况:情况一,对应的Index在区间[0,MAX_INDEX-1]内;情况二,用户所申请内存大小对应的Index大于等于MAX_INDEX。以下介绍在各个情况里,内存池所做的工作。
情况一
在这种情况下,内存池可以非常快地找到合适的内存池节点。内存池直接把Index作为free数组的下标,索引到Index对应的内存池节点链表的首节点指针,然后从链表取出首节点,把其插入到使用链表(链表头为use_head)里,最后把该节点的用户使用内存块的首地址返回给用户。如果free[]数组中Index对应的元素是空的,即内存池没有存在Index对应的空闲内存池节点,则向操作系统申请内存,生成空闲内存池节点,把其插入到使用链表后,返回用户使用内存块的首地址给用户(内存池空间大小动态增长)。显然,在这种情况里,如果内存池有对应大小的空闲内存池节点,用户向内存池申请内存的时间复杂度为O(1)。流程图如下:
情况二
Index大于等于MAX_INDEX的内存池节点是比较大的了,可以认为用户申请其的频率很小,故不会对其单独组织成一个链表。所以把所有Index大于等于MAX_INDEX的内存池节点从小到大连接成一个链表,挂在free[MAX_INDEX]下。用户向内存池申请的内存大小大于等于MAX_INDEX对应的大小时,将会遍历free[MAX_INDEX]对应的链表,如果找到合适的空闲内存池节点则取出,如果找不到,则向操作系统申请相应大小内存并生成空闲内存池节点,最后将节点插入到使用链表,把用户使用内存块的首地址返回给用户。显然时间复杂度与free[MAX_INDEX]对应空闲内存池节点链表长度有关,为O(n)。流程图如下:
内存释放
内存释放就是用户把向内存池申请到的内存归还给内存池,把申请到的内存块首地址交给内存池即可释放。实际上,在内存释放的过程中,内存池所做的工作仅仅是通过用户内存块首地址减去一个偏移量(内存池节点头大小)得出内存池节点首地址,然后把该节点移出使用链表。如果释放后内存池总空闲空间大小没有超过最大值(max_free_index),则把移出的内存池节点插入到free[]数组中对应Index的空闲链表里,否则把节点所占的内存空间还给操作系统。逻辑很简单,作者很懒,所以就不画流程图了。当归还的内存对应的index在区间[0,MAX_INDEX-1]上,并且归还后内存池总空闲空间没有超过最大值,内存释放的时间复杂度是O(1),可见非常快。
小结
在此,已经把简单内存池的数据结构和核心操作的设计介绍得差不多了,还差内存池的创建和销毁,还有多线程安全,因为篇幅问题,加上作者很懒,所以就不作多介绍了。这个内存池的数据结构是参照Apache设计的,其实还有各种各样结构的内存池,不仅限于如此。该内存池的优点和性能,在上面稍微作了一些分析,在一定情况下申请和释放特别快。不足之处也有许多,下面列举几个:
- 如果用户申请的内存大小变化特别大,空闲链表命中率(free[]数组存在对应Index的空闲链表的概率)就会特别低,导致几乎每次向内存池申请内存时,实际上是向操作系统申请内存,内存池作用不大。
- 不适合申请超大空间内存。
- 如果用户在某个瞬间申请大量Index=1的内存空间,而后在某个瞬间全部归还给内存池之后一直不使用,则这些空闲内存池节点就成了“僵尸节点”,很显然浪费了系统资源。
- 等等。。。
对于第2点,可以把free[MAX_INDEX]对应的链表改成小根堆而得以改进。使用小根堆之后,内存申请和释放的时间复杂度都是O(logn)。对于第3点,可以给内存池节点加上时间戳,再设计一个定时器,定时清除“僵尸节点。”
实现
来不及解释了,快上车!有什么话车上讲(注释)!
文件mmpool.h
/*嗯,这是参照Apache设计的一个内存池,或者说是Apache内存池的一个简易版,如果写完
* 了之后觉得挺好用的,那我就继续添加树型结构。也挺担心到时就挺迟的了。
*/
#include "semaphore.h" //没有移植的打算
#define MMPL_MAX_INDEX 10
/*下面是内存池的默认配置,内存池最大内存空间是200M*/
#define MMPL_MAX_FREE_INDEX_DEFAULT (1<<10)*100
#define MMPL_BOUNDARY_DEFAULT 2048
#define MMPL_BOUNDARY_INDEX 12
/*匹配最接近size的boundary的整数倍的整数,boundary必须为2次幂*/
#define MMPL_ALIGN(size,boundary) \
(((size) + ((boundary) - 1)) & ~((boundary) - 1))
#define MMPL_ALIGN_DEFAULT(size) MMPL_ALIGN((size),MMPL_BOUNDARY_DEFAULT)
/*如同Apache的内存节点*/
struct mm_node{
int use_flg; //使用标志,0表示没有在使用,1表示正在被使用
struct mm_node *next; //下一个节点
struct mm_node *pre; //上一个节点
unsigned int index; //既可以表示节点内存的大小,也可以作为free数组的下标
};
struct mm_pool_s{
/*内存池最大内存空间大小,防止向操作系统申请过多内存空间致使操作系统崩溃*/
unsigned int max_free_index; //最大空闲内存
unsigned int current_free_index; //当前内存池空闲的内存
struct mm_node use_head; //正在使用的内存节点的链表头
struct mm_node *free[MMPL_MAX_INDEX + 1];
sem_t mutex; //锁,用来互斥访问内存池
};
int mmpl_create(struct mm_pool_s **new_mmpl);
int mmpl_destroy(struct mm_pool_s *p_mmpl);
void* mmpl_getmem(struct mm_pool_s *p_mmpl,unsigned int size);
int mmpl_rlsmem(struct mm_pool_s *p_mmpl,void *rls_mmaddr);
int mmpl_list_remove(struct mm_node *p_rm_node);
文件mmpool.c
#include "mmpool.h"
#include "stdlib.h"
#include "string.h"
#include "stdio.h"
#include "unistd.h"
/* 函数名: int mmpl_create(struct mm_pool_s *new_mmpl)
* 功能: 创建新的内存池
* 参数: struct mm_pool_s *new_mmpl,内存池结构体指针
* 返回值: -1,
* 1,
*/
int mmpl_create(struct mm_pool_s **new_mmpl){
*new_mmpl = (struct mm_pool_s *)malloc(sizeof(struct mm_pool_s));
memset((void*)*new_mmpl,0,sizeof(struct mm_pool_s));
(*new_mmpl)->max_free_index = MMPL_MAX_FREE_INDEX_DEFAULT;
(*new_mmpl)->current_free_index = 0;
(*new_mmpl)->use_head.next = &((*new_mmpl)->use_head);
(*new_mmpl)->use_head.pre = &((*new_mmpl)->use_head);
sem_init(&(*new_mmpl)->mutex,0,1); //初始化锁
return 1;
}
/* 函数名: int mmpl_destroy(struct mm_pool_s *p_mmpl)
* 功能: 销毁内存池,把内存空间归还给操作系统
* 参数: struct mm_pool_s *p_mmpl,需要销毁的内存池的结构体
* 返回值: -1,
* 1,
*/
int mmpl_destroy(struct mm_pool_s *p_mmpl){
int i;
struct mm_node *p_mm_n,*p_free_h;
if(p_mmpl == NULL){
return -1;
}
sem_wait(&p_mmpl->mutex); //互斥访问内存池
/*归还正在使用的内存空间给操作系统*/
while(p_mmpl->use_head.next != &p_mmpl->use_head){
p_mm_n = p_mmpl->use_head.next;
mmpl_list_remove(p_mm_n);
free(p_mm_n);
}
/*归还内存池中空闲内存空间给操作系统*/
for(i = 1;i < MMPL_MAX_INDEX + 1;i++){
p_free_h = p_mmpl->free[i];
if(p_free_h == NULL)continue;
while(p_free_h->next != p_free_h){
p_mm_n = p_free_h->next;
mmpl_list_remove(p_mm_n);
free(p_mm_n);
}
free(p_free_h);
}
sem_post(&p_mmpl->mutex);
sem_destroy(&p_mmpl->mutex);
free(p_mmpl);
return 1;
}
/* 函数名: int mmpl_list_insert(struct mm_node *pre_p_n,struct mm_node *p_insert_n)
* 功能: 把内存节点插入到链表
* 参数: struct mm_node *pre_p_mm_n,需要插入位置的前驱节点指针
* struct mm_node *p_insert_mm_n,需要插入的节点的指针
* 返回值: -1,
* 1,
*/
int mmpl_list_insert(struct mm_node *p_pre_n,struct mm_node *p_insert_n){
if(p_pre_n == NULL || p_insert_n == NULL){
return -1;
}
p_insert_n->next = p_pre_n->next;
p_insert_n->pre = p_pre_n;
p_pre_n->next->pre = p_insert_n;
p_pre_n->next = p_insert_n;
return 1;
}
/* 函数名: int mmpl_list_remove(struct mm_node *p_rm_node)
* 功能: 从节点所在的链表中移除该节点
* 参数: struct mm_node *p_rm_node,需要移除的节点
* 返回值: -1,
* 1,
*/
int mmpl_list_remove(struct mm_node *p_rm_node){
if(p_rm_node->next == p_rm_node){ //如果链表就只有该节点,则无法删除
return -1;
}
p_rm_node->next->pre = p_rm_node->pre;
p_rm_node->pre->next = p_rm_node->next;
return 1;
}
/* 函数名: void* mmpl_getmem(struct mm_pool_s *p_mmpl,unsigned int size)
* 功能: 从指定的内存池里获取到内存空间
* 参数: struct mm_pool_s *p_mmpl,内存池结构体指针
* int size,需要申请空间的大小
* 返回值: NULL,获取失败
* !=NULL,获取到的内存地址
*/
void* mmpl_getmem(struct mm_pool_s *p_mmpl,unsigned int size){
unsigned int align_size;
unsigned int index;
struct mm_node *p_mm_n;
align_size = MMPL_ALIGN_DEFAULT(size); //默认2K对齐
index = align_size/MMPL_BOUNDARY_DEFAULT;
if(index > MMPL_MAX_INDEX){ //如果超过MMMPL_MAX_INDEX则向操作系统要
index = 0;
}
sem_wait(&p_mmpl->mutex); //互斥操作内存池
if(((p_mm_n = p_mmpl->free[index]) == NULL) || index == 0){
/*如果free数组中没有相应的内存节点,则向操作系统申请*/
p_mm_n = (struct mm_node *)malloc(align_size + sizeof(struct mm_node));
if(p_mm_n == NULL){//向操作系统申请内存失败
sem_post(&p_mmpl->mutex);
return NULL;
}
p_mm_n->pre=p_mm_n->next = p_mm_n;
p_mm_n->index = index;
}else if(p_mm_n->next == p_mm_n){
/*如果free[index](规则链表)链表只有一个节点*/
p_mmpl->free[index] = NULL;
p_mmpl->current_free_index -= index; //空闲内存空间减少
}else{
p_mmpl->free[index] = p_mm_n->next;
mmpl_list_remove(p_mm_n);
p_mmpl->current_free_index -= index; //空闲内存空间减少
}
p_mm_n->use_flg = 1; //表示正在被使用
mmpl_list_insert(&p_mmpl->use_head,p_mm_n); //插入到使用链表中
sem_post(&p_mmpl->mutex);
return (void *)p_mm_n + sizeof(struct mm_node);
}
/* 函数名: int mmpl_rlsmem(struct mm_pool_s *p_mmpl,void *rls_mmaddr)
* 功能: 把从内存池申请到的内存空间还给内存池,如果还给内存池之后空闲的内存空间超
* 过了内存池所设置的最大空闲内存,则把将要归还给内存池的内存空间直接还给操
* 作系统。
* 参数: strcut mm_pool_s *p_mmpl,内存池
* void *rls_mmaddr,需要释放的内存空间的首地址
* 返回值:
*/
int mmpl_rlsmem(struct mm_pool_s *p_mmpl,void *rls_mmaddr){
struct mm_node *p_mm_n;
unsigned int index;
p_mm_n = rls_mmaddr - sizeof(struct mm_node);//获得内存节点的首地址
if(p_mm_n->use_flg == 0){ //如果本来就是空闲的,则放弃回收
return -1;
}
index = p_mm_n->index;
sem_wait(&p_mmpl->mutex); //互斥操作内存池
mmpl_list_remove(p_mm_n); //从使用链表中移除
p_mm_n->use_flg = 0;
if(index == 0){ //如果超过了最大index,则直接还给操作系统
free(p_mm_n);
sem_post(&p_mmpl->mutex);
return 1;
}
if(p_mmpl->current_free_index + index > p_mmpl->max_free_index){
/*如果归还之后内存池的空闲空间超过了最大空闲空间大小则直接归还给操作系统*/
free(p_mm_n);
sem_post(&p_mmpl->mutex); //互斥操作内存池
return 1;
}
if(p_mmpl->free[index] == NULL){
p_mm_n->pre=p_mm_n->next = p_mm_n;
p_mmpl->free[index] = p_mm_n;
}else{
if(mmpl_list_insert(p_mmpl->free[index],p_mm_n) == -1){
printf("error\n");
return -1;
}
}
p_mmpl->current_free_index += index;
sem_post(&p_mmpl->mutex); //互斥操作内存池
return 1;
}