《電子技術(shù)應(yīng)用》
您所在的位置:首頁 > 嵌入式技術(shù) > 設(shè)計應(yīng)用 > 一種Linux多線程應(yīng)用下內(nèi)存池的設(shè)計與實(shí)現(xiàn)
一種Linux多線程應(yīng)用下內(nèi)存池的設(shè)計與實(shí)現(xiàn)
來源:電子技術(shù)應(yīng)用2012年第11期
許 健, 于鴻洋
電子科技大學(xué) 電子科學(xué)技術(shù)研究院,四川 成都 611731
摘要: 對內(nèi)存池中內(nèi)存塊獲取、分配機(jī)制、內(nèi)存塊大小、內(nèi)存釋放,以及在多線程環(huán)境下的安全處理等細(xì)節(jié)進(jìn)行了研究,保證了在多線程環(huán)境下能夠快速同時采用一種基于數(shù)組的鏈表機(jī)制,改進(jìn)內(nèi)存池中內(nèi)存塊的查找算法,將其時間復(fù)雜度穩(wěn)定在O(1),避免了傳統(tǒng)內(nèi)存池中請求的線程數(shù)目過多時,引發(fā)的獲取內(nèi)存塊性能下降的問題。同時在內(nèi)部設(shè)置管理線程,動態(tài)增加或刪除空閑的內(nèi)存塊。實(shí)驗(yàn)結(jié)果表明,改進(jìn)后的內(nèi)存池與傳統(tǒng)的內(nèi)存分配方式相比消耗更小,效率更好。
中圖分類號: TP31
文獻(xiàn)標(biāo)識碼: A
文章編號: 0258-7998(2012)11-0146-04
Design and implement of memory pool under multi-thread of Linux
Xu Jian,Yu Hongyang
Research Institute of Electronic Science and Technology, University of Electronic Science and Technology, Chengdu 611731, China
Abstract: After much research on the allocate and gain mechanism , dynamic adjustment, safety use, free method, the basic size of the memory block ensure it's can work well in mutli-mem enviorment. Meanwhile, using a mechanism of array-based linked list to improve the searching and allocation algorithm in the memory pool, making the time complexity stable at O(1),and avoid the degradation of allocation and query performance when aquired memory mem number is too large in the traditional mem pool. Experimental results show that the improved memory pool has a smaller cost and better efficiency compared with the traditional memory distribution.
Key words : memory pool; memroy block find method; Linux; multi-thread

    動態(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.

此內(nèi)容為AET網(wǎng)站原創(chuàng),未經(jīng)授權(quán)禁止轉(zhuǎn)載。