安景新,趙昶宇
VxWorks系統(tǒng)中內(nèi)存池的應(yīng)用
安景新1,趙昶宇2
(1.海軍駐天津八三五七所軍事代表室,天津 300308;2.天津津航計(jì)算技術(shù)研究所,天津 300308)
針對(duì)嵌入式軟件內(nèi)存管理具有快速性、可靠性和高效性的特點(diǎn),分析了VxWorks內(nèi)存管理機(jī)制,闡述了內(nèi)存池結(jié)構(gòu)和工作原理,深入剖析了內(nèi)存池的分配和釋放機(jī)制,并對(duì)VxWorks系統(tǒng)下的內(nèi)存管理提出了建議。
嵌入式系統(tǒng);VxWorks;內(nèi)存管理;內(nèi)存池
利用默認(rèn)的內(nèi)存管理函數(shù)new/delete或malloc/free在堆上分配和釋放內(nèi)存會(huì)有一些額外的開銷。如果應(yīng)用程序頻繁地在堆上分配和釋放內(nèi)存,則會(huì)導(dǎo)致性能的降低,并且會(huì)使系統(tǒng)中出現(xiàn)大量的內(nèi)存碎片,降低內(nèi)存的利用率。
經(jīng)典的內(nèi)存池(MemPool)技術(shù)是一種用于分配大量大小相同的小對(duì)象的技術(shù)。該技術(shù)可以極大地加快內(nèi)存分配/釋放過程。如果合理地規(guī)劃內(nèi)存池的大小以及數(shù)量,可以減少動(dòng)態(tài)分配、釋放內(nèi)存時(shí)的消耗,并且可以有效減少內(nèi)存碎片,避免內(nèi)存泄漏。
本文以VxWorks操作系統(tǒng)為例,重點(diǎn)分析了內(nèi)存池管理機(jī)制,并對(duì)內(nèi)存分配和釋放給出了解決方案。
VxWorks采用用戶程序、內(nèi)核處于同一個(gè)地址空間的內(nèi)存管理策略,軟件開發(fā)人員在開發(fā)程序時(shí)必須保證不侵犯其他程序和內(nèi)核的地址空間,以免破壞系統(tǒng)的正常工作,或?qū)е缕渌绦虍惓_\(yùn)行。內(nèi)核負(fù)責(zé)為程序分配內(nèi)存、動(dòng)態(tài)分配內(nèi)存和回收內(nèi)存。VxWorks為用戶提供2種內(nèi)存區(qū)域,即內(nèi)存域region和內(nèi)存分區(qū)partition.region是可變長(zhǎng)的內(nèi)存區(qū),可以從創(chuàng)建的region中再分配段segment,region的特點(diǎn)是容易產(chǎn)生碎片,但靈活、不浪費(fèi);partition是定長(zhǎng)的內(nèi)存區(qū),用戶可以從創(chuàng)建的partition中分配內(nèi)存塊,或在某個(gè)內(nèi)存分區(qū)中再創(chuàng)建一個(gè)內(nèi)存分區(qū)。
partition的特點(diǎn)是無碎片、效率高,但存在浪費(fèi)。通常情況下,VxWorks內(nèi)核和應(yīng)用程序?qū)?nèi)存的操作是基于內(nèi)存分區(qū)進(jìn)行的。內(nèi)存池是一塊連續(xù)的內(nèi)存區(qū)域,包含1個(gè)或多個(gè)內(nèi)存塊。內(nèi)存分區(qū)包含分區(qū)自身的描述信息(1個(gè)結(jié)構(gòu)體)和1個(gè)或多個(gè)內(nèi)存池,描述信息保存在系統(tǒng)內(nèi)存分區(qū)中,內(nèi)存池是該分區(qū)實(shí)際擁有的內(nèi)存空間。內(nèi)存分區(qū)剛創(chuàng)建完畢時(shí),只有1個(gè)內(nèi)存池,以后用戶程序可往該分區(qū)中添加內(nèi)存池。內(nèi)存池之間的地址不一定連續(xù),VxWorks在啟動(dòng)過程中會(huì)創(chuàng)建一個(gè)包含系統(tǒng)內(nèi)存池的系統(tǒng)內(nèi)存分區(qū),如圖1所示。
圖1 VxWorks的內(nèi)存布局
應(yīng)用程序可以通過系統(tǒng)的內(nèi)存分配調(diào)用預(yù)先一次性申請(qǐng)適當(dāng)大小的內(nèi)存作為1個(gè)內(nèi)存池,之后應(yīng)用程序?qū)?nèi)存的分配和釋放則可以通過內(nèi)存池來完成。內(nèi)存池技術(shù)申請(qǐng)內(nèi)存/釋放內(nèi)存均極其快,其內(nèi)存分配過程多數(shù)情況下復(fù)雜度為O(1),內(nèi)存釋放過程復(fù)雜度為O(1)。主要開銷在生成新的空閑內(nèi)存單元。
內(nèi)存池的結(jié)構(gòu)如圖2所示。
定義內(nèi)存池控制塊T_MemoryPool數(shù)據(jù)結(jié)構(gòu)如下:
Typedef struct MemoryPool{
MemoryBlock* pBlock; /* 第一個(gè)block 的指針 */
Unsigned short nUnitSize; /* 每個(gè)小內(nèi)存塊的字節(jié)數(shù) */
Unsigned short nInitSize; /* 初始的Block 的內(nèi)存塊數(shù)目 */
Unsigned short nGrowSize; /* 增加的Block 的內(nèi)存塊數(shù)目 */
}T_MemoryPool;
定義內(nèi)存塊T_MemoryBlock數(shù)據(jù)結(jié)構(gòu)如下:
Typedef struct MemoryBlock{
Unsigned short nSize; /* 內(nèi)存塊的大小 */
Unsigned short nFree; /* 空閑塊數(shù) */
Unsigned short nFirst; /* 第一個(gè)空閑塊 */
MemoryBlock* pNext; /* 下一個(gè)Block */
char aData[1]; /* 數(shù)據(jù)的初始位置 */
}T_MemoryBlock;
圖2 內(nèi)存池結(jié)構(gòu)
在運(yùn)行過程中,MemoryPool內(nèi)存池可能會(huì)有多個(gè)用來滿足內(nèi)存申請(qǐng)請(qǐng)求的內(nèi)存塊,這些內(nèi)存塊是從進(jìn)程堆中開辟的一個(gè)較大的連續(xù)內(nèi)存區(qū)域,它由一個(gè)MemoryBlock結(jié)構(gòu)體和多個(gè)可供分配的空閑內(nèi)存單元組成,所有內(nèi)存塊組成了一個(gè)內(nèi)存塊鏈表,MemoryPool的pBlock是這個(gè)鏈表的頭。對(duì)每個(gè)內(nèi)存塊,都可以通過其頭部的MemoryBlock結(jié)構(gòu)體的pNext成員訪問緊跟在其后面的內(nèi)存塊。
每個(gè)內(nèi)存塊由2部分組成,即1個(gè)MemoryBlock結(jié)構(gòu)體和多個(gè)內(nèi)存分配單元。這些內(nèi)存分配單元大小固定(由MemoryPool的nUnitSize表示),MemoryBlock結(jié)構(gòu)體有2個(gè)成員比較重要,即nFree和nFirst.
nFree記錄這個(gè)內(nèi)存塊中還有多少個(gè)空閑單元,而nFirst則記錄下一個(gè)空閑單元的編號(hào)。每一個(gè)空閑單元的前兩個(gè)字節(jié)記錄了緊跟它之后的下一個(gè)空閑單元的編號(hào)。在此情況下,通過利用每個(gè)空閑單元的前兩個(gè)字節(jié),一個(gè)MemoryBlock中的所有空閑單元被連接起來。
當(dāng)有新的內(nèi)存請(qǐng)求到來時(shí),MemoryPool會(huì)通過pBlock遍歷MemoryBlock鏈表,直到找到某個(gè)MemoryBlock所在的內(nèi)存塊,及其中的空閑單元(通過檢測(cè)MemoryBlock結(jié)構(gòu)體的nFree 成員是否大于0)。如果找到這樣的內(nèi)存塊,取得其MemoryBlock的nFirst值(此為該內(nèi)存塊中第一個(gè)空閑單元的編號(hào)),則根據(jù)這個(gè)編號(hào)定位到該空閑單元的起始位置(所有單元大小固定,因此,每個(gè)單元的起始位置都可以通過編號(hào)分配單元大小來偏移定位),這個(gè)位置就是用來滿足此次內(nèi)存申請(qǐng)請(qǐng)求的內(nèi)存起始地址。但在返回這個(gè)地址前,需要先將該位置開始的前兩個(gè)字節(jié)的值(這兩個(gè)字節(jié)值記錄其之后的下一個(gè)空閑單元的編號(hào))賦給本內(nèi)存塊的MemoryBlock的nFirst成員。在此情況下,下一次的請(qǐng)求就可被這個(gè)編號(hào)對(duì)應(yīng)的內(nèi)存單元來滿足,同時(shí),將此內(nèi)存塊的MemoryBlock的nFree遞減1,然后將定位到的內(nèi)存單元的起始位置作為此次內(nèi)存請(qǐng)求的返回地址返回給調(diào)用者。
如果從現(xiàn)有的內(nèi)存塊中找不到一個(gè)自由的內(nèi)存分配單元(當(dāng)?shù)谝淮握?qǐng)求內(nèi)存以及現(xiàn)有的所有內(nèi)存塊中的內(nèi)存單元都已經(jīng)被分配時(shí),會(huì)發(fā)生這種情況),MemoryPool就會(huì)從進(jìn)程堆中申請(qǐng)1個(gè)內(nèi)存塊。
當(dāng)某個(gè)被分配的單元因free操作需要回收時(shí),該單元并不會(huì)返回給進(jìn)程堆,而是返回給MemoryPool。返回時(shí),MemoryPool能獲取該單元的起始地址。此時(shí),MemoryPool開始遍歷其所維護(hù)的內(nèi)存塊鏈表,判斷該單元的起始地址是否落在某個(gè)內(nèi)存塊的地址范圍內(nèi)。如果不在所有內(nèi)存地址的范圍內(nèi),則此被回收的單元不屬于這個(gè)MemoryPool,將整個(gè)內(nèi)存塊返回給內(nèi)存堆;如果在某個(gè)內(nèi)存塊的地址范圍內(nèi),則它會(huì)將剛剛回收的分配單元加到這個(gè)內(nèi)存塊的MemoryBlock所維護(hù)的空閑單元鏈表頭部。
進(jìn)行內(nèi)存分配前,先判斷內(nèi)存池當(dāng)前內(nèi)存塊鏈表是否為空。如果為空,則意味著這是第一次內(nèi)存申請(qǐng)請(qǐng)求。此時(shí),從進(jìn)程堆中申請(qǐng)一個(gè)分配單元個(gè)數(shù)為nInitSize的內(nèi)存塊,并初始化該內(nèi)存塊。初始化的操作包括設(shè)置MemoryBlock的nSize為所有內(nèi)存分配單元的大小、nFree為-1、nFirst為1,并將編號(hào)為0的分配單元之后的所有空閑單元連接起來,即從aData位置開始,每隔nUnitSize大小取其最前面的兩個(gè)字節(jié),并記錄之后空閑單元的編號(hào)。
如果該內(nèi)存塊申請(qǐng)成功,并初始化完畢,返回第一個(gè)分配單元給調(diào)用函數(shù)。第一個(gè)分配單元以MemoryBlock結(jié)構(gòu)體內(nèi)的最后一個(gè)字節(jié)為起始地址。
當(dāng)內(nèi)存池中已有內(nèi)存塊時(shí),遍歷該內(nèi)存塊鏈表,尋找有空閑單元的內(nèi)存塊。如果找到有空閑單元的內(nèi)存塊,則“定位”該內(nèi)存塊為可用的空閑單元處,“定位”以MemoryBlock結(jié)構(gòu)體內(nèi)的最后一個(gè)字節(jié)位置aData為起始位置,以MemoryPool的nUnitSize為步長(zhǎng)來進(jìn)行;修改MemoryBlock nFree信息,以及修改此內(nèi)存塊的空閑單元鏈表信息。在找到的內(nèi)存塊中,nFirst為該內(nèi)存塊中自由存儲(chǔ)單元鏈表的表頭,其下一個(gè)空閑單元的編號(hào)存放在nFirst指示的空閑單元的前兩個(gè)字節(jié)。通過定位得到的位置,取其前兩個(gè)字節(jié)的值賦給nFirst,這就是此內(nèi)存塊空閑單元鏈表的新表頭,即下一次分配的空閑單元的編號(hào)。
如果沒有找到有空閑單元的內(nèi)存塊,則需要重新向進(jìn)程堆申請(qǐng)一個(gè)內(nèi)存塊。此時(shí),由于不是第一次申請(qǐng)內(nèi)存塊,所以,申請(qǐng)的內(nèi)存塊包含的分配單元個(gè)數(shù)為nGrowSize,而不再是nInitSize。初始化這個(gè)新申請(qǐng)的內(nèi)存塊,并將此內(nèi)存塊插入MemoryPool的內(nèi)存塊鏈表的頭部,再將此內(nèi)存塊的第一個(gè)分配單元返回給調(diào)用函數(shù)。將此新內(nèi)存塊插入內(nèi)存塊鏈表頭部的原因是該內(nèi)存塊還有很多可供分配的空閑單元,放在頭部可以使下次接收到內(nèi)存申請(qǐng)時(shí),減少對(duì)內(nèi)存塊的遍歷時(shí)間。
遍歷內(nèi)存池的內(nèi)存塊鏈表,確定該待回收分配單元(pFree)落在哪一個(gè)內(nèi)存塊的指針范圍內(nèi),可通過比較指針值來確定。找到的包含pFree所指向的待回收分配單元的內(nèi)存塊后,修改該內(nèi)存塊的空閑單元鏈表信息,將這個(gè)待回收分配單元的前兩個(gè)字節(jié)的值指向該內(nèi)存塊原先的第一個(gè)可分配的空閑單元的編號(hào),并將nFirst值改變?yōu)橹赶蜻@個(gè)待回收分配單元的編號(hào),該操作將待回收分配單元放在了空閑單元鏈表的頭部。需要注意的是,該單元的內(nèi)存地址并沒有改變,改變的只是其狀態(tài)(已分配/空閑),以及當(dāng)其處于空閑狀態(tài)時(shí)在空閑單元鏈表中的位置。
回收后,考慮到資源的有效利用及后續(xù)操作的性能,內(nèi)存池的操作會(huì)繼續(xù)判斷:如果此內(nèi)存塊所有分配單元都是空閑的,則這個(gè)內(nèi)存塊就會(huì)從MemoryPool中被移出,并作為一個(gè)整體返回給進(jìn)程堆;如果該內(nèi)存塊中還有非空閑單元,則不將此內(nèi)存塊返回給進(jìn)程堆。但是因?yàn)閯倓傆幸粋€(gè)分配單元返回給了這個(gè)內(nèi)存塊,即這個(gè)內(nèi)存塊有空閑單元可供下次分配,則它會(huì)被移到MemoryPool維護(hù)的內(nèi)存塊的頭部。在此情況下,內(nèi)存請(qǐng)求到來,MemoryPool遍歷其內(nèi)存塊鏈表尋找空閑單元時(shí),第一次尋找就會(huì)找到這個(gè)內(nèi)存塊。因?yàn)檫@個(gè)內(nèi)存塊確實(shí)有空閑單元,這樣可以減少M(fèi)emoryPool的遍歷次數(shù)。
在使用內(nèi)存池機(jī)制管理內(nèi)存時(shí),需要注意以下幾點(diǎn):①判斷內(nèi)存塊的所有單元是否都處于空閑狀態(tài)時(shí),不用遍歷其所有單元,只需要判斷nFree乘以nUnitSize是否等于nSize、nSize內(nèi)存塊中所有分配單元的大?。ú话^部MemoryBlock結(jié)構(gòu)體的大小),這樣可快速檢查某個(gè)內(nèi)存塊中所有分配單元是否全部處于空閑狀態(tài)。②不能通過比較nFree與nInitSize或nGrowSize的大小來判斷某個(gè)內(nèi)存塊中所有分配單元都為空閑狀態(tài),這是因?yàn)榈谝淮畏峙涞膬?nèi)存塊(分配單元個(gè)數(shù)為nInitSize)可能被移到鏈表的后面,甚至有可能在移動(dòng)到鏈表后面之后,因?yàn)槟硞€(gè)時(shí)間其所有單元都處于空閑狀態(tài)而被整個(gè)返回給進(jìn)程堆,即在回收分配單元時(shí),無法判定某個(gè)內(nèi)存塊中的分配單元個(gè)數(shù)是nInitSize還是nGrowSize,也就無法通過比較nFree與nInitSize或nGrowSize的大小,來判斷內(nèi)存塊的所有分配單元是否都為空閑狀態(tài)。③在進(jìn)行內(nèi)存釋放操作后,雖然pFree被“回收”,但是pFree仍然指向當(dāng)前已“回收”的單元,這個(gè)單元在回收過程中,其前兩個(gè)字節(jié)被覆寫,但其他部分的內(nèi)容并沒有改變。且從整個(gè)進(jìn)程的內(nèi)存使用角度看,該“回收”的單元的狀態(tài)仍然是“有效的”。因?yàn)檫@里的“回收”只是回收給了內(nèi)存池,而并沒有回收給進(jìn)程堆,因此,程序仍然可以通過pFree訪問此單元。但這是一個(gè)很危險(xiǎn)的操作,因?yàn)樵搯卧诨厥者^程中的前兩個(gè)字節(jié)已被覆寫,且該單元可能很快會(huì)被內(nèi)存池重新分配。因此,回收后通過pFree指針對(duì)這個(gè)單元的訪問都是錯(cuò)誤的,讀操作會(huì)讀到錯(cuò)誤的數(shù)據(jù),寫操作則可能會(huì)破壞程序中的其他數(shù)據(jù),需要注意此問題。
內(nèi)存的申請(qǐng)和釋放對(duì)一個(gè)應(yīng)用程序的整體性能影響極大,甚至在很多時(shí)候會(huì)成為某個(gè)應(yīng)用程序的瓶頸。消除內(nèi)存申請(qǐng)和釋放瓶頸的方法往往是針對(duì)內(nèi)存使用的實(shí)際情況,提供合適的內(nèi)存池。通過性能測(cè)試表明,內(nèi)存池具有malloc/free進(jìn)行內(nèi)存申請(qǐng)/釋放的方式快、不會(huì)產(chǎn)生或很少產(chǎn)生堆碎片、可避免內(nèi)存泄漏等明顯特性。以上所描述的基于內(nèi)存池的內(nèi)存管理方法具有很大的實(shí)用價(jià)值,希望能給嵌入式軟件工作者啟發(fā),從而設(shè)計(jì)出更好的內(nèi)存管理方法。
[1]顧勝元,楊丹,黃海倫.嵌入式實(shí)時(shí)動(dòng)態(tài)內(nèi)存管理機(jī)制研究與應(yīng)用[J].重慶工學(xué)院學(xué)報(bào)(自然科學(xué)),2009(01): 117-121.
[2]許健,于鴻洋.一種Linux多線程應(yīng)用下內(nèi)存池的設(shè)計(jì)與實(shí)現(xiàn)[J].電子技術(shù)應(yīng)用,2012,38(11):146-149.
[3]何煦嵐,何曉嵐.基于多鏈表結(jié)構(gòu)的嵌入式系統(tǒng)內(nèi)存管理[J].計(jì)算機(jī)應(yīng)用與軟件,2008,25(04):58-60.
〔編輯:張思楠〕
2095-6835(2018)19-0145-03
TP316.2
A
10.15913/j.cnki.kjycx.2018.19.145
安景新(1990—),男,工學(xué)碩士,助理工程師,從事裝備質(zhì)量監(jiān)督與檢驗(yàn)驗(yàn)收方面的工作與研究。趙昶宇(1982—),男,陜西漢中人,工學(xué)碩士,高級(jí)工程師,主要從事嵌入式系統(tǒng)軟件測(cè)試方面的研究。