感謝前輩分享,附上鍊接:http://www.prtos.org/vxworks-memory-management/
本篇博文,我們該談到Wind內核的內存管理模塊了,嵌入式操作系統中, 內存的管理及分配佔據着極爲重要的位置, 因爲在嵌入式系統中, 存儲容量極爲有限, 而且還受到體積、成本的限制, 更重要的是其對系統的性能、可靠性的要求極高, 所以深入剖析嵌入式操作系統的內存管理, 對其進行優化及有效管理, 具有十分重要的意義。在嵌入式系統開發中, 對內存的管理有很高的要求。概括地說, 它必須滿足以下三點要求:
- 實時性, 即在內存分配過程中要儘可能快地滿足要求。因此, 嵌入式操作系統中不可能採取通用操作系統中的一些複雜而完備的內存分配策略, 而是要採用簡單、快速的分配策略, 比如我們現在討論的VxWorks 操作系統中就採用了“ 首次適應”的分配策略。當然了具體的分配也因具體的實時性要求而各異。
- 可靠性, 即在內存分配過程中要儘可能地滿足內存需求。嵌入式系統應用千變萬化, 對系統的可靠性要求很高, 內存分配的請求必須得到滿足, 如果分配失敗, 則會帶來災難性的後果。
- 高效性, 即在內存分配過程中要儘可能地減少浪費。在嵌入式系統中, 對體積成本的要求較高, 所以在有限的內存空間內, 如何合理的配置及管理, 提高使用效率顯的尤爲重要
實時嵌入式系統開發者通常需要根據系統的要求在RTOS提供的內存管理之上實現特定的內存管理,本章研究 VxWorks的Wind內核內存管理機制。
5.1 VxWorks內存管理概述
5.1.1 VxWorks內存佈局
VxWorks5.5 版本提供兩種虛擬內存支持(Virtual MemorySupport):基本級(Basic Level)和完整級(Full Level)。基本級虛擬內存以Cache 功能爲基礎。當DMA 讀取內存前必須確保數據以更新到內存中而不是仍緩存在Catch 中。對於完整級需要購買可選組件VxVMI,其功能爲:提供對CPU 的內存管理單元的編程接口,同時提供內存保護,它在功能上覆蓋了基本級虛存。VxWorks的內存配置宏如下:
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 bytes are big enough to be a fragment on the free list -
* first, fix up the newly allocated block.
*/
//餘下的碎片最夠大,首先讓其成爲一個單獨的塊
pNewHdr->nWords = nWords;
pNewHdr->free = TRUE;
/*將這個單獨的塊加入空閒塊鏈表*/
pNextHdr = NEXT_HDR (pNewHdr);
pNextHdr->nWords = ((UINT) endOfBlock - (UINT) pNextHdr) / 2;
pNextHdr->pPrevHdr = pNewHdr;
pNextHdr->free = TRUE;
//加入空閒塊鏈表
dllAdd (&partId->freeList, HDR_TO_NODE (pNextHdr));
/* fix next block to point to the new fragment on the free list */
//調整新塊後面的空閒鏈表
NEXT_HDR (pNextHdr)->pPrevHdr = pNextHdr;
}
return (pNewHdr);
}
分析:在vxWorks的內存池中,所有的內存塊,無論是空閒塊還是非空閒塊,均是通過其每一個塊內部的第一個塊頭和最後一個塊頭將內存池中的所有塊連接成一個整體;
就單個內存塊而言,緊接着前面第一個塊頭後一個塊頭才代表着該內存塊中處於收尾兩個塊頭大小的那塊內存;
並且這第二內存塊頭和空閒塊頭相比僅少了DL_NODE成員域,用於鏈入空閒鏈表。
示意圖如下:
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;
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;
正因爲如此,在第二個內存塊後面只需要額外包含sizeof(DN_NODE)大小的空間,兩個類型就可以互相裝換。
換句話說內存塊除去收尾塊頭,只要剩下的內存可以存放一個空閒頭的大小,這個內存塊就
可以參與管理。
這也是爲什麼memSysPartition必須強調最小內存必須爲一個空閒塊頭的大小。
5.2.3.3 內存塊的釋放free()
內存釋放時,根據塊頭中的信息判斷相鄰的內存塊是否空閒,如果相鄰內存塊空閒,將進行內存塊合併,並修改空閒塊長度;否則就把新釋放的內存插入到空閒鏈表中。這裏,空閒鏈表採用的是雙鏈表的數據結構,它將緊臨塊頭的8個字節(2個指針長度)用來存放雙鏈表的指針,所有由malloc()分配的內存必須顯式調用free()進行釋放,以避免內存溢出,如圖5.10。
圖5.10 內存釋放
free()將malloc()分配的內存塊釋放到內存分區memSysPartition的空閒塊鏈表中,其代碼如下:
void free (void *ptr )
{
(void) memPartFree (&memSysPartition, (char *) ptr);
}
free()是memPartFree()的封裝。
如果pPrevHdr指向的前一塊也爲空閒塊,則只需擴大前一塊的內存空間大小即可;否則,把空閒塊鏈入分區的freeList鏈表。空閒塊鏈入的時候,還要判斷能否與後一塊內存空間合併(當前塊的頭部地址加上當前塊的大小即爲下一塊的頭部地址)。
內存釋放的關鍵函數是memPartFree,設內存分區爲partId,釋放的內存地址爲pBlock,分析如下:
(1)通過BLOCK_TO_HDR宏,獲得內存頭pHdr。
(2)獲得內存塊大小pHdr->nWords。
(3)如果前一塊空閒PREV_HDR (pHdr)->free爲TRUE,則擴大前一塊即可,擴大後的前一塊爲當前塊。否則將該塊作爲獨立空閒塊鏈入分區空閒鏈表partId->freeList。
(4)檢查後一塊是否空閒,若是則將後一塊併入到當前塊上。
(5)更新後一塊的pPrevHdr指針指向當前塊。
代碼如下:
STATUS memPartFree( PART_ID partId, char *pBlock )
{
FAST BLOCK_HDR *pHdr;
FAST unsigned nWords;
FAST BLOCK_HDR *pNextHdr;
if (OBJ_VERIFY (partId, memPartClassId) != OK)
return (ERROR);
if (pBlock == NULL)
return (OK); /* ANSI C compatibility */
pHdr = BLOCK_TO_HDR (pBlock);
/* 獲取memSysPartition的信號量 */
semTake (&partId->sem, WAIT_FOREVER);
/* optional check for validity of block */
if ((partId->options & MEM_BLOCK_CHECK) &&
!memPartBlockIsValid (partId, pHdr, FALSE))
{
semGive (&partId->sem); /* release mutual exclusion */
if (memPartBlockErrorRtn != NULL)
(* memPartBlockErrorRtn) (partId, pBlock, "memPartFree");
if (partId->options & MEM_BLOCK_ERROR_SUSPEND_FLAG)
{
if ((taskIdCurrent->options & VX_UNBREAKABLE) == 0)
taskSuspend (0);
}
errnoSet (S_memLib_BLOCK_ERROR);
return (ERROR);
}
nWords = pHdr->nWords;
//檢查該內存塊前面的內存塊是否空閒,如果空閒將該內存塊合併到前面的空閒內存塊中
if (PREV_HDR (pHdr)->free)
{
pHdr->free = FALSE; /* this isn't a free block */
pHdr = PREV_HDR (pHdr); /* coalesce with prev block */
pHdr->nWords += nWords;
}
else
{//否則單獨作爲空閒塊,摻入memSysPartition分區的空閒塊鏈表
pHdr->free = TRUE; /* add new free block */
dllInsert (&partId->freeList, (DL_NODE *) NULL, HDR_TO_NODE (pHdr));
}
//檢查與其相鄰的後面的內存塊釋放空閒,如果空閒將其後面的內存塊從空閒隊列中
//刪除,並併入當前空閒塊。
pNextHdr = NEXT_HDR (pHdr);
if (pNextHdr->free)
{
pHdr->nWords += pNextHdr->nWords; /* coalesce with next */
dllRemove (&partId->freeList, HDR_TO_NODE (pNextHdr));
}
/* fix up prev info of whatever block is now next */
NEXT_HDR (pHdr)->pPrevHdr = pHdr;
/* adjust allocation stats */
partId->curBlocksAllocated--;
partId->curWordsAllocated -= nWords;
semGive (&partId->sem);
return (OK);
}
5.3 VxWorks與動態內存相關的API
表5.2 VxWorks5.5動態內存管理相關API
VxWorks 5.5允許用戶建立並管理自己的內存分區(Memory Partition),並提供相應的函數,如下表5.3所示。
表5.3 創建內存分區函數
每個分區又各自的分區ID。ID實際是指向分區內存管理數據結構mem_part的指針,定義在memLib.h中。
typedef struct mem_part *PART_ID;
5.4 VxWorks內存分配機制總結
在可用內存塊上建立空閒塊,並交給內存分區進行管理。內存分區記錄了空閒塊鏈表和一些統計信息。申請時從分區的空閒鏈表中找到合適的空閒塊,並從該空閒塊上分割出一塊交給用戶使用,當剩餘空閒塊太小時,整塊交給用戶使用。釋放時,判斷能否進行前向、後向合併,若可以則直接合並,否則作爲一個獨立塊鏈入空閒鏈表中。
用戶申請到的內存塊,在它的前面隱藏了該塊的基本信息(BLOCK_HDR,DL_NODE等信息),內存釋放就是根據這個隱藏信息進行的。
內存塊之間建立了兩種聯繫:pPrevHdr元素存放了物理上連續的前一塊內存的地址,用於空閒內存塊的合併;鏈表節點node用於將空閒內存塊鏈入到分區的freeList鏈表中,用於空閒塊的管理。空閒內存塊合併機制保證了任意兩個空閒內存塊在物理上不是連續的,從而避免了內存碎片。
內存分配採用首次適應(first fit)算法,即每次分配時,都是分配空閒鏈表中第一個合適塊。設申請nWords字節的內存大小,要求alignment字節對齊,那麼合適的要求是內存塊的可用空間大於nWords + alignment / 2或者內存塊可用空間等於nWords且alignment字節對齊。
這裏需要注意的是當初始任務終結時,爲初始化任務預留的內存10000字節,是作爲一個獨立的空閒塊,加入到分區freeList鏈表中,爲啥vxWorks沒有這樣做?這是因爲在爲初始任務tTaskRoot分配棧空間時,分區內存管理機制還沒有建立,即malloc()服務例程還不能實現,只能手動進行分配,當初始任務被刪除時,爲其手動分配的任務只能作爲一個獨立的空閒塊,加入到分區freeList鏈表中,而不能和其它內存開合併。
至此,本篇介紹的VxWorks內存管理機制就告一段落了,嚴格意義上本篇介紹的內存管理模塊,比如malloc()/free()已經超過了Wind內存管理的範疇,但是爲了描述的完整性,稍微增加了一點點O(∩_∩)O~。