FreeRTOS系列第8篇---FreeRTOS內(nèi)存管理
該博客中貼了代碼。
內(nèi)存管理對應(yīng)用程序和操作系統(tǒng)來說都非常重要?,F(xiàn)在很多的程序漏洞和運行崩潰都和內(nèi)存分配使用錯誤有關(guān)。FreeRTOS操作系統(tǒng)將內(nèi)核與內(nèi)存管理分開實現(xiàn),操作系統(tǒng)內(nèi)核僅規(guī)定了必要的內(nèi)存管理函數(shù)原型,而不關(guān)心這些內(nèi)存管理函數(shù)是如何實現(xiàn)的。
每當(dāng)創(chuàng)建任務(wù)、隊列、互斥量、軟件定時器、信號量或事件組時,RTOS內(nèi)核會為它們分配RAM。標(biāo)準(zhǔn)函數(shù)庫中的malloc()和free()函數(shù)能夠用于完成這個任務(wù),但是:
- 在嵌入式系統(tǒng)中,它們并不總是可以使用的;
- 它們會占用更多寶貴的代碼空間;
- 它們沒有線程保護;
- 它們不具有確定性(每次調(diào)用執(zhí)行的時間可能會不同);
?為什么有這些問題?
FreeRTOS內(nèi)核規(guī)定的幾個內(nèi)存管理函數(shù)原型為:
- void *pvPortMalloc( size_t xSize ) :內(nèi)存申請函數(shù)
- void vPortFree( void *pv ) :內(nèi)存釋放函數(shù)
- void vPortInitialiseBlocks( void ) :初始化內(nèi)存堆函數(shù)
- size_t xPortGetFreeHeapSize( void ) :獲取當(dāng)前未分配的內(nèi)存堆大小
- size_t xPortGetMinimumEverFreeHeapSize( void ):獲取未分配的內(nèi)存堆歷史最小值
因此,提供一個替代的內(nèi)存分配方案通常是必要的。 嵌入式/實時系統(tǒng)具有千差萬別的RAM和時間要求,因此一個RAM內(nèi)存分配算法可能僅屬于一個應(yīng)用的子集。
FreeRTOS提供的內(nèi)存分配方案分別位于不同的源文件(heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c)之中。
一、 FreeRTOS 內(nèi)存 方案
1.1 heap_1.c
- 用于從不會刪除任務(wù)、隊列、信號量、互斥量等的應(yīng)用程序(實際上大多數(shù)使用FreeRTOS的應(yīng)用程序都符合這個條件)
- 執(zhí)行時間是確定的并且不會產(chǎn)生內(nèi)存碎片
- 實現(xiàn)和分配過程非常簡單,需要的內(nèi)存是從一個靜態(tài)數(shù)組中分配的,意味著這種內(nèi)存分配通常只是適用于那些不進行動態(tài)內(nèi)存分配的應(yīng)用。
一旦分配內(nèi)存之后,它甚至不允許釋放分配的內(nèi)存。大多數(shù)深度嵌入式(deeplyembedded)應(yīng)用只是在系統(tǒng)啟動時創(chuàng)建所有任務(wù)、隊列、信號量等,并且直到程序結(jié)束都會一直使用它們,永遠(yuǎn)不需要刪除。
API函數(shù)xPortGetFreeHeapSize()返回未分配的堆??臻g總大小,可以通過這個函數(shù)返回值對configTOTAL_HEAP_SIZE進行合理的設(shè)置。
static size_t xNextFreeByte = ( size_t ) 0;//記錄已經(jīng)非陪的內(nèi)存大小,用來定位下一空閑的內(nèi)存堆位置
static uint8_t *pucAlignedHeap = NULL;//堆對齊,這樣訪問速度更快
void *pvPortMalloc( size_t xWantedSize )
{
void *pvReturn = NULL;
static uint8_t *pucAlignedHeap = NULL;
/* 確保申請的字節(jié)數(shù),宏portBYTE_ALIGNMENT 是對齊字節(jié)數(shù),默認(rèn)為8 */
#if( portBYTE_ALIGNMENT != 1 )
{
//進行位與運算,來判斷是否為8字節(jié)對齊,等于0就說明時對齊的
//portBYTE_ALIGNMENT_MASK 為0x0007
if( xWantedSize & portBYTE_ALIGNMENT_MASK )
{
//將字節(jié)進行對齊
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
}
}
#endif
// 掛起任務(wù)調(diào)度器,因為申請內(nèi)存過程中需要保護,不能被打斷
vTaskSuspendAll();
{
if( pucAlignedHeap == NULL )
{
/* 第一次使用,確保內(nèi)存堆起始位置正確對齊 */
//內(nèi)存堆ucheap:由編譯器分配,不一定為8字節(jié)對齊的。
pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) &ucHeap[ portBYTE_ALIGNMENT ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
}
/* 邊界檢查,檢查可用內(nèi)存是否夠分配,變量xNextFreeByte是局部靜態(tài)變量,初始值為0 */
if( ( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE ) &&
( ( xNextFreeByte + xWantedSize ) > xNextFreeByte ) )
{
/* 返回申請的內(nèi)存起始地址并更新索引 */
pvReturn = pucAlignedHeap + xNextFreeByte;
//內(nèi)存申請完成后,更新變量xNextFreeByte
xNextFreeByte += xWantedSize;
}
}
( void ) xTaskResumeAll();
#if( configUSE_MALLOC_FAILED_HOOK == 1 )
{
if( pvReturn == NULL )
{
extern void vApplicationMallocFailedHook( void );
vApplicationMallocFailedHook();
}
}
#endif
// 成功返回內(nèi)存首地址,失敗返回NULL
return pvReturn;
}
1.2 heap_2.c
heap_2.c適用于需要動態(tài)創(chuàng)建任務(wù)的大多數(shù)小型實時系統(tǒng)(smallreal time)。
和方案1不同,這個方案使用一個最佳匹配算法,它允許釋放之前分配的內(nèi)存塊,但是它不會把相鄰的空閑塊合成一個更大的塊(換句話說,這會造成內(nèi)存碎片)。
有效的堆??臻g大小由位于FreeRTOSConfig.h文件中的configTOTAL_HEAP_SIZE宏來定義。
為了實現(xiàn)內(nèi)存釋放,heap_2引入內(nèi)存塊的概念,每分出去一段內(nèi)存就是一個內(nèi)存塊,剩下的空閑內(nèi)存也是一個內(nèi)存塊。
//使用一個鏈表結(jié)構(gòu)來跟蹤記錄空閑內(nèi)存塊,將空閑塊組成一個鏈表。8個字節(jié)
typedef struct A_BLOCK_LINK
{
struct A_BLOCK_LINK *pxNextFreeBlock; /*指向列表中下一個空閑塊*/
size_t xBlockSize; /*當(dāng)前空閑塊的大小,包括鏈表結(jié)構(gòu)大小*/
} BlockLink_t;
為了方便管理,可用的內(nèi)存塊會被組織在一個鏈表里,局部靜態(tài)變量xStart, xEnd用來記錄這個鏈表的頭尾
static BlockLink_t xStart, xEnd;
每個內(nèi)存塊前面都有BlockLink_t類型變量來描述此內(nèi)存塊。如下圖:
- 不能用在分配和釋放隨機字節(jié)堆??臻g的應(yīng)用程序
如果一個應(yīng)用程序動態(tài)的創(chuàng)建和刪除任務(wù),并且分配給任務(wù)的堆??臻g總是同樣大小,那么大多數(shù)情況下heap_2.c是可以使用的。但是,如果分配給任務(wù)的堆棧不總是相等,那么釋放的有效內(nèi)存可能碎片化,形成很多小的內(nèi)存塊。最后可能剩余很多個很小的空閑塊。(如下圖)最后會因為沒有足夠大的連續(xù)堆??臻g而造成內(nèi)存分配失敗。在這種情況下,heap_4.c是一個很好的選擇。 - 應(yīng)用程序直接調(diào)用pvPortMalloc() 和 vPortFree()函數(shù),而不僅是通過FreeRTOS API間接調(diào)用。

pvPortMalloc()申請內(nèi)存
void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
static BaseType_t xHeapHasBeenInitialised = pdFALSE;
void *pvReturn = NULL;
/* 掛起調(diào)度器 */
vTaskSuspendAll();
{
/* 如果是第一次調(diào)用內(nèi)存分配函數(shù),這里先初始化內(nèi)存堆,如圖2-2所示 */
if( xHeapHasBeenInitialised == pdFALSE )
{
prvHeapInit();//完成內(nèi)存堆的對齊,xstart和xEnd的初始化
xHeapHasBeenInitialised = pdTRUE;
}
/* 調(diào)整要分配的內(nèi)存值,需要增加上鏈表結(jié)構(gòu)體空間,heapSTRUCT_SIZE表示經(jīng)過對齊擴展后的結(jié)構(gòu)體大小 */
//即實際申請的內(nèi)存需要加上BlockLink_t的大小
if( xWantedSize > 0 )
{
xWantedSize += heapSTRUCT_SIZE;
/* 調(diào)整實際分配的內(nèi)存大小,向上擴大到對齊字節(jié)數(shù)的整數(shù)倍 */
if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0 )
{
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
}
}
if( ( xWantedSize > 0 ) && ( xWantedSize < configADJUSTED_HEAP_SIZE ) )
{
/* 空閑內(nèi)存塊是按照塊的大小排序的,從鏈表頭xStart開始,小的在前大的在后,以鏈表尾xEnd結(jié)束 */
pxPreviousBlock = &xStart;
pxBlock = xStart.pxNextFreeBlock;
/* 搜索最合適的空閑塊 */
while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
{
pxPreviousBlock = pxBlock;
pxBlock = pxBlock->pxNextFreeBlock;
}
/* 如果搜索到鏈表尾xEnd,說明沒有找到合適的空閑內(nèi)存塊,否則進行下一步處理 */
if( pxBlock != &xEnd )
{
/* 返回內(nèi)存空間,注意是跳過了結(jié)構(gòu)體BlockLink_t空間. */
pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + heapSTRUCT_SIZE );
/* 這個塊就要返回給用戶,因此它必須從空閑塊中去除. */
pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;
/* 如果這個塊剩余的空間足夠多,則將它分成兩個,第一個返回給用戶,第二個作為新的空閑塊插入到空閑塊列表中去*/
if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
{
/* 去除分配出去的內(nèi)存,在剩余內(nèi)存塊的起始位置放置一個鏈表結(jié)構(gòu)并初始化鏈表成員 */
pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );
pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
pxBlock->xBlockSize = xWantedSize;
/* 將剩余的空閑塊插入到空閑塊列表中,按照空閑塊的大小順序,小的在前大的在后 */
prvInsertBlockIntoFreeList( ( pxNewBlockLink ) );
}
/* 計算未分配的內(nèi)存堆大小,注意這里并不能包含內(nèi)存碎片信息 */
xFreeBytesRemaining -= pxBlock->xBlockSize;
}
}
traceMALLOC( pvReturn, xWantedSize );
}
( void ) xTaskResumeAll();
#if( configUSE_MALLOC_FAILED_HOOK == 1 )
{ /* 如果內(nèi)存分配失敗,調(diào)用鉤子函數(shù) */
if( pvReturn == NULL )
{
extern void vApplicationMallocFailedHook( void );
vApplicationMallocFailedHook();
}
}
#endif
return pvReturn;
}
釋放內(nèi)存:vPortFree()
void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;
if( pv != NULL )
{
//puc:要釋放內(nèi)存的首地址,是pvReturn所指向的地址
/* 根據(jù)傳入的參數(shù)找到鏈表結(jié)構(gòu) */
puc -= heapSTRUCT_SIZE;
/* 預(yù)防某些編譯器警告 */
pxLink = ( void * ) puc;
vTaskSuspendAll();
{
/* 將這個塊添加到空閑塊列表 */
prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
/* 更新未分配的內(nèi)存堆大小 */
xFreeBytesRemaining += pxLink->xBlockSize;
traceFREE( pv, pxLink->xBlockSize );
}
( void ) xTaskResumeAll();
}
}
1.3 heap_3.c
heap_3.c簡單的包裝了標(biāo)準(zhǔn)庫中的malloc()和free()函數(shù),包裝后的malloc()和free()函數(shù)具備線程保護。
因此,內(nèi)存堆需要通過編譯器或者啟動文件設(shè)置堆空間。
功能:
- 第一種和第二種內(nèi)存管理策略都是通過定義一個大數(shù)組作為內(nèi)存堆,數(shù)組的大小由宏configTOTAL_HEAP_SIZE指定。需要編譯器設(shè)置一個堆??臻g,一般在啟動代碼中設(shè)置,并且編譯器庫提供malloc()和free()函數(shù)。
- 不具有確定性
- 可能明顯的增大RTOS內(nèi)核的代碼大小
void *pvPortMalloc( size_t xWantedSize )
{
void *pvReturn;
vTaskSuspendAll();
{
pvReturn = malloc( xWantedSize );
traceMALLOC( pvReturn, xWantedSize );
}
( void ) xTaskResumeAll();
#if( configUSE_MALLOC_FAILED_HOOK == 1 )
{
if( pvReturn == NULL )
{
extern void vApplicationMallocFailedHook( void );
vApplicationMallocFailedHook();
}
}
#endif
return pvReturn;
}
1.4 heap_4.c
heap_4.c還特別適用于移植層代碼,可以直接使用pvPortMalloc()和 vPortFree()函數(shù)來分配和釋放內(nèi)存.
這個方案使用一個最佳匹配算法,但不像方案2那樣。它會將相鄰的空閑內(nèi)存塊合并成一個更大的塊(包含一個合并算法)。
與heap_2不同:
- 鏈表尾保存在內(nèi)存堆空間的最后位置,pxEnd,第二種是xEnd指向鏈表尾
- 第四種小地址在前,大地址在后(為了適應(yīng)合并算法),而第二種是小內(nèi)存在前。

API函數(shù)xPortGetFreeHeapSize()返回剩下的未分配堆??臻g的大?。捎糜趦?yōu)化設(shè)置configTOTAL_HEAP_SIZE宏的值),但是不能提供未分配內(nèi)存的碎片細(xì)節(jié)信息。
void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
void *pvReturn = NULL;
vTaskSuspendAll();
{
/* 如果是第一次調(diào)用內(nèi)存分配函數(shù),則初始化內(nèi)存堆 */
if( pxEnd == NULL )
{
prvHeapInit();
}
/* 申請的內(nèi)存大小合法性檢查:是否過大.結(jié)構(gòu)體BlockLink_t中有一個成員xBlockSize表示塊的大小,這個成員的最高位被用來標(biāo)識這個塊是否空閑.因此要申請的塊大小,不能使用這個位.*/
if( ( xWantedSize & xBlockAllocatedBit ) == 0 )
{
/* 計算實際要分配的內(nèi)存大小,包含鏈接結(jié)構(gòu)體BlockLink_t在內(nèi),并且要向上字節(jié)對齊 */
if( xWantedSize > 0 )
{
xWantedSize += xHeapStructSize;
/* 對齊操作,向上擴大到對齊字節(jié)數(shù)的整數(shù)倍 */
if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
{
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
configASSERT( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) == 0 );
}
}
if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) )
{
/* 從鏈表xStart開始查找,從空閑塊鏈表(按照空閑塊地址順序排列)中找出一個足夠大的空閑塊 */
pxPreviousBlock = &xStart;//上一個塊是xStart
pxBlock = xStart.pxNextFreeBlock;//將要判斷的塊
//遍歷可用空間,直到內(nèi)存大小滿足想要申請的。
while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
{
pxPreviousBlock = pxBlock;// pxPreviousBlock的下一個內(nèi)存塊就是找到的可用內(nèi)存塊
pxBlock = pxBlock->pxNextFreeBlock;
}
/* 如果最后到達(dá)結(jié)束標(biāo)識,則說明沒有合適的內(nèi)存塊,否則,進行內(nèi)存分配操作*/
if( pxBlock != pxEnd )
{
/* 返回分配的內(nèi)存指針,要跳過內(nèi)存開始處的BlockLink_t結(jié)構(gòu)體 */
pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize );
/* 將已經(jīng)分配出去的內(nèi)存塊從空閑塊鏈表中刪除 */
pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;
/* 如果剩下的內(nèi)存足夠大,則組成一個新的空閑塊 */
if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
{
/* 在剩余內(nèi)存塊的起始位置放置一個鏈表結(jié)構(gòu)并初始化鏈表成員 */
pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );
configASSERT( ( ( ( size_t ) pxNewBlockLink ) & portBYTE_ALIGNMENT_MASK ) == 0 );
pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
pxBlock->xBlockSize = xWantedSize;
/* 將剩余的空閑塊插入到空閑塊列表中,按照空閑塊的地址大小順序,地址小的在前,地址大的在后 */
prvInsertBlockIntoFreeList( pxNewBlockLink );
}
/* 計算未分配的內(nèi)存堆空間,注意這里并不能包含內(nèi)存碎片信息 */
xFreeBytesRemaining -= pxBlock->xBlockSize;
/* 保存未分配內(nèi)存堆空間歷史最小值 */
if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining )
{
xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;
}
/* 將已經(jīng)分配的內(nèi)存塊標(biāo)識為"已分配" */
pxBlock->xBlockSize |= xBlockAllocatedBit;
pxBlock->pxNextFreeBlock = NULL;
}
}
}
traceMALLOC( pvReturn, xWantedSize );
}
( void ) xTaskResumeAll();
#if( configUSE_MALLOC_FAILED_HOOK == 1 )
{ /* 如果內(nèi)存分配失敗,調(diào)用鉤子函數(shù) */
if( pvReturn == NULL )
{
extern void vApplicationMallocFailedHook( void );
vApplicationMallocFailedHook();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
configASSERT( ( ( ( size_t ) pvReturn ) & ( size_t ) portBYTE_ALIGNMENT_MASK ) == 0 );
return pvReturn;
}
1.5 heap_5.c
Heap_5通過調(diào)用vPortDefineHeapRegions()函數(shù)實現(xiàn)初始化,它允許程序設(shè)置多個非連續(xù)內(nèi)存堆,如片內(nèi)RAM和片外RAM。使用heap_5創(chuàng)建任何對象前,要先執(zhí)行vPortDefineHeapRegions()函數(shù)。
創(chuàng)建RTOS對象(任務(wù)、隊列、信號量等等)會隱含的調(diào)用pvPortMalloc().