文獻(xiàn)標(biāo)識碼: A
文章編號: 0258-7998(2012)11-0146-04
動態(tài)內(nèi)存管理非常耗時,對效率影響很大,然而在實(shí)際的編程應(yīng)用中,卻不可避免地經(jīng)常要用到堆中的內(nèi)存。但是通過Malloc函數(shù)或New等進(jìn)行的內(nèi)存分配存在先天缺陷: (1)利用默認(rèn)的內(nèi)存管理函數(shù)在堆上分配和釋放內(nèi)存需要花費(fèi)很多時間;(2)隨著時間的推移,堆上會形成許多內(nèi)存碎片,在應(yīng)用程序進(jìn)行內(nèi)存申請操作將受到更大的影響,導(dǎo)致應(yīng)用程序的運(yùn)行越來越慢[1-3]。
當(dāng)應(yīng)用程序需要對固定大小的對象經(jīng)常性地申請內(nèi)存時,常會采用內(nèi)存池(Memory Pool)技術(shù)來提高內(nèi)存管理效率。經(jīng)典的內(nèi)存池做法是一次性分配大量大小相同的小塊內(nèi)存,通過該技術(shù)可以極大地加快內(nèi)存分配/釋放過程。內(nèi)存池技術(shù)通過批量申請內(nèi)存,降低了內(nèi)存申請次數(shù),從而使操作節(jié)省了時間。在減少了內(nèi)存碎片產(chǎn)生的同時,對性能的提升有顯著的幫助。
綜上,內(nèi)存池有其巨大的優(yōu)勢,但是原有的內(nèi)存池也存在一定的缺陷。在多線程場合下應(yīng)用時,每個新產(chǎn)生的線程如何在O(1)時間內(nèi)獲取內(nèi)存塊,如何保證其安全有效性,以及如何管理內(nèi)存塊的數(shù)量方面存在一定的不足的,本文對此進(jìn)行研究,并給出一種新的解決方案。
1 內(nèi)存池制作原理以及工作流程
本內(nèi)存池基于多線程環(huán)境,需要考慮到多線程下數(shù)據(jù)的安全,以及快速獲取內(nèi)存塊等條件。在獲取內(nèi)存塊索引號時,采用加鎖的方式,雖然會耗費(fèi)一定的時間,但是運(yùn)行安全得到了保障。在程序運(yùn)行之前需創(chuàng)建好內(nèi)存池,并用一個結(jié)構(gòu)體struct mem_pool進(jìn)行封裝,里面包含內(nèi)存池的一些私有數(shù)據(jù)。當(dāng)有新線程產(chǎn)生時,直接像內(nèi)存池申請一塊已經(jīng)分配好了的內(nèi)存,線程的具體內(nèi)存操作都在該內(nèi)存塊中進(jìn)行。同時,內(nèi)存池結(jié)構(gòu)體中隱藏一個管理線程,該線程的工作是定時檢查內(nèi)存池中空閑內(nèi)存塊的數(shù)量是否過多或者過少。當(dāng)過多時,申請釋放,直到達(dá)到門檻值;當(dāng)過少時,申請增加若干,以備不時之需。內(nèi)存池結(jié)構(gòu)如圖1所示。
對于內(nèi)存池中的內(nèi)存塊,采用結(jié)構(gòu)struct mem_block維護(hù)其數(shù)據(jù),該結(jié)構(gòu)以一個鏈表的形式維護(hù)實(shí)際內(nèi)存區(qū)域,結(jié)構(gòu)體中有兩個管理內(nèi)存的區(qū)域:(1)常規(guī)大小鏈表區(qū)域。當(dāng)需要的內(nèi)存小于常規(guī)大小時,則線程的內(nèi)存請求都將從該區(qū)域獲得;當(dāng)該區(qū)域內(nèi)存將滿時,則線程可以繼續(xù)申請同樣大小的內(nèi)存塊,鏈接到該常規(guī)大小鏈表上。(2)大塊內(nèi)存鏈表區(qū)域。當(dāng)線程申請的內(nèi)存超過該內(nèi)存塊的大小時,將在系統(tǒng)中申請一塊大的內(nèi)存鏈接到該大塊內(nèi)存鏈表上,這樣可以對內(nèi)存塊進(jìn)行統(tǒng)一管理;當(dāng)線程壽命結(jié)束時,調(diào)用reset函數(shù)將大塊內(nèi)存釋放,同時重設(shè)常規(guī)內(nèi)存鏈表區(qū)域,只保留最開始的一塊,其余后面申請的塊全部釋放,并標(biāo)記內(nèi)存塊的使用狀態(tài)為空閑,同時重設(shè)一些內(nèi)部指針,指向內(nèi)存塊可用的起始位置[4]。
創(chuàng)建內(nèi)存池結(jié)構(gòu),并初始化,此時在內(nèi)存中存儲著一份內(nèi)存池的動態(tài)管理單元。當(dāng)創(chuàng)建新線程時,該線程通過內(nèi)存塊查找函數(shù),并查詢內(nèi)存池結(jié)構(gòu)。如有空閑內(nèi)存塊則直接將該內(nèi)存塊的索引號index送給線程,同時將該內(nèi)存塊的空閑標(biāo)志flag設(shè)為繁忙;如果內(nèi)存池中沒有可用的空閑內(nèi)存塊,且內(nèi)存塊數(shù)量未達(dá)到設(shè)置的頂峰,則可以申請add_memblock;若正在使用的內(nèi)存塊超過了最大設(shè)置的內(nèi)存塊數(shù)量,則線程將調(diào)用Malloc函數(shù),并自行調(diào)用Free釋放該內(nèi)存塊。管理線程周期性地調(diào)用get_mp_status來查看內(nèi)存池狀態(tài),若空閑線程低于門檻值(最小空閑內(nèi)存塊數(shù)),則調(diào)度add_memblock函數(shù)創(chuàng)建新的內(nèi)存塊到池中;若空閑內(nèi)存塊高于門檻值(最大空閑內(nèi)存數(shù)),則調(diào)度del_memblock銷毀多余的內(nèi)存塊。線程生命周期結(jié)束后,將內(nèi)存塊的繁忙標(biāo)記設(shè)置為空閑狀態(tài),并且重新初始化該內(nèi)存塊,將內(nèi)存塊重新投入到內(nèi)存池中,等待其他線程重復(fù)利用。內(nèi)存池的工作流程如圖2所示。
2 內(nèi)存池主要技術(shù)
2.1 內(nèi)存池中空閑內(nèi)存塊的查找方式
當(dāng)進(jìn)程服務(wù)繁忙時,一些內(nèi)存塊長期被某些線程占用的情況下,也可能延長內(nèi)存池響應(yīng)時間,影響響應(yīng)速度。內(nèi)存池調(diào)度算法的一項(xiàng)重要任務(wù)就是盡可能提高查找空閑內(nèi)存塊的速度。而簡單地遍歷內(nèi)存池鏈表顯然不能滿足請求線程的需要,這種方式不僅延長了調(diào)用者的返回時間,而且極大增加了內(nèi)存池對請求線程的響應(yīng)時間。特別是在服務(wù)器繁忙時,當(dāng)處于請求內(nèi)存塊的線程越多,查找空閑內(nèi)存塊所花費(fèi)的時間就越長。因此,本文提出以下兩種查找方法:
(1) 位圖查找方式
用位圖方式維護(hù)內(nèi)存池中的內(nèi)存塊單元,查找空閑內(nèi)存塊將只需遍歷位圖,位圖按單個字節(jié)進(jìn)行查找效率較高。另外,在線程結(jié)束時的前一刻,維護(hù)當(dāng)前空閑內(nèi)存塊的索引index,可在下次查找空閑內(nèi)存塊時直接獲取這個值,而時間耗費(fèi)是O(1)級的,這將大大提高響應(yīng)時間。
(2) 基于數(shù)組的方式
基于鏈表實(shí)現(xiàn)的內(nèi)存池,在創(chuàng)建內(nèi)存池時或者每次增加池中內(nèi)存塊數(shù)時都必須分配新的管理結(jié)構(gòu),再進(jìn)行鏈接;并且在查找空閑內(nèi)存塊時,需要遍歷內(nèi)存池,其直接后果是造成線程請求內(nèi)存塊的時間大大增加。而數(shù)組的方式有其天然的優(yōu)勢,當(dāng)用位圖查找到了空閑內(nèi)存塊的索引后,也即知道了內(nèi)存塊在數(shù)組中的位置,由此可以迅速地定位空閑內(nèi)存塊,大大提高了響應(yīng)速度。
2.2 內(nèi)存池中內(nèi)存塊的數(shù)量動態(tài)調(diào)整
固定的內(nèi)存池在有些情況下并不能滿足實(shí)際情況的需要,動態(tài)內(nèi)存池常見的調(diào)整方法有基于閾值觸發(fā)和基于預(yù)測公式兩種形式?;陬A(yù)測公式方法通過統(tǒng)計學(xué)的經(jīng)驗(yàn)公式來預(yù)測,優(yōu)點(diǎn)是能夠反應(yīng)內(nèi)存池消耗內(nèi)存的真實(shí)傾向,迅速查找和釋放內(nèi)存塊;缺點(diǎn)是按照統(tǒng)計公式計算的結(jié)果,通常局限于某些特定場合和應(yīng)用,并且在內(nèi)存池中造成系統(tǒng)資源消耗較大?;陂撝涤|發(fā)方法通常按照參數(shù)配置來控制內(nèi)存池的某些參數(shù),優(yōu)點(diǎn)是實(shí)現(xiàn)簡單、通用性強(qiáng)、可控性好;缺點(diǎn)是需要精確計算配置參數(shù),否則性能會急劇下降。為保證內(nèi)存池的通用性,這里使用參數(shù)可調(diào)整的閾值觸發(fā)方式動態(tài)調(diào)整內(nèi)存池。
(1) 相關(guān)參數(shù)合理性設(shè)置
設(shè)內(nèi)存池中最大內(nèi)存塊數(shù)為MAX_NUM,內(nèi)存池中最小內(nèi)存塊數(shù)為MIN_NUM,內(nèi)存池中最小空閑內(nèi)存塊數(shù)為MIN_IDLE,內(nèi)存池最大空閑內(nèi)存塊數(shù)為MAX_IDLE,方法是:
①初始化創(chuàng)建MIN_NUM個空閑內(nèi)存塊;
②池中空閑內(nèi)存塊數(shù)量低于MIN_IDLE時,觸發(fā)內(nèi)存池調(diào)整,添加MIN_IDLE個內(nèi)存塊;
③池中空閑內(nèi)存塊數(shù)量高于MAX_IDLE時,觸發(fā)內(nèi)存發(fā)池調(diào)整,刪除MIN_IDLE個內(nèi)存塊
④調(diào)整過程確保內(nèi)存池中內(nèi)存塊數(shù)不多于MAX_NUM個,也不少于MIN_NUM個。
對以上參數(shù)的合理設(shè)置可以保證內(nèi)存池能動態(tài)地處理內(nèi)存塊過多或過少時的情況,同時在處理大量請求時,避免請求線程等待太久。
(2) 設(shè)置內(nèi)存池模式
內(nèi)存池的工作模式能夠影響的調(diào)整行為:
①可增可減模式:內(nèi)存池處于動態(tài)管理狀態(tài),實(shí)時調(diào)整內(nèi)存塊的數(shù)量,在條件允許的情況下增加或刪除空閑內(nèi)存塊。
②只增不減模式:內(nèi)存池處于動態(tài)管理狀態(tài),內(nèi)存池只會做出添加內(nèi)存塊的調(diào)整行為,不會做出刪減內(nèi)存塊的調(diào)整。
③不增不減模式:內(nèi)存池處于動態(tài)管理狀態(tài),既不增加內(nèi)存塊,也不刪除內(nèi)存塊。
對內(nèi)存池模式的設(shè)置應(yīng)盡可能多地滿足不同的應(yīng)用場合,使內(nèi)存池具有更好的適應(yīng)性和通用性。相對于其他兩種模式來說,可增可減模式適應(yīng)性較強(qiáng),既不浪費(fèi)系統(tǒng)資源,又可提供良好的服務(wù)。
2.3 內(nèi)存池中內(nèi)存塊組織結(jié)構(gòu)的調(diào)整
將內(nèi)存塊大小固定的內(nèi)存池在使用時將遇到諸多不便。不同的任務(wù)線程對于內(nèi)存大小的需求不一樣,對于一般的服務(wù),可能線程所需要的內(nèi)存塊只在幾十~幾百字節(jié)之間,但對于另外一些服務(wù),線程將需要幾千甚至幾兆的內(nèi)存來處理數(shù)據(jù)。因此,合適的內(nèi)存塊的大小將影響請求線程的效率。內(nèi)存塊組織結(jié)構(gòu)如圖3所示。
3 代碼組織
借鑒C++語言中的面向?qū)ο蟮乃枷?,在C語言中利用結(jié)構(gòu)體模擬C++語言中的類,用函數(shù)指針模擬類方法,通過指針強(qiáng)制轉(zhuǎn)換實(shí)現(xiàn)數(shù)據(jù)隱藏。頭文件.h中包含數(shù)據(jù)結(jié)構(gòu),而.c文件中包含實(shí)際的內(nèi)存池結(jié)構(gòu),這樣可避免用戶操作結(jié)構(gòu)體中的數(shù)據(jù)成員雖然不能真正地像C++中隱藏數(shù)據(jù),但是也達(dá)到了一定的隱藏效果[5-6]。
3.1 內(nèi)存池使用方法
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 函數(shù)與接口的功能
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 );
函數(shù)用戶接口較為簡單,主要為創(chuàng)建和銷毀內(nèi)存池的接口,以及查找池中空閑內(nèi)存塊索引。內(nèi)存池本身也有自己的接口struct mp_mem_pool_s,只有類似C++中的成員函數(shù)沒有數(shù)據(jù),所有數(shù)據(jù)都在實(shí)現(xiàn)文件中進(jìn)行處理,這樣隱藏數(shù)據(jù)的好處是避免用戶隨意操作內(nèi)存池管理單元中的數(shù)據(jù)。
create_mem_pool: 創(chuàng)建內(nèi)存池;
destroy_mem_pool: 銷毀內(nèi)存池;
init: 初始化內(nèi)存池(沒有初始化是無法使用的,可以根據(jù)配置文件調(diào)節(jié)內(nèi)存池行為);
reset:關(guān)閉內(nèi)存池,將其退回到創(chuàng)建時的狀態(tài);
reset_memblock:重置具體的內(nèi)存塊,將其退回到初始化時的狀態(tài);
get_mp_status:獲取內(nèi)存池狀態(tài)(當(dāng)前的內(nèi)存塊數(shù)量,最大內(nèi)存塊數(shù),以及空閑內(nèi)存塊數(shù)量等);
print_mp_status:打印內(nèi)存池的工作狀態(tài)到終端上顯示;
find_min_idle_index:返回內(nèi)存池中空閑內(nèi)存塊的索引;
palloc:請求線程申請到內(nèi)存塊之后,調(diào)用該函數(shù)進(jìn)行內(nèi)存分配操作,分配時進(jìn)行對齊處理;
pnalloc:請求線程申請到內(nèi)存塊之后,調(diào)用該函數(shù)進(jìn)行內(nèi)存分配操作,分配時不進(jìn)行對齊處理;
pcalloc:請求線程申請到內(nèi)存塊之后,調(diào)用該函數(shù)進(jìn)行內(nèi)存分配操作,分配時進(jìn)行對齊處理,同時將內(nèi)存清零;
pmemalign:分配大塊內(nèi)存的操作函數(shù)。
實(shí)際的應(yīng)用中內(nèi)存池通常都是與線程池、以及任務(wù)池結(jié)合在一起使用,但各個模塊之間耦合越緊,模塊的重用就會越困難,移植性越低。因此內(nèi)存池的接口應(yīng)盡可能地保持其獨(dú)立性,不依賴外部條件。而內(nèi)存池的使用者只需要做初期初始化工作,將描述內(nèi)存池的結(jié)構(gòu)體作為全局變量,然后在線程的工作函數(shù)中調(diào)用find_min_idle_index尋找到可用內(nèi)存塊索引即可,操作簡單方便[6-8]。
4 比較與測試
(1) 測試環(huán)境
Intel(R) Core(TM) i3-2100 CPU @ 2.80 GHz,2 GB內(nèi)存;Fedora 14(內(nèi)核2.6.35.14-106.fc14.i686,GCC 4.5.1,GLIBC 2.12.90)
(2) 測試設(shè)計
內(nèi)存池的使用相比線程中直接調(diào)用Malloc、Free函數(shù)分配和銷毀內(nèi)存的優(yōu)勢,主要體現(xiàn)在一次性申請連續(xù)N塊內(nèi)存,并且在程序結(jié)束后統(tǒng)一釋放。而多線程環(huán)境中每個線程單獨(dú)調(diào)用Malloc、Free則需要大量的系統(tǒng)調(diào)用開銷,同時,將可能產(chǎn)生許多內(nèi)存碎片。而內(nèi)存池的使用能夠節(jié)省Malloc、Free不斷地調(diào)用時間,避免了可能出現(xiàn)的內(nèi)存碎片,從而保證內(nèi)存池的使用有利于復(fù)用和管理。
針對本測試所設(shè)計的測試程序?yàn)?,在使用?nèi)存池環(huán)境下,主線程先創(chuàng)建并初始化內(nèi)存池,內(nèi)存池中每個Memblock的大小設(shè)為1 KB,內(nèi)存池的配置文件中設(shè)置最大內(nèi)存塊數(shù)量為201,最小內(nèi)存塊數(shù)量為30,最大空閑內(nèi)存塊數(shù)量為60,最小空閑內(nèi)存塊數(shù)量為10。之后主線程產(chǎn)生200個線程,所有線程的工作就是向內(nèi)存池申請內(nèi)存塊,之后再在申請到的Memblock中分別分配128 B、1 KB、 2 KB,以及同時申請128 B和1 KB,然后用time命令來計算user time和sys time。
在不使用內(nèi)存池情況下,每個線程將單獨(dú)調(diào)用malloc和free函數(shù)來分配和釋放內(nèi)存,同樣分別分配128 B,1 KB,2 KB以及同時申請128 B和1 KB內(nèi)存。需要注意的是,為了避免客觀因素影響,兩個測試程序中的其余部分應(yīng)盡量一致。每種情況進(jìn)行100次測試,平均得到的測試結(jié)果如表1所示。
(3) 結(jié)果分析
由表1可見,在不使用內(nèi)存池的測試中,當(dāng)一個線程中多次分配以及釋放時,將消耗更多的時間。而使用內(nèi)存池的結(jié)果還是比較理想的。每個線程分配內(nèi)存的大小對于用戶時間和系統(tǒng)時間幾乎毫無影響,它不需要Malloc和Free不斷地操作,節(jié)約了大量庫函數(shù)調(diào)用、系統(tǒng)調(diào)用的開銷,減少了內(nèi)存碎片,特別對于服務(wù)器程序的運(yùn)行,是非常可觀的。
本文設(shè)計一種在多線程環(huán)境下的內(nèi)存池算法,優(yōu)化了池中內(nèi)存塊的維護(hù)和查找算法,并保證了接口的簡單易用性,使其更易于與項(xiàng)目集成。
參考文獻(xiàn)
[1] 翁小東. 電信級用系統(tǒng)中多進(jìn)程共享內(nèi)存池的實(shí)現(xiàn)[J].電腦知識技術(shù),2009,4(5-12):3300-3306
[2] 劉小華. 基于C++的內(nèi)存池的實(shí)現(xiàn)[J].福建電腦, 2008(1):82-83.
[3] 張海闊,趙沖沖,王玉,等. 鏈表快速查找的內(nèi)存池管理優(yōu)化技術(shù)研究[C]. 2007年全國高性能計算學(xué)術(shù)年會, 2007.
[4] 胡萌,趙衛(wèi)東,王志成,等. 線程池設(shè)計與動態(tài)優(yōu)化[J]. 電腦知識與技術(shù),2008,4(9):2753-2755.
[5] STEVENS W R. UNIX網(wǎng)路編程(第2卷)[M]. 北京:人民郵電出版社,2010.
[6] 趙海,李志蜀,韓學(xué)為,等. 一種鏈?zhǔn)浇Y(jié)構(gòu)在內(nèi)存管理中的應(yīng)用[J].高等函授學(xué)報:自然科學(xué)版,2002,15(4):46-48.
[7] 翁小東.基于UNIX C語言的一種線程池實(shí)現(xiàn)[J]. 電腦知識與技術(shù),2009,5(16):4222-4223.
[8] LOWELL R M. A C++ pooled, shared memory allocator for simulator development[C]. IEEE,2004,Proceedings of the 37th Annual Simulation Symposium,2004.