文獻標識碼: A
文章編號: 0258-7998(2012)11-0146-04
動態內存管理非常耗時,對效率影響很大,然而在實際的編程應用中,卻不可避免地經常要用到堆中的內存。但是通過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 內存池中內存塊的數量動態調整
固定的內存池在有些情況下并不能滿足實際情況的需要,動態內存池常見的調整方法有基于閾值觸發和基于預測公式兩種形式?;陬A測公式方法通過統計學的經驗公式來預測,優點是能夠反應內存池消耗內存的真實傾向,迅速查找和釋放內存塊;缺點是按照統計公式計算的結果,通常局限于某些特定場合和應用,并且在內存池中造成系統資源消耗較大?;陂撝涤|發方法通常按照參數配置來控制內存池的某些參數,優點是實現簡單、通用性強、可控性好;缺點是需要精確計算配置參數,否則性能會急劇下降。為保證內存池的通用性,這里使用參數可調整的閾值觸發方式動態調整內存池。
(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不斷地操作,節約了大量庫函數調用、系統調用的開銷,減少了內存碎片,特別對于服務器程序的運行,是非??捎^的。
本文設計一種在多線程環境下的內存池算法,優化了池中內存塊的維護和查找算法,并保證了接口的簡單易用性,使其更易于與項目集成。
參考文獻
[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 W R. UNIX網路編程(第2卷)[M]. 北京:人民郵電出版社,2010.
[6] 趙海,李志蜀,韓學為,等. 一種鏈式結構在內存管理中的應用[J].高等函授學報:自然科學版,2002,15(4):46-48.
[7] 翁小東.基于UNIX C語言的一種線程池實現[J]. 電腦知識與技術,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.