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