nucleus操作系统内存池模块移植的研究与应用 Linux多线程应用程序中内存池的设计与实现

动态内存管理非常耗时,对效率影响也很大,但在实际编程应用中,不可避免地要使用堆中的内存,而通过Malloc函数或者New函数进行内存分配存在着先天的缺陷:(1)使用默认的内存管理函数在堆上分配和释放内存需要花费大量的时间;(2)随着时间的推移,堆上会形成很多的内存碎片,对应用程序的内存申请操作造成较大的影响,导致应用程序的运行速度越来越慢[1-3]。

当应用程序需要频繁为固定大小的对象申请内存时,通常会使用内存池技术来提高内存管理效率。经典的内存池做法是一次性分配大量大小相同的小内存块。该技术可以大大加快内存分配/释放过程。内存池技术通过批量申请内存来减少内存申请次数,从而节省时间。在减少内存碎片产生的同时,也显著有助于提高性能。

综上所述,内存池有着很大的优势,但是原有的内存池也存在一定的缺陷,在多线程场景下使用时,每个新生成的线程如何在O(1)时间内获取内存块,如何保证其安全性和有效性,如何管理内存块的数量等都存在一定的不足,本文对此进行了研究,并提供了一种新的解决方案。

1 内存池生产原理及工作流程

此内存池是基于多线程环境的,需要考虑多线程环境下数据的安全性,以及快速访问内存块等条件。在获取内存块索引号时,采用加锁的方式,虽然需要花费一定的时间,但是操作安全性是有保障的。在程序运行前,需要创建内存池,并用一个结构struct mem_pool进行封装,里面包含内存池的一些私有数据。当产生新的线程时,直接从内存池中申请一块分配的内存,线程的具体内存操作都在这个内存块中进行。同时,在内存池结构中隐藏着一个管理线程,这个线程的工作就是定期检查内存池中空闲内存块的数量是过多还是过少。过多时,申请释放,直至达到阈值;过少时,再申请一定数量的空闲内存块,以备不时之需。内存池结构如图1所示。

对于内存池中的内存块,使用结构体struct mem_block来维护其数据。该结构体以链表的形式维护实际的内存区域。结构体中有两个内存管理区域:(1)常规大小链表区域。当所需内存小于常规大小时,线程的内存请求将从此区域获取;当此区域内存即将满时,线程可以继续申请相同大小的内存块并链接到常规大小链表中。(2)大块内存链表区域。当线程申请的内存超出内存块大小时,系统会申请一个大块内存并链接到大块内存链表中,这样就可以统一管理内存块;当线程生命结束时,调用reset函数释放大块内存,同时重置常规内存链表区域,只保留第一个块,释放后面申请的所有剩余块。该内存块的使用状态被标记为空闲,并且一些内部指针被重置,以指向该内存块可用的起始位置[4]。

创建内存池结构并初始化,此时内存中保存了一个内存池的动态管理单元。当新线程被创建时,线程通过内存块查找函数查找内存池结构,如果有空闲的内存块,则直接将该内存块的索引号发送给线程,并将该内存块的空闲标志位设置为busy;如果内存池中没有可用的空闲内存块,并且内存块数量还没有达到设定的峰值,则可以申请add_memblock;如果正在使用的内存块超出设置的最大内存块数,线程会调用Malloc函数,调用Free释放内存块。管理线程定期调用get_mp_status检查内存池状态,如果空闲线程数低于阈值(空闲内存块最小数量),则调度add_memblock函数在池中创建新的内存块;如果空闲内存块数量高于阈值(最大空闲内存块数量),则调度del_memblock销毁多余的内存块。线程生命周期结束后,将内存块的busy标志设置为free状态,并重新初始化内存块,将内存块放回内存池中,等待其他线程重用。内存池的工作流程如图2所示。

2 内存池主要技术

2.1 如何在内存池中查找空闲的内存块

当进程服务繁忙时nucleus操作系统内存池模块移植的研究与应用,如果某些内存块被某些线程长期占用,则可能延长内存池响应时间,影响响应速度。内存池调度算法的一个重要任务就是尽可能提高查找空闲内存块的速度。单纯地遍历内存池链表显然不能满足请求线程的需求,这种方式不仅延长了调用者的返回时间,而且也大大增加了内存池对请求线程的响应时间。尤其在服务器繁忙时,请求内存块的线程越多,查找空闲内存块所需的时间就越长。因此本文提出了以下两种查找方法:

(1)位图搜索法

内存池中的内存块单元以位图的方式维护,查找空闲内存块只需要遍历位图,按单字节查找效率较高。另外,线程结束前会维护当前空闲内存块的索引,下次查找空闲内存块时可以直接获得该值,时间消耗为O(1),将大大提升响应时间。

(2)基于数组的方法

基于链表的内存池在创建内存池或者增加内存块数量时,都必须分配新的管理结构然后进行链表化,另外查找空闲内存块时需要遍历内存池,这直接增加了线程申请内存块的时间。而数组的方式有其天然的优势,使用位图找到空闲内存块的索引后,就知道了该内存块在数组中的位置,因此可以很快的找到空闲内存块,大大提高了响应速度。

2.2 动态调整内存池的内存块数量

在某些情况下nucleus操作系统内存池模块移植的研究与应用,固定的内存池无法满足实际情况的需求,动态内存池常见的调整方法包括基于阈值触发和基于预测公式调整。基于预测公式的方法利用统计经验公式进行预测,其优点是能够反映内存池消耗的真实趋势,快速发现并释放内存块,其缺点是按照统计公式计算的结果通常局限于某些特定的场合和应用,导致内存池的系统资源消耗较大。基于阈值触发的方法通常通过参数配置来控制内存池的某些参数,其优点是实现简单,通用性强,可控性好,缺点是需要精确计算配置参数,否则性能会急剧下降。为了保证内存池的通用性,这里采用参数可调的阈值触发方法来动态调整内存池。

(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中使用结构体模拟类,用函数指针模拟类方法,通过指针强制转换实现数据隐藏。头文件.h中包含数据结构,.c文件中包含实际的内存池结构。这样可以防止用户操作结构体中的数据成员。虽然不能像C++那样真正做到数据隐藏,但也达到了一定的隐藏效果[5-6]。

3.1 如何使用内存池

mp_mem_pool *pool = create_mem_pool();

池->初始化(池,NULL,“log.txt”);

池->find_min_idle_index(池);

池->palloc(池,索引,大小);

destroy_mem_pool(池);

3.2 函数和接口

结构mp_mem_pool_s{

MPBOOL (*init)(mp_mem_pool *pool, mp_mem_conf *conf, const char *log_file);

无效(*重置)(mp_mem_pool *pool);

无效(*reset_memblock)(mp_mem_pool *pool,const int index);

无效(* get_mp_status)(mp_mem_pool *pool);

无效(* print_mp_status)(mp_mem_pool *pool);

int(*find_min_idle_index)(mp_mem_pool *pool);

无效*(* palloc)(mp_mem_pool *pool,const int index,size_t size);

无效(* pnalloc)(mp_mem_pool *pool,const int index,size_t size);

无效(* pcalloc )( mp_mem_pool *池, const int index , size_t 大小);

void (*pmemalign)(mp_mem_pool *pool,const int index,size_t size,size_t alignment);

mp_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 RAM;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的大小设置为1KB,在内存池的配置文件中设置最大内存块数为201,最小内存块数为30,最大空闲内存块数为60,最小空闲内存块数为10。之后主线程生成200个线程,所有线程的工作都是从内存池中申请内存块,然后在申请的Memblock中分别分配128B、1KB、2KB、128B和1KB,然后使用time命令计算用户时间和系统时间。

不使用内存池的情况下,每个线程都会调用malloc和free函数进行内存的分配和释放,分别分配128B、1KB、2KB,同时申请128B和1KB内存。需要注意的是,为了避免客观因素的影响,其余两个测试方案尽量保持一致。每种情况都进行了100次测试,平均测试结果如表1所示。

(3)结果分析

如表1所示,在未使用内存池的测试中,当在一个线程中进行多次分配和释放时,会消耗较多的时间。但使用内存池的结果还是比较理想的,每个线程分配的内存大小对用户时间和系统时间几乎没有影响,不需要不断的Malloc和Free操作,节省了大量的库函数调用和系统调用开销,减少了内存碎片,这一点非常有意义,特别是对于服务器程序的运行。

本文设计了多线程环境下的内存池算法,优化了池中内存块的维护和查找算法,并且保证了接口的简单性和易用性,使其更容易与项目集成。

参考

[1] 翁晓东. 电信级系统中多进程共享内存池的实现[J]. 电脑知识与技术, 2009, 4(5-12): 3300-3306

[2]刘小华.基于C++的内存池的实现[J].福建计算机,2008(1):82-83.

[3]张海阔,赵崇崇,王宇,等.快速链表查找的内存池管理优化技术研究[C].2007全国高性能计算学术年会,2007.

[4]胡猛,赵卫东,王志成,等.线程池设计与动态优化[J].电脑知识与技术,2008,4(9):2753-2755.

[5] STEVENS WR. UNIX网络编程(第2卷)[M].北京:人民邮电出版社, 2010.

[6]赵海,李志书,韩学伟,等.链式结构在内存管理中的应用[J].高等教育学报:自然科学版,2002,15(4):46-48.

[7] 翁晓东. 一种基于UNIX C语言的线程池实现[J]. 电脑知识与技术, 2009, 5(16): 4222-4223.

[8] LOWELL R M. 用于模拟器开发的 C++ 池化共享内存分配器 [C]。IEEE,2004 年,第 37 届年度模拟研讨会论文集,2004 年。

© 版权声明
THE END
喜欢就支持一下吧
点赞278赞赏 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容