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