本文導讀
在程序運行過程中,可能產生一些數據,例如,串口接收的數據,ADC采集的數據。若需將數據存儲在內存中,以便進一步運算、處理,則應為其分配合適的內存空間,數據處理完畢后,再釋放相應的內存空間。為了便于內存的分配和釋放,AWorks提供了兩種內存管理工具:堆和內存池。
本文為《面向AWorks框架和接口的編程(上)》第三部分軟件篇——第9章內存管理——第1~2小節:堆管理器和內存池。
本章導讀
在計算機系統中,數據一般存放在內存中,只有當數據需要參與運算時,才從內存中取出,交由CPU運算,運算結束再將結果存回內存中。這就需要系統為各類數據分配合適的內存空間。
一些數據需要的內存大小在編譯前可以確定。主要有兩類:一類是全局變量或靜態變量,這部分數據在程序的整個生命周期均有效,在編譯時就為這些數據分配了固定的內存空間,后續直接使用即可,無需額外的管理;一類是局部變量,這部分數據僅在當前作用域中有效(如函數中),它們需要的內存自動從棧中分配,也無需額外的管理,但需要注意的是,由于這一部分數據的內存從棧中分配,因此,需要確保應用程序有足夠的棧空間,盡量避免定義內存占用較大的局部變量(比如:一個占用數K內存的數組),以避免棧溢出,棧溢出可能破壞系統關鍵數據,極有可能造成系統***。
一些數據需要的內存大小需要在程序運行過程中根據實際情況確定,并不能在編譯前確定。例如,可能臨時需要1K內存空間用于存儲遠端通過串口發過來的數據。這就要求系統具有對內存空間進行動態管理的能力,在用戶需要一段內存空間時,向系統申請,系統選擇一段合適的內存空間分配給用戶,用戶使用完畢后,再釋放回系統,以便系統將該段內存空間回收再利用。在AWorks中,提供了兩種常見的內存管理方法:堆和內存池。
9.1 堆管理器
堆管理器用于管理一段連續的內存空間,可以在滿足系統資源的情況下,根據用戶的需求分配任意大小的內存塊,完全“按需分配”,當用戶不再使用分配的內存塊時,又可以將內存塊釋放回堆中供其它應用分配使用。類似于C標準庫中的malloc()/free()函數實現的功能。
9.1.1 堆管理器的原理概述
在使用堆管理器前,首先通過一個示例對其原理作簡要的介紹,以便用戶更加有效的使用堆管理器。例如,使用堆管理器對1024字節的內存空間進行管理。初始時,所有內存均處于空閑狀態,可以將整個內存空間看作一個大的空閑內存塊。示意圖詳見圖9.1。
圖9.1 初始狀態——單個空閑內存塊
內存分配的策略是:當用戶申請一定大小的內存空間時,堆管理器會從第一個空閑內存塊開始查找,直到找到一個滿足用戶需求的空閑內存塊(即容量不小于用戶請求的內存空間大小),然后從該空閑內存塊中分配出用戶指定大小的內存空間。
例如,用戶向該堆管理器申請100字節的內存空間,由于第一個空閑內存塊的容量為1024,滿足需求,因此,可以從中分配100字節的內存空間給該用戶,分配后的內存示意圖詳見圖9.2。
圖9.2 分配100字節——分割為兩個內存塊
注:填充為陰影表示該塊已被分配,否則,表示該塊未被分配,處于空閑狀態,數字表示該塊的容量。
同理,若用戶再向該堆管理器連續請求三次內存空間,每次請求的內存空間容量分別為:150、250、200字節,則分配后的內存示意圖詳見圖9.3。
圖9.3 再分配100、300、200字節內存空間——分割為五個內存塊
隨著分配的繼續,內存塊可能越來越多。若用戶的請求無法滿足,則會分配失敗,如果在圖9.3的基礎上,用戶再請求400字節內存空間,由于僅有的一個空閑塊,且容量為324字節,無法滿足需求,此時,分配會失敗,用戶無法得到一段有效的內存空間。
當用戶申請的某一段內存不再使用時,應該將該內存塊釋放回堆中,以便回收再利用。
回收的策略是:當用戶將某一內存塊釋放回堆中時,首先,將該內存塊變為空閑塊,若該內存塊相鄰的內存塊也為空閑塊,則將它們合并為一個大的空閑內存塊。
例如,在圖9.3的基礎上,釋放之前分配的250字節內存空間,則首先將該內存塊變為空閑塊,示意圖詳見圖9.4。
圖9.4 釋放一個內存塊——相鄰塊不為空閑塊
由于該內存塊前后均不為空閑塊,因此,無需作合并操作。此時,釋放了250字節的空閑空間,堆***存在574字節的內存空間。但是,若用戶此時向該堆管理器申請400字節的內存空間,由于第一個空閑塊和第二個空閑塊均不滿足需求,因此,內存分配還是會失敗。
這里暴露出了一個堆管理器的缺點。頻繁的分配和釋放大小不一的內存塊,將產生諸多內存碎片,即圖9.4中被已分配內存塊打斷的空閑內存塊,圖中容量為250字節和容量為324字節的空閑內存塊被一個已分配的容量為200字節的內存塊打斷,使得各個空閑塊在物理上不連續,無法形成一個大的空閑塊。這種情況下,即使請求的內存空間大小小于當前堆的空閑空間總大小,也可能會由于沒有一個大的空閑塊而分配失敗。
在圖9.4的基礎上,即使再釋放掉第一次分配的100字節內存空間,使總空閑空間達到674字節,詳見圖9.5,同樣無法滿足一個400字節內存空間的分配請求。
圖9.5 釋放容量為100字節內存塊
隨著系統中大量內存塊的分配和回收,內存碎片將越來越多,一些長時間不被釋放的內存塊,將始終分割著一些空閑內存塊,造成內存碎片。這也告知用戶,當不再使用某一內存塊時,應盡快釋放掉該內存塊,以避免造成過多的內存碎片。
在圖9.5中,有3個空閑塊,若用戶在此基礎上再請求一個280字節的內存空間,根據分配策略,堆管理器首先查看第一個空閑塊,其容量為100字節,無法滿足需求,接著查看第二個空閑塊,其容量為250字節,同樣無法滿足需求,接著查看第三個空閑塊,其容量為324字節,最終從該塊中分配一段空間給用戶,分配后的內存示意圖詳見圖9.6。
圖9.6 再分配280字節內存空間
在圖9.6的基礎上,若用戶再釋放200字節的內存塊,首先,將其變為空閑塊,示意圖詳見圖9.7。
圖9.7 釋放200字節的內存空間(1)——標記為空閑
由于其左側大小為250的內存塊同樣為空閑塊,因此,需要將它們合并為一個大的內存塊,即合并為一個大小為450的內存塊,示意圖詳見圖9.8。
圖9.8 釋放200字節的內存空間(2)——與相鄰空閑塊合并
此時,由于存在一個大小為450的空閑塊,因此,若此時用戶申請400字節的內存空間,則可以申請成功。與圖9.5對比可知,雖然圖9.5共計有674字節的空閑空間,而圖9.8只有594字節的空閑空間,但圖9.8卻可以滿足一個大小為400字節的內存空間請求。由此可見,受內存碎片的影響,總的空閑空間大小并不能決定一個內存請求的成功與否。
申請400字節成功后的示意圖詳見圖9.9。
圖9.9 再分配400字節內存空間
在圖9.9的基礎上,若用戶再釋放280字節的內存塊,同樣,首先將其變為空閑塊,示意圖詳見圖9.10。
圖9.10 釋放280字節的內存空間(1)——標記為空閑
由于其左右兩側均為空閑塊,因此,需要將它們合并為一個大的內存塊,即合并為一個大小為374的內存塊,示意圖詳見圖9.11。
圖9.11 釋放280字節的內存空間(2)——與相鄰空閑塊合并
之所以要將相鄰的空閑內存塊合并,主要是為了避免內存塊越來越多,越來越零散,如此下去,將很難再有一個大的空閑塊,用戶后續再申請大的內存空閑時,將無法滿足需求。
通過上面一系列內存分配和釋放的示例,展示了堆管理器分配和釋放內存的策略,使得用戶對相關的原理有了一定的了解。
實際上,在堆管理器的軟件實現中,為了便于管理,各個內存塊是以鏈表的形式組織起來的,每個內存塊的首部會固定存放一些用于內存塊管理的相關信息,示意圖詳見圖9.12。
圖9.12 內存塊(含空閑內存塊和已分配內存塊)鏈表
圖中展示了4個非常重要的信息:magic、used、p_next、p_prev。
magic被稱為魔數,會被賦值為一個特殊的固定值,它表示了該內存塊是堆管理器管理的內存塊,可以在一定程度上檢查錯誤的內存操作。例如,若這個區域被改寫,magic的值被修改為了其它值,表明存在非法的內存操作,可能是用戶內存操作越界等,應及時處理;在釋放一個內存塊時,堆管理器會檢查magic的值,若其值不為特殊的固定值,表明這并不是堆管理器分配的內存塊,該釋放操作是非法的。
used用于表示該內存塊是否已經被使用。若其值為0,則表示該內存塊是空閑塊;若其值為1,則表示該內存塊已經被分配,不是空閑塊。
p_next和p_prev用于將各個內存塊使用雙向鏈表的形式組織起來,以便可以方便的找到當前內存塊的下一個或上一個內存塊,例如,在釋放一個內存塊時,需要查看相鄰的內存塊(上一個內存塊和下一個內存塊)是否為空閑塊,以決定是否將它們合并為一個空閑塊。
此外,為了在分配內存塊時,加快搜索空閑塊的效率,信息中還會額外另外兩個指針,用于將所有空閑塊單獨組織為一個雙向鏈表。這樣,在分配一個內存塊時,只需要在空閑鏈表中查找滿足需求的空閑塊即可,無需依次遍歷所有的內存塊。示意圖詳見圖9.13。
圖9.13 空間塊鏈表
對于用戶來講,其獲得的內存空間為用戶數據空間(獲得的是直接指向用戶數據空間的指針),存放在內存塊首部的信息對用戶是不可見的,用戶不應該嘗試訪問、修改這些信息。
需要用戶注意的是,由于內存塊的相關信息需要占用一定的內存空間(在32位系統中,通常為24字節),因此,每個內存塊占用的實際大小會大于用戶請求的內存空間大小,例如,用戶請求一個100字節的內存空間,實際中,為了存儲內存塊的相關信息,會分配一個大小為124字節的內存塊。基于此,用戶實際可以使用的內存空間要略小于堆的總空間。
9.1.2 堆管理器接口
AWorks提供了堆管理器,用戶通過相關接口使用即可。相關函數的原型詳見表9.1。
表9.1 堆管理器接口(aw_memheap.h)
1. 定義堆管理器實例
在使用堆管理器前,必須先使用aw_memheap_t類型定義堆管理器實例,該類型在aw_memheap.h中定義,具體類型的定義用戶無需關心,僅需使用該類型定義堆管理器實例即可,即:
其地址即可作為初始化接口中memheap參數的實參傳遞。
在AWorks中,一個堆管理器用于管理一段連續的內存空間,一個系統中,可以使用多個堆管理器分別管理多段連續的內存空間。這就需要定義多個堆管理器實例,例如:
如此一來,各個應用可以定義自己的堆管理器,以管理該應用中的內存分配與釋放,使得各個應用之間的內存使用互不影響。
如果所有應用使用一個堆,那么各個應用之間的內存分配將相互影響,某一應用出現內存泄漏,隨著時間的推移,極有可能造成一個堆的空間被完全泄漏,這將影響所有應用程序。同時,所有應用共用一個堆,也造成在一個堆中的分配與釋放更加頻繁,分配的內存塊大小更加不一致,更容易產生內存碎片。
2. 初始化堆管理器
定義堆管理器實例后,必須使用該接口初始化后才能使用,以指定其管理的內存空間,其函數原型為:
其中,memheap為指向堆管理器實例的指針;name為堆管理器的名字,名字為一個字符串,僅用作標識;start_addr指定了其管理的內存空間首地址;size指定了其管理的內存空間大小(字節數)。
函數的返回值為標準的錯誤號,返回AW_OK時,表示初始化成功;否則,表示初始化失敗。
初始化函數的核心在于指定其管理的內存空間,通常情況下,這段連續的內存空間可以通過定義一個全局的數組變量獲得,其大小與實際應用相關,應根據實際情況決定。例如,使用堆管理器管理1KB的內存空間,則初始化堆管理器的范例程序詳見程序清單9.1。
程序清單9.1 初始化堆管理器范例程序
程序中,定義了一個大小為1024字節的數組,用于分配一個1KB的內存空間,以便使用堆管理器進行管理。
3. 分配內存
堆管理器初始化完畢后,可以使用該接口為用戶分配指定大小的內存塊,內存塊的大小可以是滿足資源需求的任意大小。分配內存塊的函數原型為:
其中,heap為指向堆管理器的指針;size為內存塊大小。返回值為分配內存塊的首地址,特別地,若返回值為NULL,則表明分配失敗。
在分配內存塊時,由于堆管理器并不知道分配的內存塊用來存儲何種類型的數據,因此,返回值為通用的無類型指針,即void *類型,代表其指向了某一類型不確定的地址。顯然,分配的內存塊用以存儲何種類型的數據是由用戶決定的,因此,用戶在使用這段內存空間時,必須將其轉換為實際數據類型的指針。
例如,分配用以存儲100個無符號8位數據的內存塊,其范例程序詳見程序清單9.2。
程序清單9.2 分配內存塊的范例程序
程序中,將aw_memheap_alloc()的返回值強制轉換為了指向uint8_t數據類型的指針。注意,使用aw_memheap_alloc()分配的內存中,數據的初始值是隨機的,不一定為0。因此,若不對ptr指向的內存賦值,則其值可能是任意的。
4. 重新調整內存大小
有時候,需要動態調整之前分配的內存塊大小,如果一開始分配了一個較小的內存塊,但隨著數據的增加,內存不夠,此時,就可以使用該函數重新調整之前分配的內存塊大小。其函數原型為:
其中,heap為指向堆管理器的指針;ptr為使用aw_memheap_alloc()函數分配的內存塊首地址,即調用aw_memheap_alloc()函數的返回值;new_size為調整后的內存塊大小。返回值為調整大小后的內存空間首地址,特別地,若返回值為NULL,則表明調整大小失敗。
newsize指定的新的大小可以比原內存塊大(擴大),也可以比原內存塊小(縮小)。如果是擴大內存塊,則新擴展部分的內存不會被初始化,值是隨機的,但原內存塊中的數據仍然保持不變。如果是縮小內存塊,則超出new_size部分的內存會被釋放,其中的數據被丟棄,其余數據保持不變。特別地,若newsize的值為0,則相當于不再使用ptr指向的內存塊,此時,將會直接釋放整個內存塊,這種情況下,返回值同樣為NULL。
函數返回值與ptr的值可能不同,即系統可能重新選擇一塊合適的內存塊來滿足調整大小的需求,此時,內存首地址將發生變化。例如,當新的大小比原空間大時,系統先判斷原內存塊后是否還有足夠的連續空間滿足本次擴大內存的要求,如果有,則直接擴大內存空間,內存首地址不變,同樣返回原內存塊的首地址,即ptr的值;如果沒有,則重新按照newsize分配一個內存塊,并將原有數據原封不動的拷貝到新分配的內存塊中,而后自動釋放原有的內存塊,原內存塊將不再可用,此時,返回值將是重新分配的內存塊首地址,與ptr將無直接關系。
例如,首先使用aw_memheap_alloc()分配了一個100字節大小的內存塊,然后重新將其調整為200字節大小的內存塊。范例程序詳見程序清單9.3。
程序清單9.3 重新調整內存大小
值得注意的是,重新調整內存空間的大小失敗后,原內存空間還是有效的。因此,若采用程序清單9.3第9行的調用形式,若內存擴大失敗,則返回值為NULL,這將會導致ptr的值被設置為NULL。此時,雖然原內存空間仍然有效,但由于指向該內存空間的指針信息丟失了,用戶無法再訪問到對應的內存空間,也無法釋放對應的內存空間,造成內存泄漏。
在實際使用時,為了避免調整內存大小失敗時將原ptr的值覆蓋為NULL,應該使用一個新的指針保存函數的返回值,詳見程序清單9.4。
程序清單9.4 重新調整內存大小(使用新的指針保存函數返回值)
此時,即使擴大內存空間失敗,指向原內存空間的指針ptr依然有效,依舊可以使用原先分配的內存空間,避免了內存泄漏。擴大內存空間成功時,則直接將ptr的值重新賦值為new_ptr,使其指向擴展完成后的內存空間。
5. 釋放內存
當用戶不再使用申請的內存塊時,必須釋放,否則將造成內存泄漏。釋放內存的函數原型為:
其中,ptr為使用aw_memheap_alloc()或aw_memheap_realloc()函數分配的內存塊首地址,即調用這些函數的返回值。注意,ptr只能是上述幾個函數的返回值,不能是其它地址值,例如,不能將數組首地址作為函數參數,以釋放靜態數組占用的內存空間。傳入錯誤的地址值,極有可能導致系統***。
當使用aw_memheap_free()將內存塊釋放后,相應的內存塊將變為無效,用戶不能再繼續使用。釋放內存塊的范例程序詳見程序清單9.5。
程序清單9.5 釋放內存塊的范例程序
為了避免釋放內存塊后再繼續使用,可以養成一個良好的習慣,每當內存釋放后,都將相應的ptr指針賦值為NULL。
9.1.3 系統堆管理
在AWorks中,整個內存空間首先用于滿足存放一些已知占用內存空間大小的數據,比如:全局變量、靜態變量、棧空間、程序代碼或常量等。
全局變量和靜態變量都比較好理解,其占用的內存大小通過數據的類型即可確定。需要注意的是,在介紹堆管理器時,定義一塊待管理的內存空間同樣使用了靜態數組的方式,詳見程序清單9.1的第6行:
這也屬于一種靜態變量,其占用的內存大小是已知的。
對于棧空間,在AWorks中,棧空間是靜態分配的,類似于一個靜態數組,其占用的內存空間大小由用戶決定,同樣是已知的。
通常情況下,程序代碼和常量都存儲在ROM或FLASH等只讀存儲器中,不會放在內存中。但在部分平臺中,出于效率或芯片架構的考慮,也可能將程序代碼和常量存儲在內存中。例如,在i.MX28x平臺中,程序代碼和常量也存儲在DDR內存中。程序代碼和常量占用的內存空間大小在編譯后即可確定,占用的內存空間大小也是已知的。
在滿足這些數據的存儲后,剩下的所有內存空間即作為系統堆空間,便于用戶在程序運行過程中動態使用。
為了便于用戶使用,需要使用某種合適的方法管理系統堆空間。在AWorks中,默認使用前文介紹的堆管理器對其進行管理。出于系統的可擴展性考慮,AWorks并沒有限制必須基于AWorks提供的堆管理器管理系統堆空間,如果用戶有更加適合特殊應用場合的管理方法,也可以在特定環境下使用自有方法管理系統堆空間。為了保持應用程序的統一,AWorks定義了一套動態內存管理通用接口,便于用戶使用系統堆空間,而無需關心具體的管理方法。相關函數原型詳見表9.2。
表9.2 動態內存管理接口(aw_mem.h)
通過前文的介紹可知,在使用堆管理器前,需要定義堆管理器實例,然后初始化該實例,以指定其管理的內存空間,初始化完成后,用戶才可向其請求內存。若是使用堆管理器管理系統堆空間(默認),那么,表9.2中的接口均可基于堆管理器接口實現。此時,系統中將定義一個默認的堆管理器實例,并在系統啟動時自動完成其初始化操作,指定其管理的內存空間為系統堆空間。如此一來,系統啟動后,用戶可以直接向系統中默認的堆管理器申請內存塊。例如,aw_mem_alloc()用于分配一個內存塊,其直接基于堆管理器實現,范例程序詳見程序清單9.6。
程序清單9.6 aw_mem_alloc()函數實現范例
其中,__g_system_heap為系統定義的堆管理器實例,已在系統啟動時完成了初始化。程序清單9.6只是aw_mem_alloc()函數實現的一個簡單范例,實際中,用戶可以根據具體情況,使用最為合適的方法管理系統堆空間,實現AWorks定義的動態內存管理通用接口。
下面,詳細介紹各個接口的含義及使用方法。
1. 分配內存
aw_mem_alloc()用于從系統堆中分配指定大小的內存塊,用法和C標準庫中的malloc()相同,其函數原型為:
參數size指定內存空間的大小,單位為字節。返回值為void *類型的指針,其指向分配內存塊的首地址,特別地,若返回值為NULL,則表示分配失敗。
例如,申請用以存儲一個int類型數據的存儲空間,范例程序詳見程序清單9.7。
程序清單9.7 申請內存范例程序
程序中,將aw_mem_alloc()的返回值強制轉換為了指向int數據類型的指針。注意,使用aw_mem_alloc()分配的內存中,數據的初始值是隨機的,不一定為0。因此,若不對ptr指向的內存賦值,則其值將是任意的。
2. 分配多個指定大小的內存塊
除使用aw_mem_alloc()直接分配一個指定大小的內存塊外,還可以使用aw_mem_calloc()分配多個連續的內存塊,用法與C標準庫中的calloc()相同,其函數原型為:
該函數用于分配nelem個大小為size的內存塊,分配的內存空間總大小為:nelem×size,實際上相當于分配一個容量為nelem×size的大內存塊,返回值同樣為分配內存塊的首地址。與aw_mem_alloc()不同的是,該函數分配的內存塊會被初始化為0。例如,分配用以存儲10個int類型數據的內存,則范例程序詳見程序清單9.8。
程序清單9.8 分配10個用于存儲int類型數據的內存塊
由于分配的內存空間會被初始化為0,因此,即使不對ptr指向的內存賦值,其值也是確定的0。
3. 分配具有一定對齊要求的內存塊
有時候,用戶申請的內存塊可能用來存儲具有特殊對齊要求的數據,要求分配內存塊的首地址必須按照指定的字節數對齊。此時,可以使用aw_mem_align()分配一個滿足指定對齊要求的內存塊。其函數原型為:
其中,size為分配的內存塊大小,align表示對齊要求,其值是2的整數次冪,比如:2、4、8、16、32、64等。返回值同樣為分配內存塊的首地址,其值滿足對齊要求,是align的整數倍。如align的值為16,則按照16字節對齊,分配內存塊的首地址將是16的整數倍,地址的低4位全為0。程序范例詳見程序清單9.9。
程序清單9.9 分配具有一定對齊要求的內存塊范例程序
程序中,將分配的地址通過aw_kprintf()打印輸出,以查看地址的具體值,實際運行可以發現,地址值是滿足16字節對齊的。注意,該函數與aw_mem_alloc()分配的內存塊一樣,其中的數據初始值是隨機的,不一定為0。
在堆管理器中,并沒有類似的分配滿足一定對齊要求的內存塊接口,只有普通的分配內存塊接口:aw_memheap_alloc()。其分配的內存塊,可能是對齊的,也可能是未對齊的。為了使返回給用戶的內存塊能夠滿足對齊要求,在使用aw_memheap_alloc()分配內存塊時,可以多分配align - 1字節的空間,此時,即使獲得的內存塊首地址不滿足對齊要求,也可以返回從內存塊首地址開始,順序第一個對齊的地址給用戶,以滿足用戶的對齊需求。
例如,要分配200字節的內存塊,并要求滿足8字節對齊,則首先使用aw_memheap_alloc()分配207字節(200 + 8 - 1)的內存塊,假定得到的內存塊地址范圍為:3 ~ 209,示意圖詳見圖9.14(a)。由于首地址3不是8的整數倍,因此其不是按8字節對齊的,此時,直接返回順序第一個對齊的地址給用戶,即:8。由于用戶需要的是200字節的內存塊,因此,對于用戶來講,其使用的內存塊地址范圍為 :8 ~ 207,顯然,其在實際使用aw_memheap_alloc()獲得的內存塊地址范圍內,用戶獲得的內存塊是完全有效的,示意圖詳見圖9.14(b)。
圖9.14 內存對齊的處理——多分配align-1字節的空間
為什么要多分配align - 1字節的空間呢?在獲得的實際內存塊不滿足對齊需求時,表明內存塊首地址不是align的整數倍,即其對align取余數的結果(C語言的%運算符)不為0,假定其對align取余數的結果為N(N ≥1),則只要將首地址加上align-N,得到的地址值就是align的整數倍。該值也為從首地址開始,順序第一個對齊的地址。由于在首地址不對齊時,必然有:N ≥1,因此:align - N ≤ align – 1,即順序第一個對齊的地址相對于起始地址的偏移不會超過align - 1。基于此,只要在分配內存塊時多分配align-1字節的空間,那么就可以在向用戶返回一個對齊地址的同時,依舊滿足用戶請求的內存塊容量需求。
若不多分配align-1字節的內存空間,例如,只使用aw_memheap_alloc()分配200字節的內存塊,得到的內存塊地址范圍為:3 ~ 203,示意圖詳見圖9.15(a)。此時,若同樣返回順序第一個對齊的地址給用戶,即:8。由于用戶需要的是200字節的內存塊,因此,對于用戶來講,其使用的內存塊地址范圍為 :8 ~ 208,顯然,204 ~ 208 這段空間并不是通過分配得到的有效內存,使得用戶得到了一段非法的內存空間,一旦訪問,極有可能導致應用出錯。示意圖詳見圖9.15 (b)。
圖9.15 內存對齊的處理——未多分配align-1字節的空間
實際中,由于align - 1的值往往是一些比較特殊的奇數值,例如:3、7、15、31等,經常如此分配容易把內存塊的首地址打亂,出現很多非對齊的地址。因此,往往會直接多分配align字節的內存空間。
同時,出于效率考慮,在AWorks中,每次分配的內存往往都按照默認的CPU自然對齊字節數對齊,例如,在32位系統中,默認分配的所有內存都按照4字節對齊,基于此,aw_mem_alloc()函數的實現可以更新為如程序清單9.10所示的程序。
程序清單9.10 aw_mem_alloc()函數分配的內存按照4字節對齊
4. 重新調整內存大小
有時候,需要動態調整分配內存塊的大小,如果一開始分配了一個較小的內存塊,但隨著數據的增加,內存不夠,此時,則可以使用該函數重新調整之前分配的內存塊大小。其函數原型為:
其中,ptr為使用aw_mem_alloc()、aw_mem_calloc()或aw_mem_align()函數分配的內存塊首地址,即調用這些函數的返回值。new_size為調整后的大小。返回值為調整大小后的內存塊首地址,特別地,若調整大小失敗,則返回值為NULL。
例如,首先使用aw_mem_alloc()分配了存儲1個int類型數據的內存塊,然后重新調整內存塊的大小,使其可以存儲2個int類型的數據。范例程序詳見程序清單9.11。
程序清單9.11 內存分配范例程序(重新調整內存大小)
5. 釋放內存
前面講解了4種分配內存塊的方法,無論使用何種方式動態分配的內存塊,在使用結束后,都必須釋放,否則將造成內存泄漏。釋放內存塊的函數原型為:
其中,ptr為使用aw_mem_alloc()、aw_mem_calloc()、aw_mem_align()或aw_mem_realloc()函數分配的內存塊首地址,即調用這些函數的返回值。
當使用aw_mem_free()將內存塊釋放后,相應的地址空間將變為無效,用戶不能再繼續使用。釋放內存塊的范例程序詳見程序清單9.12。
程序清單9.12 釋放內存塊的范例程序
9.2 內存池
堆管理器極為靈活,可以分配任意大小的內存塊,非常方便。但其也存在明顯的缺點:一是分配效率不高,在每次分配時,都要依次查找所有空閑內存塊,直到找到一個滿足需求的空閑內存塊;二是容易產生大小各異的內存碎片。
為了提高內存分配的效率,以及避免內存碎片,AWorks提供了另外一種內存管理方法:內存池(Memory Pool)。其舍棄了堆管理器中可以分配任意大小的內存塊這一優點,將每次分配的內存塊大小設定為固定值。
由于每次分配的內存塊大小為固定值,沒有空間大小的限制,因此,在用戶每次申請內存塊時,分配其第一個空閑內存塊即可,無需任何查找過程,同理,在釋放一個內存塊時,也僅需將其標志為空閑即可,無需任何額外的合并操作,這極大的提高了內存分配和釋放的效率。
同時,由于每次申請和釋放的內存塊都是同樣的大小,只要存在空閑塊,就可以分配成功,某幾個空閑內存塊不可能由于被某一已分配的內存塊分割而導致無法使用。這種情況下,任一空閑塊都可以被沒有限制的分配使用,不再存在任何內存碎片,徹底避免了內存碎片。
但是,將內存塊大小固定,會限制其使用的靈活性,并可能造成不必要的內存空間浪費,例如,用戶只需要很小的一塊內存空間,但若內存池中每一塊內存都很大,這就會使內存分配時不得不分配出一塊很大的內存,造成了內存浪費。這就要求在定義內存池時,應盡可能將內存池中內存塊的大小定義為一個合理的值,避免過多的內存浪費。
系統中可以存在多個內存池,每個內存池包含固定個數和大小的內存塊。基于此,在實際應用中,為了滿足不同大小的內存塊需求,可以定義多種尺寸(內存池中內存塊的大小)的內存池(比如:小、中、大三種),然后在實際應用中根據實際用量選擇從合適的內存池中分配內存塊,這樣可以在一定程度上減少內存的浪費。
9.2.1 內存池原理概述
內存池用于管理一段連續的內存空間,由于各個內存塊的大小固定,因此,首先將內存空間分為若干個大小相同的內存塊,例如,管理1024字節的內存空間,每塊大小為128字節。則共計可以分為8個內存塊。初始時,所有內存塊均為空閑塊,示意圖詳見圖9.16。
圖9.16 初始狀態——8個空閑塊
在AWorks中,為便于管理,將各個空閑內存塊使用單向鏈表的形式組織起來,示意圖詳見圖9.17。
圖9.17 以單向鏈表的形式組織各個空閑塊
這就要求一個空閑塊能夠存放一個p_next指針,以便組織鏈表。在32位系統中,指針的大小為4個字節,因此,要求各個空閑塊的大小不能低于4個字節。此外,出于對齊考慮,各個空閑塊的大小必須為自然對齊字節數的正整數倍。例如,在32位系統中,塊大小應該為4字節的整數倍,比如:4、8、12、6……而不能為5、7、9、13等。
基于此,當需要分配一個內存塊時,只需從鏈表中的取出第一個空閑塊即可。例如,需要在圖9.17的基礎上分配一個內存塊,可以直接從鏈表中取出第一個空閑塊,示意圖詳見圖9.18。
圖9.18 從鏈表中取出一個內存塊
此時,空閑塊鏈表中,將只剩下7個空閑塊,示意圖詳見圖9.19。
圖9.19 剩余7個空閑塊
值得注意的是,雖然在空閑塊鏈表中,各個內存塊中存放了一個p_next指針,占用了一定的內存空間,但是,當該內存塊從空閑鏈表中取出,分配給用戶使用時,已分配的內存塊并不需要組織為一個鏈表,p_next的值也就沒有任何意義了,因此,用戶可以使用內存塊中所有的內存,不存在用戶不可訪問的區域,不會造成額外的空間浪費。
而在堆管理器中,無論是空閑塊還是已分配的內存塊,頭部存儲的相關信息都必須保持有效,其占用的內存空間用戶是不能使用的,對于用戶來講,這相當于造成了一定的內存空間浪費。
當用戶不再使用一個內存塊時,需要釋放相應的內存塊,釋放時,直接將內存塊重新加入空閑塊鏈表即可。示意圖詳見圖9.20。
圖9.20 釋放一個內存塊
釋放后,空閑鏈表中將新增一個內存塊,示意圖詳見圖9.21。
圖9.21 釋放后,新增一個內存塊
由此可見,整個內存池的分配和釋放操作都非常簡單。分配時,從空閑鏈表中取出一個內存塊,釋放時,將內存塊重新加入空閑鏈表中。
9.2.2 內存池接口
AWorks提供了內存池軟件庫,用戶通過相關接口使用即可。相關函數的原型詳見表9.3。
表9.3 內存池接口(aw_pool.h)
1. 定義內存池實例
在使用內存池前,必須先使用aw_pool_t類型定義內存池實例,該類型在aw_pool.h中定義,具體類型的定義用戶無需關心,僅需使用該類型定義內存池實例即可,即:
其地址即可作為初始化接口中p_pool參數的實參傳遞。
一個內存池可以管理一段連續的內存空間,在AWorks中,可以使用多個內存池,以分別管理多段連續的內存空間。此時,就需要定義多個內存池實例,例如:
為了滿足各種大小的內存塊需求,可以定義多個具有不同內存塊大小的內存池。例如:定義小、中、大三種尺寸的內存池,它們對應的內存塊大小分別為8、64、128。用戶根據實際用量選擇從合適的內存池中分配內存塊,以在一定程度上減少內存的浪費。
2. 初始化內存池
定義內存池實例后,必須使用該接口初始化后才能使用,以指定內存池管理的內存空間,以及內存池中各個內存塊的大小。其函數原型為:
其中,p_pool指向待初始化的內存池,即使用aw_pool_t類型定義的內存池實例;p_pool_mem為該內存池管理的實際內存空間首地址;pool_size指定整個內存空間的大小;item_size指定內存池中每個內存塊的大小。
函數的返回值為內存池ID,其類型aw_pool_id_t,該類型的具體定義用戶無需關心,該ID可作為其它功能接口的參數,用以表示需要操作的內存池。特別地,若返回ID的值為NULL,表明初始化失敗。
初始化時,系統會將pool_size大小的內存空間,分為多個大小為item_size的內存塊進行管理。例如,使用內存池管理1KB的內存空間,每個內存塊的大小為16字節,初始化范例程序詳見程序清單9.13。
程序清單9.13 初始化內存池
程序中,將1024字節的空間分成了大小為16字節的內存塊進行管理。注意,出于效率考慮,塊大小并不能是任意值,只能為自然對齊字節數的正整數倍。例如,在32位系統中,塊大小應該為4字節的整數倍,若不滿足該條件,初始化時,將會自動向上修正為4字節的整數倍,例如,塊大小的值設置為5,將被自動修正為8。用戶可以通過aw_pool_item_size ()函數獲得實際的內存塊大小。
3. 獲取內存池中實際的塊大小
前面提到,初始化時,為了保證內存池的管理效率,可能會對用戶傳入的塊大小進行適當的修正,用戶可以通過該函數獲取當前內存池中實際的塊大小。其函數原型為:
其中,pool_id為初始化函數返回的內存池ID,其用于指定要獲取信息的內存池。返回值即為內存池中實際的塊大小。
例如,初始化時,將內存池的塊大小設定為5,然后通過該函數獲取內存池中實際的塊大小。范例程序詳見程序清單9.14。
程序清單9.14 獲取內存池中實際的塊大小
運行程序可以發現,實際內存塊的大小為8。
實際應用中,為了滿足不同容量內存申請的需求,可以定義多個內存池,每個內存池定義不同的塊大小。如定義3種塊大小尺寸的內存池,分別為8字節(小)、64字節(中)、128字節(大)。范例程序詳見程序清單9.15。
程序清單9.15 定義多種不同塊大小的內存池
程序中,將三種類型內存池的總容量分別定義為了512、1024、2048。實際中,應根據情況定義,例如,小型內存塊需求量很大,則應該增大對應內存池的總容量。
4. 獲取內存塊
內存池初始化完畢后,用戶可以從內存池中獲取固定大小內存塊,其函數原型為:
其中,pool_id為初始化函數返回的內存池ID,其用于指定內存池,表示從該內存池中獲取內存塊。返回值為void *類型的指針,其指向獲取內存塊的首地址,特別地,若返回值為NULL,則表明獲取失敗。從內存池中獲取一個內存塊的范例程序詳見程序清單9.16。
程序清單9.16 獲取內存塊范例程序
5. 釋放內存塊
當獲取的內存塊使用完畢后,應該釋放該內存塊,將其返還到內存池中。其函數原型為:
其中,pool_id為初始化函數返回的內存池ID,其用于指定內存池,表示將內存塊釋放到該內存池中。p_item為使用aw_pool_item_get()函數獲取內存塊的首地址,即調用aw_pool_item_get()函數的返回值,表示要釋放的內存塊。
返回值為aw_err_t類型的標準錯誤號,若值為AW_OK,表示釋放成功,否則,表示釋放失敗,釋放失敗往往是由于參數錯誤造成的,例如,釋放一個不是由aw_pool_item_get()函數獲取的內存塊。注意,內存塊從哪個內存池中獲取,釋放時,就必須釋放到相應的內存池中,不可將內存塊釋放到其它不對應的內存池中。
當使用aw_pool_item_return()將內存塊釋放后,相應的內存空間將變為無效,用戶不能再繼續使用。釋放內存塊的范例程序詳見程序清單9.17。
程序清單9.17 釋放內存塊范例程序
-
內存
+關注
關注
8文章
3111瀏覽量
75025 -
數據存儲
+關注
關注
5文章
997瀏覽量
51626 -
管理器
+關注
關注
0文章
252瀏覽量
18953
原文標題:AWorks軟件篇 — 內存管理
文章出處:【微信號:Zlgmcu7890,微信公眾號:周立功單片機】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
C++內存池的設計與實現
基于嵌入式裸機或RTOS系統下內存管理方法的探究
關于RT-Thread內存管理的內存池簡析
java線程內存模型

Java內存模型及原理分析

評論