利用二叉樹的思想來實現分配和釋放記憶體方法 雖然大部分系統都有提供記憶體動態分配和釋放函數(即C語言中的malloc和free函數),但是在嵌入式開發中由於系統的限制往往需要自己來實現記憶體管理,如在有些平台上可動態申請的最大空間不能滿足程式設計的需要,有些系統提供的記憶體配置和釋放函數會造成大量的記憶體片段導致記憶體不夠用,在這些時候往往就需要自己先申請一塊較大的記憶體,然後在這個較大的記憶體中進行重新分配,即做一套獨立的記憶體管理程式。其實自己設計一個記憶體管理程式有如下幾個好處:1、便於對程式在運行中對記憶體的使用方式進行監測,如是否有記憶體流失,記憶體使用量的峰值等;2、防止因程式設計不當(如指標越界、野指標)造成對系統的破壞,便於界定問題出現的範圍;3、通過最佳化記憶體管理演算法可以自行減少記憶體片段;4、便於程式移植,在程式移植到不同平台時可以不用考慮系統的記憶體管理情況,只須考慮能否得到大塊記憶體即可。關於記憶體的分配和釋放方法也許有很多方法可以採用,這裡提出一個利用二叉樹的思想來管理記憶體。二叉樹是一種常用的資料結構,被運用到很多種演算法中,二叉樹的定義在這裡就不再做贅述了,其從形態上可以看出二叉樹主要特點是:二叉樹中每一個結點最多有兩個子結點且最多有一個父結點。記憶體的分配即是對一個較大的記憶體塊分割成幾個較小的記憶體塊,一般採用的方法是以2的冪次作為塊的大小(即記憶體粒度),當要分配的記憶體大小不等於所設定的記憶體粒度時則找一個最靠近這個大小且比它大的粒度來分配,這種分配方法完全可以用一個二叉樹的形式來表示,如:是從一個1KB的記憶體空間中分配出64位元組的情況,葉子結點表示被分割出來的記憶體塊,當要尋找一塊閒置記憶體塊時,應當是在這些葉子結點中尋找,如果找不到,則找一個較大一點的空閑結點進行再分配。運用二叉樹的思想主要目的是用於理解空閑空間的回收方法,記憶體的不斷分配和釋放勢必會造成大量的記憶體片段,如果不對記憶體片段進行整理最終會導致無記憶體可用,所以在釋放(free)記憶體時要把連續的記憶體空間合并起來,以免有記憶體片段,但並不是所有的連續空間都適宜合并,如:記憶體塊h、i、j、k為連續空間,其中h和k空間為已被使用的空間,i和j空間是剛被釋放出來的空閑空間,顯然,空閑空間i和j可以組成一個更大的如128B大小的空閑空間,但是這樣做的結果是不僅破壞了二叉樹原有的思想也造成以後若要合并空間h和k會比較複雜,會增加很多演算法,如要判斷空間i和j是否已合并,而且每釋放一個記憶體空間都要對每層的二叉樹進行這種類似的判斷,雖然這樣可以更加減少記憶體片段,但是釋放記憶體函數是個常用的函數,這種計算量特別是在嵌入式開發中是無法承受的,而且出現上面的這種情況在程式中一般較少出現,所以我們對於這樣的連續空間不進行合并,只有對是同一個父結點的兩個葉子結點才進行合并。總的思想是:
對記憶體進行切割時,總是把一塊大的記憶體空間切割成兩塊小的記憶體空間,對記憶體進行合并時,只把原來都是由同一個記憶體塊分割出來的兩塊記憶體塊進行合并。下面就根據這樣的思想來實現記憶體申請和釋放方法:
1.
資料結構定義用一個數組定義記憶體粒度(以2的倍數來定義):#define GRANULARITY_NUMBER 12 const int MemGranularity [GRANULARITY_NUMBER] = {4,8,16,32,64,128,256,512,1024,2048,4096,0xffff}; 建立一個以粒度來分類的記憶體塊指標數組(記憶體桶): typedef struct mem_block_info_struct { void* memptr; //記憶體塊指標 mem_block_info_struct* nextblock;//指向後一個記憶體塊的指標 }MemBlockInfo; typedef struct mem_array_head_struct { MemBlockInfo* freedmemarray;//指向空閑記憶體塊鏈表的指標 MemBlockInfo* usedmemarray;//指向已用記憶體塊鏈表的指標 }MemArrayHead; MemArrayHead MemBucket[GRANULARITY_NUMBER] ={ }; 如MemBucket [0]是指向記憶體塊大小為4位元組的記憶體塊鏈表,MemBucket [1]則是指向記憶體塊大小為8位元組的記憶體塊鏈表,依此類推。 可以在大塊記憶體前面划出一部分記憶體來儲存每個記憶體塊的資訊,如:
這裡就必須要預先估計要劃分出多大的記憶體來儲存每個記憶體塊的資訊,對於較小的運行程式比較容易估計,但是如果程式較複雜則要通過實驗來確定劃分大小,可以事先設定一個宏定義值,以後只要改這個宏定義值就可以了,如:
#define MEMORY_BLOCK_MAX_NUMBER 2048 MemBlockInfo* MemBlockArray = NULL; MemBucket指向的記憶體塊鏈表都按記憶體塊指標(memptr)值進行由低到高的排序,其實只要在插入一個新記憶體塊時按指標值大小有序插入就可以了,這樣做以便於尋找鏈表中的結點,以提高尋找效率。
2.
演算法實現1)、
初始化方法 a. 初始化全域變數; b. 在記憶體塊前部分出一部分用來儲存記憶體塊資訊;c. 從剩餘的記憶體空間開始按設定的記憶體粒度對記憶體進行切割,切割原則是使得切割出來的記憶體塊總數為最小值,方法是按粒度由大到小進行切割,假設有剩餘空間大小為7680B,則可切割出記憶體塊有:4096B、2048B、1024B、512B(這意味著我們先建立4個只有一個結點的二叉樹),這些記憶體塊的指標存到MemBlockArray中,然後根據粒度大小和記憶體塊大小把MemBucket相應指標指到MemBlockArray中。
2
)、動態分配記憶體方法(即
malloc
函數實現方法) a. 根據傳入參數中指定的大小來決定要分配多大的記憶體粒度,當分配大小剛好等於某個粒度大小時,則按該粒度大小來分配,如果找不到相應大小粒度,則找一個比要分配的空間大小要大的最小粒度作為分配空間,例如要分配20位元組空間,則分配一個粒度為32位元組的空間。 b. 根據粒度大小在空閑鏈表中尋找相應空閑空間:若找到則返回該空閑空間指標,並把相應結點移入到已用鏈表中;若沒有找到,則找一個粒度更大一點的空閑塊,然後把該空閑塊進行一半一半地分割,直到分割出得到所要的粒度大小為止,最後把被分割的空閑塊從鏈表中移出,並把該結點的指標都置為NULL,分割出來的空閑塊指標都存到MemBlockArray中,MemBlockArray中新增的數組單元內容作為結點添加到MemBucket空閑鏈表中(按指標大小順序來添加),取分割出來的其中一個最小的空閑塊作為要返回的記憶體塊,並把這個記憶體塊移入到已用鏈表中。 3)、釋放記憶體塊方法(即free函數實現方法) 搜尋所有的已用鏈表,找出與傳入的指標大小一樣的結點,把該結點從已用鏈表中移出到空閑鏈表中,也是按順序插入到空閑鏈表中,如果發現相鄰結點為連續空間,而且相鄰結點都原來為從同一個較大的空閑記憶體塊分割出來的(即為同一個父結點的兩個子結點),則合并這兩個記憶體塊,把合并後的記憶體塊移入到更大粒度的空閑鏈表中,如果在更大的粒度空閑鏈表中又發現有可合并記憶體塊,則再進行合并,依此方法,直到不能合并為止。 合并空閑記憶體塊方法:把這兩個記憶體塊結點都從空閑鏈表中移出,按記憶體位址大小,把較小地址指標的結點移到更大粒度的空閑鏈表中,把較大地址指標的結點中所有指標值都置為NULL。 判斷兩個記憶體塊是否是從同一個較大的空閑記憶體塊分割出來的方法:如 如果記憶體塊1和記憶體塊2都是從同一個較大的空閑記憶體塊分割出來的,則滿足如下條件: (前面記憶體塊+記憶體塊1)/記憶體塊1的大小:這個結果為奇數 (前面記憶體塊+記憶體塊1+記憶體塊2)/記憶體塊2的大小:這個結果為偶數3.該方法優缺點1)、優點:實現方法較簡單,產生的記憶體片段較少。2)、缺點:記憶體空間利用率不高,當記憶體配置中需要較多記憶體塊時,由於要儲存記憶體塊資訊,要浪費較大的記憶體空間,而且儲存記憶體塊資訊的數組長度為固定大小,則在具體應用中可能需要調節該大小,較不靈活。