vxWorks核心解讀五--記憶體管理
本篇博文,我們該談到Wind核心的記憶體管理模組了,嵌入式作業系統中, 記憶體的管理及分配佔據著極為重要的位置, 因為在嵌入式系統中, 儲存容量極為有限, 而且還受到體積、成本的限制, 更重要的是其對系統的效能、可靠性的要求極高, 所以深入剖析嵌入式作業系統的記憶體管理, 對其進行優化及有效管理, 具有十分重要的意義。在嵌入式系統開發中, 對記憶體的管理有很高的要求。概括地說, 它必須滿足以下三點要求:
- 實時性, 即在記憶體分配過程中要儘可能快地滿足要求。因此, 嵌入式作業系統中不可能採取通用作業系統中的一些複雜而完備的記憶體分配策略, 而是要採用簡單、快速的分配策略, 比如我們現在討論的VxWorks 作業系統中就採用了“ 首次適應”的分配策略。當然了具體的分配也因具體的實時性要求而各異。
- 可靠性, 即在記憶體分配過程中要儘可能地滿足記憶體需求。嵌入式系統應用千變萬化, 對系統的可靠性要求很高, 記憶體分配的請求必須得到滿足, 如果分配失敗, 則會帶來災難性的後果。
- 高效性, 即在記憶體分配過程中要儘可能地減少浪費。在嵌入式系統中, 對體積成本的要求較高, 所以在有限的記憶體空間內, 如何合理的配置及管理, 提高使用效率顯的尤為重要
實時嵌入式系統開發者通常需要根據系統的要求在RTOS提供的記憶體管理之上實現特定的記憶體管理,本章研究 VxWorks的Wind核心記憶體管理機制。
5.1 VxWorks記憶體管理概述
5.1.1 VxWorks記憶體佈局
VxWorks5.5 版本提供兩種虛擬記憶體支援(Virtual MemorySupport):基本級(Basic Level)和完整級(Full Level)
INCLUDE_MMU_BASIC:基本級虛存管理,無需VxVMI;
INCLUDE_MMU_FULL:完整級虛存管理,需元件VxVMI;
INCLUDE_PROTECT_TEXT:防寫text,需元件VxVMI;
INCLUDE_PROTECT_VEC_TABLE:防寫異常向量表,需元件VxVMI;
備註:VxVMI,即虛擬記憶體介面,是VxWorks的一個功能模組,它利用使用者片上或板上的記憶體管理單元(MMU),為使用者提供了對記憶體的高階管理功能。
在VxVMI的最小配置中,它防寫了幾個關鍵資源,其中包括VxWorks程式程式碼體、異常向量表、以及通過VxWorks裝載器下載的應用程式程式碼體。保護特性讓開發人員集中精力編寫自己的程式,無需擔心無意中修改關鍵程式碼段或引發耗時的系統錯誤。這在開發階段是很有用的,因為它簡化了對致命性錯誤的診斷。在產品的定型階段也是如此,因為它提高了系統可靠性。VxVMI提供的其它工具主要用於修改這些被保護的區域,如修改異常表或者插入斷點。
以上配置可在Tornado 開發環境中MMU 配置管理工具中改變,也可在BSP 的config.h 中完成。記憶體頁表的劃分和屬性配置,包括BSP 的config.h 中VM_PAGE_SIZE 和sysLib.h 中的sysPhysMemDesc 定義。本篇博文只考慮VxWorks基本級內部保護,即INCLUDE_MMU_BASIC配置模組。
VxWorks 的記憶體管理函式存在於2 個庫中 :memPartLib (緊湊的記憶體分割槽管理器) 和memLib (完整功能的記憶體分割槽管理器)。memPartLib 提供的工具用於從記憶體分割槽中分配記憶體塊。該庫包含兩類程式, 一類是通用工具memPartXXX(),包含建立和管理記憶體分割槽並從這些分割槽中分配和管理記憶體塊;另一類是標準的malloc/free記憶體分配介面。系統記憶體分割槽(其ID為memSysPartId 是一個全域性變數,關於它的定義在memLib.h 中)在核心初始化kernelInit() 是由usrRoot() (包含在usrConfig.c 中) 呼叫memInit 建立。其開始地址為RAM 中緊接著VxWorks 的BSS段之後的地址,大小為所有空閒記憶體。
VxWorks 5.5在目標板上有兩個記憶體池(Memmory Pool)用於動態記憶體分配:系統記憶體池(System Memory Pool)和WDB記憶體池。對於VxWorks上程式的設計者來說,需要關注的是系統記憶體池的動態記憶體管理機制。嵌入式系統的動態記憶體管理實際上就是需要儘量避免動態分配和釋放記憶體,以最大程度地保證系統的穩定性,VxWorks5.5的記憶體佈局如圖5.1所示。
5.1 VxWorks記憶體佈局
如上圖所示,VxWorks 5.5的記憶體按照裝載內容不同從記憶體的低地址開始依次分為低端記憶體區、VxWorks記憶體區、WDB記憶體池、系統記憶體池和使用者保留區五部分。各部分在記憶體中的位置由一些巨集引數來決定,記憶體區域的劃分及相關引數的定義與CPU的體系結構相關,這裡只介紹VxWorks 5.5的典型記憶體佈局。
整個目標板記憶體的起始地址是LOCAL_MEM_LOCAL_ADRS,大小是LOCAL_MEM_SIZE,sysPhysMemTop()一般返回記憶體的物理最高地址。各部分的內容及引數如下:
(1)低端記憶體區
在低端記憶體區中通常包含了中斷向量表、bootline(系統引導配置)和exception message(異常資訊)等資訊。
LOCAL_MEM_LOCAL_ADRS是低端記憶體區的起始地址,典型設定是0,但是在Pentium平臺一般設定位0x100000(即1M)。RAM_LOW_ADRS是低端記憶體區最高地址,也即vxWorks系統映像載入到記憶體中的地址,在Pentium平臺一般為0x308000。
(2)VxWorks區域
VxWorks區存放作業系統映像,其中依次是VxWorks image的程式碼段、資料段和BSS段。
由RAM_LOW_ADRS和FREE_MEM_ADRS決定該區的大小和位置。
在Pentium平臺RAM_LOW_ADRS為0x308000,FREE_MEM_ADRS通過連結指令碼中的全部變數end指定,一般緊隨VxWorks系統映像BSS段末尾。
(3)系統可用記憶體
系統可用記憶體主要是提供給VxWorks記憶體池用於動態記憶體的分配(如malloc())、任務的堆疊和控制塊及VxWorks執行時需要的記憶體。這部分記憶體有VxWorks管理,開銷位於目標板上。系統記憶體池在系統啟動時初始化,它的大小是整個記憶體減去其他區的大小。在啟動後可以通過函式memAddToPool()向系統記憶體池中增加記憶體,sysMemTop()返回系統記憶體池(System Memory Pool)的最高地址。
(4)WDB記憶體池
又叫Target Server記憶體池,是在目標板上為Tornado工具(如Wind Debugger)保留的一個記憶體池,主要用於動態下載目標模組、傳送引數等。這個記憶體池由Target Server管理,管理的開銷位於宿主機。Target Server記憶體池必要時(如Target Server記憶體池中記憶體不夠)可以從系統記憶體池中分配記憶體。
其起始地址是WDB_POOL_BASE,通常等於FREE_RAM_ADRS,初始大小由WDB_POOL_SIZE定義,預設值是系統記憶體池的1/16,在installDir/target/config/all/configAll.h中定義:
#define WDB_POOL_SIZE((sysMemTop()-FREE_RAM_ADRS)/16)
系統中不一定包含Target Server記憶體池.只有當系統配置中包含元件INCLUDE_WDB時才需要Target Server記憶體池。
(5)使用者保留區
使用者保留區是使用者為特定應用程式保留的記憶體。該區的大小由USER_RESERVED_MEM決定,預設值為0。以上所涉及的大部分巨集定義和函式在目標板的BSP或configAll.h中定義。
5.1.2 VxWorks記憶體分配策略
嵌入式系統中為任務分配記憶體空間有兩種方式,靜態分配和動態分配。靜態分配為系統提供了最好的可靠性與實時性,如可以通過修改USER_RESERVED_MEM 分配記憶體給應用程式的特殊請求用。對於那些對實時性和可靠性要求極高的需求,只能採用靜態分配方式。但採用靜態分配必然會使系統失去靈活性,因此必須在設計階段考慮所有可能的情況,並對所有的需求做出相應的空間分配,一旦出現沒有考慮到的情況,系統就無法處理。此外靜態分配方式也必然導致很大的浪費,因為必須按照最壞情況進行最大的配置,而在實際執行中可能只用到其中的一小部分。因此一般系統中只有1 個記憶體分割槽,即系統分割槽,所有任務所需要的記憶體直接呼叫malloc()從其中分配。分配採用First-Fit演算法(空閒記憶體塊按地址大小遞增排列,對於要求分配的分割槽容量size,從頭開始比較,直至找到滿足大小≥size 的塊為止,並從連結串列相應塊中分配出相應size 大小的塊指標),通過free釋放的記憶體將被聚合以形成更大的空閒塊。這就是VxWorks的動態記憶體分配機理。但是使用動態記憶體分配malloc/free時要注意到以下幾個方面的限制:
- 因為系統記憶體分割槽是一種臨界資源,由訊號量保護,使用malloc 會導致當前呼叫掛起,所以它不能用於中斷服務程式;
- 因為進行記憶體分配需要執行查詢演算法,其執行時間與系統當前的記憶體使用情況相關,是不確定的,所以對於有規定時限的操作它是不適宜的;
- 採用簡單的最先匹配演算法,容易導致系統中存在大量的記憶體碎片,降低記憶體使用效率和系統性能。
一般在系統設計時採用靜態分配與動態分配相結合的方法。也就是說,系統中的一部分任務有嚴格的時限要求,而另一部分只是要求完成得越快越好。按照RMS(RateMonotonic Scheduling)理論,所有硬實時任務總的CPU 時間應小於70%,這樣的系統必須採用搶先式任務排程;而在這樣的系統中,就可以採用動態記憶體分配來滿足那一部分可靠性和實時性要求不那麼高的任務。
VxWorks採用最先適應法來動態分配記憶體,
優點:
- 滿足嵌入式系統對實時性的要求;
- 儘可能的利用低地址空間,從而保證高地址空間有較大的空閒來放置要求記憶體較多的任務;
缺點:
VxWorks沒有清除碎片的功能,只在記憶體釋放時,採用了上下空閒區融合的方法,即把相鄰的空閒記憶體塊融合成一個空閒塊。
5.1.3 VxWorks對虛擬記憶體的支援
VxWorks 5.5提供兩級虛擬記憶體支援(Virtual Memory Support):基本級(Basic Level)和完整級(Full Level)。後者需要購買可選元件VxVMI。在VxWorks 5.5中有關虛擬記憶體的配置包括兩個部分。第一部分是對vxWorks虛擬內支援級別和保護的配置,下表列出了這些選項。
表5.1 VxWorks虛擬記憶體配置常量
以上配置一般在BSP的config.h中完成。
第二部分是記憶體頁表的劃分和屬性配置。配置包括BSP的config.h中的VM_PAGE_SIZE和sysLib.c中的sysPhysMemDesc。
VM_PAGE_SIZE定義了CPU預設的頁的大小。需參照CPU手冊的MMU部分定義該值。
sysPhysMemDesc用於初始化MMU的TLB表,它是以PHYS_MEM_DESC為元素的常量陣列。PHYS_MEM_DESC在vmLib.h中定義,用於部分記憶體的虛擬地址到實體地址的對映:
typedef struct phes_mem_desc
{
void virtualAddr ; /*虛擬記憶體的實體地址*/
void *physicalAddr ; /*虛擬地址*/
UNIT len ; /*這部分的大小*/
UNIT initialStateMask ;
UNIT initialState ; /*設定這部分記憶體的初始狀態*/
}PHYS_MEM_DESC;
sysPhysMemDesc中的值需要根據系統的實際配置進行修改。sysPhysMemDesc中定義的記憶體地址必須頁對齊,且必須跨越完整的頁。也就是說PHYS_MEM_DESC結構中的前三個域的值必須能被VM_PAGE_SIZE整除,否則會導致VxWorks初始化失敗。
基本級虛擬記憶體庫vmBaseLib提供了系統中所需最低限度的MMU支援,其主要目的是為建立Cache-safe緩衝區提供支援。
5.2 VxWorks記憶體分配演算法
5.2.1 VxWorks核心資料結構
VxWorks系統初始化時建立系統記憶體分割槽,VxWorks定義了全域性變數memSysPartition來管理系統記憶體分,使用者也可以使用memPartLib庫的函式實現自己的分割槽。用於分割槽中記憶體管理的資料結構是在memPartLib.h中定義的mem_part,包含物件標記、保護該分割槽的訊號量、一些分配統計信心(如當前分配的塊數)及分割槽中所有的空閒記憶體快形成的一個雙向連結串列freeList(在VxWorks6.8中,空閒記憶體塊採用平衡二叉樹來組織)。
具體定義如下:
typedef struct mem_part
{
OBJ_CORE objCore; /* 物件標識 */
DL_LIST freeList; /* 空閒連結串列 */
SEMAPHORE sem; /* 分割槽訊號量,保護該分割槽互斥訪問 */
unsigned totalWords; /* 分割槽中的字數(一個字=兩個位元組) */
unsigned minBlockWords; /* 以字為單位的最小塊數,包含頭結構 */
unsigned options; /* 選項,用於除錯和統計*/
/*分配統計資訊*/
unsigned curBlocksAllocated; /*當前分配的塊數 */
unsigned curWordsAllocated; /* 當前分配的字數 */
unsigned cumBlocksAllocated; /* 累積分配的塊數 */
unsigned cumWordsAllocated; /* 累積分配的字數*/
} PARTITION;
備註:需要注意的是記憶體分割槽訊號量sem是分割槽描述符中的一個成員,而不是指向動態建立的訊號量結構的指標,採用靜態分配的方式,是從提高系統性能的角度考慮。
VxWorks在初始化的過程中,通過usrInit()->usrKernelInit()->kernelInit()將系統分配給vxWorks記憶體池的基地址MEM_POOL_START和大小,通過usrRoot()的引數傳遞給memInit()函式。換句話說:vxWorks系統分割槽memSysPartition的初始化由usrRoot()->memInit()來完成,而其中記憶體的佈局則通過usrInit()->usrKernelInit()->KernelInit()傳遞的引數來確定,
usrKernelInit()傳遞給kernelInit()的引數如下:
kernelInit ((FUNCPTR) usrRoot, ROOT_STACK_SIZE, MEM_POOL_START,
sysMemTop (), ISR_STACK_SIZE, INT_LOCK_LEVEL);
其中ROOT_STACK_SIZE為10000=0x2710,
MEM_POOL_START為end,這裡我們假定為0x3c33a0
sysMemTop ()返回的是最大可用記憶體,這裡假定為32M,即sysMemTop ()返回值為0x200 0000。
ISR_STACK_SIZE值為1000=0x3e8
INT_LOCK_LEVEL值為0
我們假設系統可用記憶體為32M,VxWorks的入口地址為0x30800c,end的值,即VxWorks核心映像BSS段末尾地址;
sysMemTop ()返回的值為0x2000000,作為VxWorks記憶體池的最高地址pMemPoolEnd。VxWorks在記憶體中的佈局如圖5.2所示。
圖5.2 VxWorks內部佈局
由於我們不配置WDB模組,故沒有標出WDB記憶體池的分配。
我們仍以Pentium平臺為例,這裡假設VxWorks核心映像載入到記憶體的地址RAW_LOW_ADRS為0x30 8000,LOCAL_MEM_LOCAL_ADDR定位為0x10 0000,即1M位元組位置。
Pentium平臺的全域性描述符表放在的0x10 0000+0x1000=1M+4K位置處的80位元組記憶體範圍。
中斷描述符表放置在0x10 0000開始的2K位元組記憶體範圍。
這樣中斷棧的棧底vxIntStackEnd=end=0x3c33a0,
中斷棧的棧基址為vxIntStackBase= end+ISR_STACK_SIZE=0x3c33a0+0x3e8=0x3c3788
將要分配給系統分割槽記憶體基地址pMemPoolStart=vxIntStackBase=0x3c3788
用於初始任務tRootTask的記憶體起始地址為:
pRootMemStart =pMemPoolEnd-ROOT_STACK_SIZE
=pMemPoolEnd-10000
=0x2000000-0x2710
=0x1ff d8f0
用於初始任務tRootTask的任務棧大小為:
rootStackSize = rootMemNBytes - WIND_TCB_SIZE - MEM_TOT_BLOCK_SIZE
=10000 – 416 –( (2 * MEM_BLOCK_HDR_SIZE) + MEM_FREE_BLOCK_SIZE)
=10000 – 416 – 32=9532=0x253c
用於tRootTask任務棧的棧頂:
pRootStackBase= pRootMemStart + rootStackSize + MEM_BASE_BLOCK_SIZE
=pRootMemStart+rootStackSize+(MEM_BLOCK_HDR_SIZE+MEM_FREE_BLOCK_SIZE)
=0x1ff d8f0+0x253c+0x8+0x20
= 0x1ff fe54
這樣32M的記憶體就被分成了三個部分:
0x10 0000到0x3c33a0 用於存放vxWorks核心映像的程式碼段、資料段、BSS段;
0x3c33a0到0x3c3788 用作vxWorks核心的中斷棧
0x3c3788(即end+1000) 到0x1ff d8f0(即0x200 0000-0x2710)用於VxWorks的記憶體池;
0x1FFFE54-0x253c(即十進位制數9532)到0x1FFFE54用作tRootTask任務棧的棧底;
這樣的話,用於建立初始任務tRootTask的TCB控制塊構架介面如下:
taskInit (pTcb, "tRootTask", 0, VX_UNBREAKABLE | VX_DEALLOC_STACK,
pRootStackBase, (int) rootStackSize, (FUNCPTR) rootRtn,
(int) pMemPoolStart, (int)memPoolSize, 0, 0, 0, 0, 0, 0, 0, 0);
這裡的pMemPoolStart就是圖中vxIntStackBase,即end+1000=0x3c 3788
記憶體池的範圍從vxIntStackBase到pRootMemStart的記憶體空間,
即0x1ff d8f0-0x3c 3788=1C3 A168,約為28.2M,這一段區域作為初始任務tRootTask執行程式碼usrRoot()的入口引數傳遞給memInit ()函式用於建立初始分割槽。
memInit()函式的最重要作用是建立記憶體分割槽類memPartClass和記憶體分割槽類的一個全域性的系統物件memSysPartition,並用指定的記憶體塊(pMemPoolStart, pMemPoolStart + memPoolSize)來初始化系統記憶體物件memSysPartition的分割槽空閒連結串列。
這樣得到的VxWorks記憶體分割槽邏輯圖如圖5.3所示。
圖5.3 VxWorks記憶體分割槽邏輯圖
6.2.2 VxWorks核心分割槽初始化
usrInit()->usrKernelInit()->kernelInit()構建並啟動初始化任務tRootTask,執行usrRoot()
usrRoot()-> memInit (pMemPoolStart, memPoolSize),我們從memInit()開始分析。
還是使用上面的示例:
pMemPoolStart =0x3c3788(即end+1000) 到0x1ff d8f0(即0x200 0000-0x2710)用於VxWorks的初始化記憶體分割槽。
memPoolSize=0x1ff d8f0-0x3c3788=0x1C3 A168,約為28.2M,示意圖如圖5.2所示。
memInit()程式碼如下:
STATUS memInit ( char *pPool, unsigned poolSize)
{
memLibInit (); /*初始化化的記憶體管理函式 */
//初始化記憶體物件
//初始化系統記憶體分割槽
//將分配的(pMemPoolStart, memPoolSize)約28.2M加入系統記憶體分割槽
return (memPartLibInit (pPool, poolSize));
}
6.2.2.1 memLibInit ()分析
STATUS memLibInit (void)
{
if (!memLibInstalled)
{
_func_valloc = (FUNCPTR) valloc;
_func_memalign = (FUNCPTR) memalign;
memPartBlockErrorRtn = (FUNCPTR) memPartBlockError;
memPartAllocErrorRtn = (FUNCPTR) memPartAllocError;
memPartSemInitRtn = (FUNCPTR) memSemInit;
memPartOptionsDefault = MEM_ALLOC_ERROR_LOG_FLAG |
MEM_BLOCK_ERROR_LOG_FLAG |
MEM_BLOCK_ERROR_SUSPEND_FLAG |
MEM_BLOCK_CHECK;
memLibInstalled = TRUE;
}
return ((memLibInstalled) ? OK : ERROR);
}
分析:初始化VxWorks提供的更高層的記憶體管理介面,例如:
_func_valloc初始化為虛擬地址按頁邊界對齊的從系統分割槽分配記憶體的函式valloc;
_func_memalign初始化為安裝自定義對齊方式從系統分割槽分配記憶體的函式memalign;
memPartSemInitRtn用於指定初始化系統分割槽memSysPartition的訊號量sem的型別,這裡使用具有優先順序佇列、安全刪除和防止優先順序翻轉的互斥訊號量的初始化函式memSemInit;
另外兩個介面定義出錯處理方式。
6.2.2.2 memPartLibInit (pPool, poolSize)分析
memPartLibInit()初始化了VxWorks的記憶體物件類和記憶體物件類的一個例項系統分割槽,並將配置的28.2M的記憶體塊加入到系統分割槽當中。
STATUS memPartLibInit (char *pPool, unsigned poolSize )
{
if ((!memPartLibInstalled) &&
(classInit (memPartClassId, sizeof (PARTITION),
OFFSET (PARTITION, objCore), (FUNCPTR) memPartCreate,
(FUNCPTR) memPartInit, (FUNCPTR) memPartDestroy) == OK))
{
memPartInit (&memSysPartition, pPool, poolSize);
memPartLibInstalled = TRUE;
}
return ((memPartLibInstalled) ? OK : ERROR);
}
分析:
我們在第1章概述中提到,vxWorks採用類和物件的思想將wind核心的任務管理模組、記憶體管理模組、訊息佇列管理模組、訊號量管理模組、以及看門狗管理模組組織起來,各個物件類都指向元類classClass,每個物件類只負責管理各自的物件,如圖5.4。
圖5.4 Wind核心物件組織關係圖
本篇所分析的記憶體分割槽memSysPartition就是記憶體分割槽物件類memPartClass的一個例項,即系統分割槽,其紅色部分的組織形式如圖5.3所示。
classInit (memPartClassId, sizeof (PARTITION),
OFFSET (PARTITION, objCore), (FUNCPTR) memPartCreate,
(FUNCPTR) memPartInit, (FUNCPTR) memPartDestroy)初始化圖中的記憶體物件類memPartClass,其程式碼如下:
STATUS classInit
(
OBJ_CLASS *pObjClass, /* pointer to object class to initialize */
unsigned objectSize, /* size of object */
int coreOffset, /* offset from objCore to object start */
FUNCPTR createRtn, /* object creation routine */
FUNCPTR initRtn, /* object initialization routine */
FUNCPTR destroyRtn /* object destroy routine */
)
{
/* default memory partition is system partition */
pObjClass->objPartId = memSysPartId; /* partition to allocate from */
pObjClass->objSize = objectSize; /* record object size */
pObjClass->objAllocCnt = 0; /* initially no objects */
pObjClass->objFreeCnt = 0; /* initially no objects */
pObjClass->objInitCnt = 0; /* initially no objects */
pObjClass->objTerminateCnt = 0; /* initially no objects */
pObjClass->coreOffset = coreOffset; /* set offset from core */
/* initialize object methods */
pObjClass->createRtn = createRtn; /* object creation routine */
pObjClass->initRtn = initRtn; /* object init routine */
pObjClass->destroyRtn = destroyRtn; /* object destroy routine */
pObjClass->showRtn = NULL; /* object show routine */
pObjClass->instRtn = NULL; /* object inst routine */
/* 初始化記憶體物件類memPartClass為合法的物件類 */
//記憶體物件類memPartClass指向其wind核心的元類classClass
objCoreInit (&pObjClass->objCore, classClassId);
return (OK);
}
接著由memPartInit (&memSysPartition, pPool, poolSize)初始化記憶體佇列類memPartClass的例項物件memSysPartition,並將配置的記憶體池分配給系統分割槽memSysPartition。
6.2.2.3 memPartInit (&memSysPartition, pPool, poolSize)分析
memPartInit()將初始化系統分割槽memSysPartition,並將配置的記憶體池分配給系統分割槽memSysPartition,是程式碼實現如下:
void memPartInit ( FAST PART_ID partId, char *pPool, unsigned poolSize )
{
/*初始化分割槽描述符 */
bfill ((char *) partId, sizeof (*partId), 0);
partId->options = memPartOptionsDefault;
partId->minBlockWords = sizeof (FREE_BLOCK) >> 1;
/* initialize partition semaphore with a virtual function so semaphore
* type is selectable. By default memPartLibInit() will utilize binary
* semaphores while memInit() will utilize mutual exclusion semaphores
* with the options stored in _mutexOptionsMemLib.
*/
//通過呼叫一個函式指標memPartSemInitRtn,來初始化分割槽描述符的訊號量,採用這種方
//式的好處是訊號量型別的選擇是可選的。預設情況下
//memPartLib庫中將其初始化二進位制訊號量,但是memInit()將會使用存放在
//_mutexOptionsMemLib中的屬性值來初始化互斥訊號量。
(* memPartSemInitRtn) (partId);
dllInit (&partId->freeList); /*初始化空閒連結串列 */
objCoreInit (&partId->objCore, memPartClassId); /* initialize core */
(void) memPartAddToPool (partId, pPool, poolSize);
}
分析:
系統分割槽memSysPartition的屬性初始化為:
memPartOptionsDefault = MEM_ALLOC_ERROR_LOG_FLAG |
MEM_BLOCK_ERROR_LOG_FLAG |
MEM_BLOCK_ERROR_SUSPEND_FLAG |
MEM_BLOCK_CHECK;
memSysPartition的分割槽做小大小為sizeof (FREE_BLOCK)個位元組,在Pentium平臺為16個位元組。
6.2.2.4 將初始記憶體塊加入系統分割槽記憶體池中
VxWorks呼叫函式memPartAddToPool (&memSysPartition, pPool, poolSize)來完成這一功能,我們來分析這一函式的具體實現過程:
我們要加入的記憶體塊區域是從0x3c3788(即end+1000) 到0x1ff d8f0(即0x200 0000-0x2710)用於vxWorks的初始化記憶體分割槽,大小為0x1ff d8f0-0x3c3788=0x1C3A168=29598056位元組
從0x3c3788開始的8個位元組作為塊的開頭,其初始化程式碼如下:
pHdrStart = (BLOCK_HDR *) pPool;// 0x3c3788
pHdrStart->pPrevHdr = NULL;
pHdrStart->free = FALSE;
pHdrStart->nWords = sizeof (BLOCK_HDR) >> 1;//nWorks=8>>1=4
從0x3c3790開始的8個位元組,其初始化程式碼如下:
pHdrMid = NEXT_HDR (pHdrStart);// 0x3c3788
pHdrMid->pPrevHdr = pHdrStart;
pHdrMid->free = TRUE;
pHdrMid->nWords = (poolSize - 2 * sizeof (BLOCK_HDR)) >> 1;
//(29598056-2*8)/2=14799020=0xE1D0AC
由於pHdrMid->free = TRUE,所以0x3c3790出的值為0x80e1d0ac。
由於通過BLOCK_HDR的結構體中,nWords和free共用了4個位元組的長度,nWords佔用了低0~30bit位,free佔用了第31bit位。
typedef struct blockHdr /* BLOCK_HDR */
{
struct blockHdr * pPrevHdr; /* pointer to previous block hdr */
unsigned nWords : 31; /* size in words of this block */
unsigned free : 1; /* TRUE = this block is free */
} BLOCK_HDR;
所以其在記憶體中的佈局如圖5.5。
圖5.5 記憶體塊頭在記憶體中的佈局
以上是記憶體塊的頭部的設定,我們在看下尾部的設定,對要加入的記憶體塊區域是從0x3c3788(即end+1000) 到0x1ff d8f0(即0x200 0000-0x2710)用於vxWorks的初始化記憶體分割槽,最後的8個位元組表示的塊頭的初始化結果。
特別需要注意的塊頭地址0x1ffd8e8,是從初始化記憶體分割槽尾端分出8個位元組,其初始化程式碼如下:
pHdrEnd = NEXT_HDR (pHdrMid);//指向的是0x373C90
pHdrEnd->pPrevHdr = pHdrMid;
pHdrEnd->free = FALSE;
pHdrEnd->nWords = sizeof (BLOCK_HDR) >> 1;
其初始化的結構,正如圖5.6的記憶體區域所示。
圖5.6 記憶體塊尾部結構佈局
從上面的初始化過程,我們看到vxWorks把記憶體塊區域是從0x3c3788(即end+1000) 到0x1ff d8f0(即0x200 0000-0x2710)用於vxWorks的初始化記憶體分割槽加入的分割槽描述符memSysPartition.freeList中去時,真正加入的區域從送0x3c3788+8到0x1ff d8f0-8這個區域,即從這個初始分割槽的開頭和末尾處分別劃出了一個8位元組的區域填寫兩個記憶體塊的頭結構BLK_HDR。以表示與初始化分割槽相鄰的兩個記憶體塊,只不過這兩個記憶體塊只有頭部,資料部分為0。這樣做的目的是為了將來在釋放0x3c3788+8開始的記憶體塊時,可以0x3c3788位置的空白塊合併;以及在釋放以0x1ff d8f0-8結束的記憶體塊時可以和0x1ff d8f0位置的空白記憶體塊合併。以使得vxWorks分割槽記憶體管理模組對0x3c3788+8開始的記憶體塊和0x1ff d8f0-8結束的記憶體塊才操作進行統一。此時sysMemPartition的 佈局如圖5.7所示。
圖5.7 sysMemPartition的 內部佈局
通過上面的描述,memPartAddToPool (&memSysPartition, pPool, poolSize)的具體實現如下:
STATUS memPartAddToPool ( PART_ID partId, char *pPool, unsigned poolSize )
{
FAST BLOCK_HDR *pHdrStart;
FAST BLOCK_HDR *pHdrMid;
FAST BLOCK_HDR *pHdrEnd;
char * tmp;
int reducePool; /*記憶體池的實際減少量*/
if (OBJ_VERIFY (partId, memPartClassId) != OK)//驗證partId合法性
return (ERROR);
/*確保記憶體池的實際開始地址是4位元組對齊(假設是Pentium平臺) */
tmp = (char *) MEM_ROUND_UP (pPool); /* 獲取實際的其實地址 */
reducePool = tmp - pPool;
if (poolSize >= reducePool) /* 調整記憶體池的長度*/
poolSize -= reducePool;
else
poolSize = 0;
pPool = tmp;//調整記憶體池的開始位置
/*
*確保記憶體池大小poolSize是4位元組的整數倍,並且至少包含3個空閒記憶體塊的塊頭和
*1個空閒記憶體塊(僅有包含一個塊頭)
*/
poolSize = MEM_ROUND_DOWN (poolSize);
if (poolSize < ((sizeof (BLOCK_HDR) * 3) + (partId->minBlockWords * 2)))
{
errno = S_memLib_INVALID_NBYTES;
return (ERROR);
}
/* 初始化化3個記憶體塊頭 -
* 記憶體塊的開頭和結束各有一個塊頭,並且還有一個程式碼真正的記憶體塊 */
pHdrStart = (BLOCK_HDR *) pPool;
pHdrStart->pPrevHdr = NULL;
pHdrStart->free = FALSE;
pHdrStart->nWords = sizeof (BLOCK_HDR) >> 1;
pHdrMid = NEXT_HDR (pHdrStart);
pHdrMid->pPrevHdr = pHdrStart;
pHdrMid->free = TRUE;
pHdrMid->nWords = (poolSize - 2 * sizeof (BLOCK_HDR)) >> 1;
//中間的記憶體塊頭程式碼真正的記憶體塊,其大小應除去開頭和結尾兩個記憶體塊頭結構佔用
//的記憶體
pHdrEnd = NEXT_HDR (pHdrMid);
pHdrEnd->pPrevHdr = pHdrMid;
pHdrEnd->free = FALSE;
pHdrEnd->nWords = sizeof (BLOCK_HDR) >> 1;
semTake (&partId->sem, WAIT_FOREVER);
//從中我們可以看出sysMemPartition中的訊號量是保證空閒塊連結串列必須被互斥訪問
dllInsert (&partId->freeList, (DL_NODE *) NULL, HDR_TO_NODE (pHdrMid));
partId->totalWords += (poolSize >> 1);
semGive (&partId->sem);
return (OK);
}
分析:這裡有必要分析一下將記憶體塊插入系統分割槽sysMemPartition的空閒連結串列freeList的操作:
dllInsert (&partId->freeList, (DL_NODE *) NULL, HDR_TO_NODE (pHdrMid));
記憶體池的第二個記憶體塊塊(pHdrMid指向)插入freeList,這裡存在一個強制型別轉換過程。
typedef struct blockHdr /* 記憶體塊頭BLOCK_HDR */
{
struct blockHdr * pPrevHdr;/* pointer to previous block hdr */
unsigned nWords : 31; /* size in words of this block */
unsigned free : 1; /* TRUE = this block is free */
} BLOCK_HDR;
空閒記憶體塊頭結構是在BLOCK_HDR的基礎上,加上DL_NODE型別的成員變數node。
typedef struct /* 空閒記憶體塊FREE_BLOCK */
{
struct
{
struct blockHdr * pPrevHdr; /* pointer to previous block hdr */
unsigned nWords : 31;/* size in words of this block */
unsigned free : 1; /* TRUE = this block is free */
} hdr;
DL_NODE node; /* freelist links */
} FREE_BLOCK;
因此其加入sysMemPartition.freeList的過程如下:
dllInsert (&partId->freeList, (DL_NODE *) NULL, HDR_TO_NODE (pHdrMid));
#define HDR_TO_NODE(pHdr) (& ((FREE_BLOCK *) pHdr)->node),其組合示意如圖5.8所示。
圖5.8 空閒塊頭示意圖
5.2.3 VxWorks記憶體分配機制
VxWorks中的動態記憶體分配採用最先匹配(First-Fit)演算法,即從空閒連結串列中查詢記憶體塊,然後從高地址開始查詢,當找到第一個滿足分配請求的空閒記憶體塊時,就分配所需記憶體並修改該空閒塊的大小。空閒塊的剩餘部分仍然保留在空閒連結串列中。當從大的記憶體塊中分配出新的記憶體塊時,需要將新記憶體塊頭部的一塊空間(稱為“塊頭”)用來儲存分配、回收和合並等操作的資訊,因此,實際佔用的記憶體大小是分配請求的記憶體大小與塊頭開銷之和。
6.2.3.1 malloc()分配記憶體機制
分配函式malloc()返回的地址是分配請求所獲可用記憶體的起始地址,為優化效能,malloc()會自動對齊邊界,分配的記憶體的起始地址和大小都是邊界之的整數倍,因此有可能造成內部碎片。塊頭的開銷與處理器的體系結構有關,對於X86體系結構來說,,塊頭的開銷是8位元組,通常這部分會被自動以4位元組邊界對齊,以減少出現碎片的可能並優化效能。
void *malloc (size_t nBytes )
{
return (memPartAlloc (&memSysPartition, (unsigned) nBytes));
}
malloc()是memPartAlloc()的進一步封裝,並且從系統記憶體分割槽memSysPartition分配記憶體。
memPartAlloc()函式如下:
void *memPartAlloc (PART_ID partId, unsigned nBytes)
{
return (memPartAlignedAlloc (partId, nBytes, memDefaultAlignment));
}
memPartAlloc()又是memPartAlignedAlloc()的進一步封裝,其中memDefaultAlignment在Pentium平臺是4位元組。
memPartAlignedAlloc()從指定的系統分割槽memSysPartition中分配nBytes位元組的記憶體,分配的位元組數必須要被memDefaultAlignment整除,其中memDefaultAlignment必須為2的冪數。
首次適應演算法就體現在函式memPartAlignedAlloc()的實現上:
void *memPartAlignedAlloc (PART_ID partId, unsigned int nBytes, unsigned int alignment)
{
FAST unsigned nWords;
FAST unsigned nWordsExtra;
FAST DL_NODE * pNode;
FAST BLOCK_HDR * pHdr;
BLOCK_HDR * pNewHdr;
BLOCK_HDR origpHdr;
if (OBJ_VERIFY (partId, memPartClassId) != OK)
return (NULL);
/* 實際分配的記憶體大小為請求分配記憶體大小+塊頭大小 */
nWords = (MEM_ROUND_UP (nBytes) + sizeof (BLOCK_HDR)) >> 1;
/*檢查是否溢位,如果溢位,設定errno,並返回NULL*/
if ((nWords << 1) < nBytes)
{
if (memPartAllocErrorRtn != NULL)
(* memPartAllocErrorRtn) (partId, nBytes);
errnoSet (S_memLib_NOT_ENOUGH_MEMORY);
//如果分割槽設定了MEM_ALLOC_ERROR_SUSPEND_FLAG,則當前請求記憶體的任務被阻塞
if (partId->options & MEM_ALLOC_ERROR_SUSPEND_FLAG)
{
if ((taskIdCurrent->options & VX_UNBREAKABLE) == 0)
taskSuspend (0); /* 阻塞自己*/
}
return (NULL);
}
//如果當前請求分配的記憶體小於分割槽允許的最小記憶體(Pentium平臺,8個位元組),則以最小內
//存為準
if (nWords < partId->minBlockWords)
nWords = partId->minBlockWords;
/* 獲取分割槽訊號量,互斥範圍分割槽空閒塊連結串列,在這裡如果訊號量已經被佔用,則當
*前請求分配記憶體的任務將會被阻塞,等待該訊號量釋放,這也是malloc()會導致被呼叫*任務阻塞的根源*/
semTake (&partId->sem, WAIT_FOREVER);
/* 首次適應演算法,就體現在下面的連結串列查詢中 */
pNode = DLL_FIRST (&partId->freeList);
/* 我們需分配一個空閒的塊,並帶有額外的空間用於對齊,最壞的情況是我們需要一*個對齊的額外位元組數
*/
nWordsExtra = nWords + alignment / 2;
FOREVER
{
while (pNode != NULL)
{
//如果當前塊大於包含額外對齊位元組空間的請求位元組數,或者
//當前塊和請求的位元組數剛好一致,並且滿足對齊條件
//則當前塊滿足要求
if ((NODE_TO_HDR (pNode)->nWords > nWordsExtra) ||
((NODE_TO_HDR (pNode)->nWords == nWords) &&
(ALIGNED (HDR_TO_BLOCK(NODE_TO_HDR(pNode)), alignment))))
break;
pNode = DLL_NEXT (pNode);
}
//如果找不到滿足要求的當前塊
if (pNode == NULL)
{
semGive (&partId->sem);
if (memPartAllocErrorRtn != NULL)
(* memPartAllocErrorRtn) (partId, nBytes);
errnoSet (S_memLib_NOT_ENOUGH_MEMORY);
//默契情況下memLibInit()中設定memSysPartition.options如下表所示
// memPartOptionsDefault = MEM_ALLOC_ERROR_LOG_FLAG |
// MEM_BLOCK_ERROR_LOG_FLAG |
// MEM_BLOCK_ERROR_SUSPEND_FLAG |
// MEM_BLOCK_CHECK
if (partId->options & MEM_ALLOC_ERROR_SUSPEND_FLAG)
{
if ((taskIdCurrent->options & VX_UNBREAKABLE) == 0)
taskSuspend (0); /* suspend ourselves */
}
return (NULL);
}
//找到滿足的記憶體塊
pHdr = NODE_TO_HDR (pNode);
origpHdr = *pHdr;
//從當前塊中分配出使用者指定的記憶體,需要注意的是vxWorks從空閒塊的末端開始分配捏成
//這樣的好處是,餘下的部分可以仍然在空閒連結串列中。如果memAlignedBlockSplit()返回為
//NULL,意味著將會使第一個記憶體塊餘下的部分調小,而不能放在空閒連結串列中,
//這種情況下,vxWorks會繼續嘗試使用下一個記憶體塊
pNewHdr =
memAlignedBlockSplit (partId, pHdr, nWords, partId->minBlockWords,
alignment);
if (pNewHdr != NULL)
{//找到並分配處使用者指定的記憶體塊,退出迴圈
pHdr = pNewHdr; /* give split off block */
break;
}
//當前塊不合適的話,繼續嘗試
pNode = DLL_NEXT (pNode);
}
/*標記分配出來的空閒塊為非空閒 */
pHdr->free = FALSE;
/* 更新memSysPartition分割槽的統計量 */
partId->curBlocksAllocated++;
partId->cumBlocksAllocated++;
partId->curWordsAllocated += pHdr->nWords;
partId->cumWordsAllocated += pHdr->nWords;
//是否訊號量
semGive (&partId->sem);
//跳過塊頭,返回可用記憶體的地址
return ((void *) HDR_TO_BLOCK (pHdr));
}
這裡比較複雜的函式是,當memPartAlignedAlloc()找到合適的塊後,從該空閒塊分割出使用者所需要記憶體所考慮的情況,下面我們會分析這個分割函式。
6.2.3.2 記憶體塊的分割
分配記憶體時,首先找到一個合適的空閒記憶體塊,然後從該塊上分割出一個新塊(包含頭部)。最後把該新塊的記憶體區域返回給使用者,因此頭部對使用者的透明的。
分配時,有時為了對齊,會將一個塊一分為三,第一塊是申請後剩餘空間構成的塊,第二塊是申請塊的目標塊,第三塊是用第二塊為了對齊而剩餘的記憶體空間構成的塊。如果第三塊太小,則不將其獨立出來,而是作為第二塊的一部分。
示意圖如5.9所示。
圖5.9 記憶體塊分割示意圖
memAlignedBlockSplit()函式是空閒記憶體塊分割的核心函式,設從A塊申請nWords位元組的記憶體大小,要求alignment位元組對齊,其程式碼分析如下:
(1)將從A塊的尾部endOfBlock往上偏移nWords-BLOCK_HDR位元組處pNewBlock作為新塊的頭部地址。
(2)pNewBlock往上偏移,以使之alignment位元組對齊。這一步導致申請塊的後面出現了剩餘空間,正如上圖中所示。
(3)計算塊A的剩餘空間blockSize=(char *) pNewHdr - (char *) pHdr,若blockSize小於最小塊大小minWords,且新塊的起始地址pNewHdr等於塊A的起始地址pHdr,那麼新快就從塊A的起始地址開始,即將塊A分配出去,否則申請失敗。若blockSize大於最小塊minWords,則更新塊A的大小,即塊A仍然存在只是其記憶體空間變小了。
(4)計算新塊尾部剩餘多小空間endOfBlock - (UINT) pNewHdr - nWords,若剩餘的空間小於最小塊大小minWords,則將該剩餘空間也分配給申請的塊,否則為該剩餘空間構建一個新塊,並插入記憶體分割槽的空閒連結串列。
(5)返回pNewHdr。
memAlignedBlockSplit()實現如下:
LOCAL BLOCK_HDR *memAlignedBlockSplit
(
PART_ID partId,
BLOCK_HDR *pHdr,
unsigned nWords, /* 需要分割出來的字數,包含塊頭 */
unsigned minWords, /* 所允許的最小字數 */
unsigned alignment /* 邊界對齊數 */
)
{
FAST BLOCK_HDR *pNewHdr;
FAST BLOCK_HDR *pNextHdr;
FAST char *endOfBlock;
FAST char *pNewBlock;
int blockSize;
/*計算出當前塊的末尾位置 */
endOfBlock = (char *) pHdr + (pHdr->nWords * 2);
/* 計算出新塊的起始位置 */
//通過memPartAlignedAlloc()呼叫函式的分析,我們指定nWords中起始已經包含了
//塊頭的位置,所以這裡在分配記憶體是隻考慮實際使用的記憶體。
pNewBlock = (char *) ((unsigned) endOfBlock -
((nWords - sizeof (BLOCK_HDR) / 2) * 2));
/* 通過邊界向內對齊調整記憶體塊起始位置,這將使得分配的記憶體偏大 */
pNewBlock = (char *)((unsigned) pNewBlock & ~(alignment - 1));
//將確定的記憶體塊起始位置假設塊頭大小,這裡才考慮進了塊頭的大小
pNewHdr = BLOCK_TO_HDR (pNewBlock);
/* 分割之後剩下的塊的大小 */
blockSize = ((char *) pNewHdr - (char *) pHdr) / 2;
if (blockSize < minWords)
{
//如果分割之後剩下的記憶體塊,小於分割槽規定的最小記憶體,並且切換分割出去的記憶體塊
//恰好就是原來的記憶體塊,則將原來的記憶體塊從空閒塊連結串列中刪除
//否則,分割之後剩餘的記憶體太小,不足以繼續掛載空閒塊連結串列上,則函式返回NULL;
// memPartAlignedAlloc()將會嘗試這從下一個空閒塊中繼續分割
if (pNewHdr == pHdr)
dllRemove (&partId->freeList, HDR_TO_NODE (pHdr));
else
return (NULL);
}
else
{
pNewHdr->pPrevHdr = pHdr;
pHdr->nWords = blockSize;
}
//檢查由於新塊地址對齊導致的多出來的記憶體碎片,是否足夠大
//足夠足夠大,則單獨作為一個空閒塊插入空閒塊連結串列;
//否則併入新分割出來的塊中。
if (((UINT) endOfBlock - (UINT) pNewHdr - (nWords * 2)) < (minWords * 2))
{
/* 將產生的碎片全部併入新分割出來的塊中 */
pNewHdr->nWords = (endOfBlock - pNewBlock + sizeof (BLOCK_HDR)) / 2;
pNewHdr->free = TRUE;
/*調整後面的空閒塊,使其指向新分割出來的塊*/
NEXT_HDR (pNewHdr)->pPrevHdr = pNewHdr;
}
else
{
/* the extra byte