摘 要:对内存池中内存块获取、分配机制、内存块大小、内存释放,以及在多线程环境下的安全处理等细节进行了研究,保证了在多线程环境下能够快速同时采用一种基于数组的链表机制,改进内存池中内存块的查找算法,将其时间复杂度稳定在O(1),避免了传统内存池中请求的线程数目过多时,引发的获取内存块性能下降的问题。同时在内部设置管理线程,动态增加或删除空闲的内存块。实验结果表明,改进后的内存池与传统的内存分配方式相比消耗更小,效率更好。
关键词:内存池;内存块查找算法; Linux;多线程
动态内存管理非常耗时,对效率影响很大,然而在实际的编程应用中,却不可避免地经常要用到堆中的内存。但是通过Malloc函数或New等进行的内存分配存在先天缺陷: (1)利用默认的内存管理函数在堆上分配和释放内存需要花费很多时间;(2)随着时间的推移,堆上会形成许多内存碎片,在应用程序进行内存申请操作将受到更大的影响,导致应用程序的运行越来越慢[1-3]。
当应用程序需要对固定大小的对象经常性地申请内存时,常会采用内存池(Memory Pool)技术来提高内存管理效率。经典的内存池做法是一次性分配大量大小相同的小块内存,通过该技术可以极大地加快内存分配/释放过程。内存池技术通过批量申请内存,降低了内存申请次数,从而使操作节省了时间。在减少了内存碎片产生的同时,对性能的提升有显著的帮助。
综上,内存池有其巨大的优势,但是原有的内存池也存在一定的缺陷。在多线程场合下应用时,每个新产生的线程如何在O(1)时间内获取内存块,如何保证其安全有效性,以及如何管理内存块的数量方面存在一定的不足的,本文对此进行研究,并给出一种新的解决方案。
1 内存池制作原理以及工作流程
本内存池基于多线程环境,需要考虑到多线程下数据的安全,以及快速获取内存块等条件。在获取内存块索引号时,采用加锁的方式,虽然会耗费一定的时间,但是运行安全得到了保障。在程序运行之前需创建好内存池,并用一个结构体struct mem_pool进行封装,里面包含内存池的一些私有数据。当有新线程产生时,直接像内存池申请一块已经分配好了的内存,线程的具体内存操作都在该内存块中进行。同时,内存池结构体中隐藏一个管理线程,该线程的工作是定时检查内存池中空闲内存块的数量是否过多或者过少。当过多时,申请释放,直到达到门槛值;当过少时,申请增加若干,以备不时之需。内存池结构如图1所示。
对于内存池中的内存块,采用结构struct mem_block维护其数据,该结构以一个链表的形式维护实际内存区域,结构体中有两个管理内存的区域:(1)常规大小链表区域。当需要的内存小于常规大小时,则线程的内存请求都将从该区域获得;当该区域内存将满时,则线程可以继续申请同样大小的内存块,链接到该常规大小链表上。(2)大块内存链表区域。当线程申请的内存超过该内存块的大小时,将在系统中申请一块大的内存链接到该大块内存链表上,这样可以对内存块进行统一管理;当线程寿命结束时,调用reset函数将大块内存释放,同时重设常规内存链表区域,只保留最开始的一块,其余后面申请的块全部释放,并标记内存块的使用状态为空闲,同时重设一些内部指针,指向内存块可用的起始位置[4]。
创建内存池结构,并初始化,此时在内存中存储着一份内存池的动态管理单元。当创建新线程时,该线程通过内存块查找函数,并查询内存池结构。如有空闲内存块则直接将该内存块的索引号index送给线程,同时将该内存块的空闲标志flag设为繁忙;如果内存池中没有可用的空闲内存块,且内存块数量未达到设置的顶峰,则可以申请add_memblock;若正在使用的内存块超过了最大设置的内存块数量,则线程将调用Malloc函数,并自行调用Free释放该内存块。管理线程周期性地调用get_mp_status来查看内存池状态,若空闲线程低于门槛值(最小空闲内存块数),则调度add_memblock函数创建新的内存块到池中;若空闲内存块高于门槛值(最大空闲内存数),则调度del_memblock销毁多余的内存块。线程生命周期结束后,将内存块的繁忙标记设置为空闲状态,并且重新初始化该内存块,将内存块重新投入到内存池中,等待其他线程重复利用。内存池的工作流程如图2所示。
2 内存池主要技术
2.1 内存池中空闲内存块的查找方式
当进程服务繁忙时,一些内存块长期被某些线程占用的情况下,也可能延长内存池响应时间,影响响应速度。内存池调度算法的一项重要任务就是尽可能提高查找空闲内存块的速度。而简单地遍历内存池链表显然不能满足请求线程的需要,这种方式不仅延长了调用者的返回时间,而且极大增加了内存池对请求线程的响应时间。特别是在服务器繁忙时,当处于请求内存块的线程越多,查找空闲内存块所花费的时间就越长。因此,本文提出以下两种查找方法:
(1) 位图查找方式
用位图方式维护内存池中的内存块单元,查找空闲内存块将只需遍历位图,位图按单个字节进行查找效率较高。另外,在线程结束时的前一刻,维护当前空闲内存块的索引index,可在下次查找空闲内存块时直接获取这个值,而时间耗费是O(1)级的,这将大大提高响应时间。
(2) 基于数组的方式
基于链表实现的内存池,在创建内存池时或者每次增加池中内存块数时都必须分配新的管理结构,再进行链接;并且在查找空闲内存块时,需要遍历内存池,其直接后果是造成线程请求内存块的时间大大增加。而数组的方式有其天然的优势,当用位图查找到了空闲内存块的索引后,也即知道了内存块在数组中的位置,由此可以迅速地定位空闲内存块,大大提高了响应速度。
2.2 内存池中内存块的数量动态调整
固定的内存池在有些情况下并不能满足实际情况的需要,动态内存池常见的调整方法有基于阈值触发和基于预测公式两种形式。基于预测公式方法通过统计学的经验公式来预测,优点是能够反应内存池消耗内存的真实倾向,迅速查找和释放内存块;缺点是按照统计公式计算的结果,通常局限于某些特定场合和应用,并且在内存池中造成系统资源消耗较大。基于阈值触发方法通常按照参数配置来控制内存池的某些参数,优点是实现简单、通用性强、可控性好;缺点是需要精确计算配置参数,否则性能会急剧下降。为保证内存池的通用性,这里使用参数可调整的阈值触发方式动态调整内存池。
(1) 相关参数合理性设置
设内存池中最大内存块数为MAX_NUM,内存池中最小内存块数为MIN_NUM,内存池中最小空闲内存块数为MIN_IDLE,内存池最大空闲内存块数为MAX_IDLE,方法是:
①初始化创建MIN_NUM个空闲内存块;
②池中空闲内存块数量低于MIN_IDLE时,触发内存池调整,添加MIN_IDLE个内存块;
③池中空闲内存块数量高于MAX_IDLE时,触发内存发池调整,删除MIN_IDLE个内存块
④调整过程确保内存池中内存块数不多于MAX_NUM个,也不少于MIN_NUM个。
对以上参数的合理设置可以保证内存池能动态地处理内存块过多或过少时的情况,同时在处理大量请求时,避免请求线程等待太久。
(2) 设置内存池模式
内存池的工作模式能够影响的调整行为:
①可增可减模式:内存池处于动态管理状态,实时调整内存块的数量,在条件允许的情况下增加或删除空闲内存块。
②只增不减模式:内存池处于动态管理状态,内存池只会做出添加内存块的调整行为,不会做出删减内存块的调整。
③不增不减模式:内存池处于动态管理状态,既不增加内存块,也不删除内存块。
对内存池模式的设置应尽可能多地满足不同的应用场合,使内存池具有更好的适应性和通用性。相对于其他两种模式来说,可增可减模式适应性较强,既不浪费系统资源,又可提供良好的服务。
2.3 内存池中内存块组织结构的调整
将内存块大小固定的内存池在使用时将遇到诸多不便。不同的任务线程对于内存大小的需求不一样,对于一般的服务,可能线程所需要的内存块只在几十~几百字节之间,但对于另外一些服务,线程将需要几千甚至几兆的内存来处理数据。因此,合适的内存块的大小将影响请求线程的效率。内存块组织结构如图3所示。
3 代码组织
借鉴C++语言中的面向对象的思想,在C语言中利用结构体模拟C++语言中的类,用函数指针模拟类方法,通过指针强制转换实现数据隐藏。头文件.h中包含数据结构,而.c文件中包含实际的内存池结构,这样可避免用户操作结构体中的数据成员虽然不能真正地像C++中隐藏数据,但是也达到了一定的隐藏效果[5-6]。
3.1 内存池使用方法
mp_mem_pool *pool = create_mem_pool();
pool->init(pool, NULL,“log。txt”);
pool->find_min_idle_index(pool);
pool->palloc(pool, index, size);
destroy_mem_pool( pool);
3.2 函数与接口的功能
struct mp_mem_pool_s{
MPBOOL (*init)(mp_mem_pool *pool, mp_mem_conf *conf, const char *log_file);
void (*reset)(mp_mem_pool *pool);
void(*reset_memblock)(mp_mem_pool *pool, const int index);
void( *get_mp_status )( mp_mem_pool *pool);
void(*print_mp_status)(mp_mem_pool *pool);
int(*find_min_idle_index)(mp_mem_pool *pool);
void *(*palloc)(mp_mem_pool *pool, const int index, size_t size);
void (*pnalloc)(mp_mem_pool *pool, const int index, size_t size);
void (*pcalloc)(mp_mem_pool *pool, const int index, size_t size);
void (*pmemalign)(mp_mem_pool *pool, const int index, size_t size, size_t alignment);
mp_mem_pool *create_mem_pool();
void destroy_mem_pool( mp_mem_pool* pool );
函数用户接口较为简单,主要为创建和销毁内存池的接口,以及查找池中空闲内存块索引。内存池本身也有自己的接口struct mp_mem_pool_s,只有类似C++中的成员函数没有数据,所有数据都在实现文件中进行处理,这样隐藏数据的好处是避免用户随意操作内存池管理单元中的数据。
create_mem_pool: 创建内存池;
destroy_mem_pool: 销毁内存池;
init: 初始化内存池(没有初始化是无法使用的,可以根据配置文件调节内存池行为);
reset:关闭内存池,将其退回到创建时的状态;
reset_memblock:重置具体的内存块,将其退回到初始化时的状态;
get_mp_status:获取内存池状态(当前的内存块数量,最大内存块数,以及空闲内存块数量等);
print_mp_status:打印内存池的工作状态到终端上显示;
find_min_idle_index:返回内存池中空闲内存块的索引;
palloc:请求线程申请到内存块之后,调用该函数进行内存分配操作,分配时进行对齐处理;
pnalloc:请求线程申请到内存块之后,调用该函数进行内存分配操作,分配时不进行对齐处理;
pcalloc:请求线程申请到内存块之后,调用该函数进行内存分配操作,分配时进行对齐处理,同时将内存清零;
pmemalign:分配大块内存的操作函数。
实际的应用中内存池通常都是与线程池、以及任务池结合在一起使用,但各个模块之间耦合越紧,模块的重用就会越困难,移植性越低。因此内存池的接口应尽可能地保持其独立性,不依赖外部条件。而内存池的使用者只需要做初期初始化工作,将描述内存池的结构体作为全局变量,然后在线程的工作函数中调用find_min_idle_index寻找到可用内存块索引即可,操作简单方便[6-8]。
4 比较与测试
(1) 测试环境
Intel(R) Core(TM) i3-2100 CPU @ 2.80 GHz,2 GB内存;Fedora 14(内核2.6.35.14-106.fc14.i686,GCC 4.5.1,GLIBC 2.12.90)
(2) 测试设计
内存池的使用相比线程中直接调用Malloc、Free函数分配和销毁内存的优势,主要体现在一次性申请连续N块内存,并且在程序结束后统一释放。而多线程环境中每个线程单独调用Malloc、Free则需要大量的系统调用开销,同时,将可能产生许多内存碎片。而内存池的使用能够节省Malloc、Free不断地调用时间,避免了可能出现的内存碎片,从而保证内存池的使用有利于复用和管理。
针对本测试所设计的测试程序为,在使用内存池环境下,主线程先创建并初始化内存池,内存池中每个Memblock的大小设为1 KB,内存池的配置文件中设置最大内存块数量为201,最小内存块数量为30,最大空闲内存块数量为60,最小空闲内存块数量为10。之后主线程产生200个线程,所有线程的工作就是向内存池申请内存块,之后再在申请到的Memblock中分别分配128 B、1 KB、 2 KB,以及同时申请128 B和1 KB,然后用time命令来计算user time和sys time。
在不使用内存池情况下,每个线程将单独调用malloc和free函数来分配和释放内存,同样分别分配128 B,1 KB,2 KB以及同时申请128 B和1 KB内存。需要注意的是,为了避免客观因素影响,两个测试程序中的其余部分应尽量一致。每种情况进行100次测试,平均得到的测试结果如表1所示。
(3) 结果分析
由表1可见,在不使用内存池的测试中,当一个线程中多次分配以及释放时,将消耗更多的时间。而使用内存池的结果还是比较理想的。每个线程分配内存的大小对于用户时间和系统时间几乎毫无影响,它不需要Malloc和Free不断地操作,节约了大量库函数调用、系统调用的开销,减少了内存碎片,特别对于服务器程序的运行,是非常可观的。
还没注册? 现在免费注册,您即可: ?阅读所有技术文章及下载网站资料; ?定期获得业界最新资讯及设计实例; ?拥有个人空间参与网站及客户活动; ?撰写博客与业界朋友交流分享经验; 已经注册? 登录阅览全部精彩内容 用户名: 密码: