FreeRTOS的堆內存管理
- 簡介
- 動態內存分配及其與 FreeRTOS 的相關性
- 動態內存分配選項
- 內存分配方案
- Heap_1
- heap_2
- Heap_3
- Heap_4
- 設置heap_4的起始地址
- Heap_5
- vPortDefineHeapRegions()
- 堆相關的函數
- xPortGetFreeHeapSize
- xPortGetMinimumEverFreeHeapSize
- Malloc調用失敗的Hook函數
這篇文章先說原理,下一遍文章說代碼的具體實現
簡介
從FreeRTOS V9.0.0開始,FreeRTOS應用程序可以完全靜態分配,無序包含堆內存管理器
動態內存分配及其與 FreeRTOS 的相關性
從FreeRTOS V9.0.0開始,內核對象可以在編譯時靜態分配,也可以在運行時動態分配,內核對象有任務、隊列、信號量和事件組。為了使FreeRTOS盡可能容易使用,這些內核對象不是在編譯時靜態分配,而是在運行時動態分配。 FreeRTOS會在每次創建內核對象時分配RAM,并在每次刪除內核對象時釋放RAM。該策略減少了設計和規劃工作,簡化了API,并最大限度減少了RAM占用空間。
動態內存分配是一個C編程概念,而不是特定于FreeRTOS或多任務處理的概念。它與FreeRTOS相關,因為內核對象是動態分配的,通用編譯器提供的動態內存分配方案不總是適合應用程序。可以使用C標準庫malloc()和free()分配內存,但由于一些原因,它們不合適:
- 它們在小型嵌入式系統上并不總是可用。
- 它們的實現可能相對較大,占用寶貴的代碼空間。
- 它們很少是線程安全的。
- 它們不是確定性的; 執行函數所花費的時間會因調用而異。
- 它們可能會受到碎片化 1 的影響。
- 它們會使鏈接器配置復雜化。
- 如果允許堆空間增長為其他變量使用的內存,它們可能是難以調試錯誤的根源。
動態內存分配選項
FreeRTOS現在將內存分配視為可移植層的一部分(而不是核心代碼庫的一部分)。這是因為不同的嵌入式系統具有不同的動態內存分配和時序要求,從核心代碼庫中刪除動態內存分配使應用程序編寫者能夠在適當的時候提供他們自己的特定實現。
當FreeRTOS需要RAM時,它不會調用malloc(),而是調用pvPortMalloc(),當RAM釋放內存時,內核不會調用free(),而是調用vPortFree()。pvPortMalloc()與標準C庫malloc函數原型相同,vPortFree()與標準C庫free()函數原型相同。
pvPortMalloc()和vPortFree()是公共函數,因此也可以從應用程序代碼中調用。
FreeRTOS提供了pvPortMalloc()和vPortFree()的五個示例實現,FreeRTOS應用程序可以使用示例實現之一,也可以提供自己的實現。這五個實例分別定義在heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c源文件中,均位于FreeRTOS/Source/portable/MemMang目錄中。
內存分配方案
Heap_1
小型專用嵌入式系統通常只在調度程序啟動前,只創建任務和其他內核對象,在這種情況下,內存僅在應用程序開始執行任何實時功能之前由內核動態分配,并且內存在應用程序的生命周期內保持分配狀態。 這意味著所選擇的分配方案不必考慮任何更復雜的內存分配問題,例如確定性和碎片,而可以只考慮代碼大小和簡單性等屬性。 也就是說,只分配內存,其他像把兩個相鄰的空閑塊整合到一塊,這個是不管的。
Heap_1.c實現了一個非常基本的pvPortMalloc()版本,并沒有實現vPortFree(),所以從不刪除任務或其他內核對象的應用程序可以使用heap_1。當調用pvPortMalloc()時,heap_1分配方案將一個堆細分為更小的塊,堆的總大小由FreeRTOSConfig.h中的宏configTOTAL_HEAP_SIZE設置,但是為字節。
每個創建的任務都要兩個內存塊,一個是任務控制塊(TCB),另一個堆棧,這兩個都從堆分配。下圖演示來了heap_1如何在創建任務時細分堆
- A 顯示創建任何任務之前的堆——整個堆都是空閑的。
- B 顯示創建一項任務后的堆。
- C 顯示創建三個任務后的堆。
heap_1適用于那些一旦創建好任務、信號量、隊列和事件組就再也不會刪除的應用。一些禁止使用動態內存分配的商業關鍵和安全關鍵系統也有可能使用 heap_1。由于與非確定性、內存碎片和分配失敗相關的不確定性,關鍵系統通常禁止動態內存分配——但 Heap_1 始終是確定性的,不能對內存進行碎片化。heap_1代碼實現和內存分配過程都很簡單,內存是從一個靜態數組(堆)分配到的,也就是適合那些不需要動態內存分配的應用
heap_2
為了向后兼容,FreeRTOS保留了Heap_21,但不建議使用它,使用heap_4,因為heap_4提供了增強的功能。
heap_2.c的大小也通過configTOTAL_HEAP_SIZE確定,它使用最佳擬合算法來分配內存,并且與heap_1不同,它允許釋放內存。同樣數組(堆)是靜態聲明的,因此會消耗大量的RAM,最佳擬合算法確保PVPortMalloc()使用大小與請求的字節數最接近的空閑內存塊,比如,考慮以下場景:堆包含三個空閑內存塊,分為為5字節、25字節和100字節,調用pvPortMalloc來請求20字節的RAM。可以容納請求的字節數最小RAM空閑塊是25字節,因此pvPortMalloc()將25字節塊分為一個20字節塊和一個5字節塊,然后返回指向20字節的塊的指針,新的5字節塊依然可以被pvPortMalloc調用。
與heap_4不同,heap_2不會將相鄰的空閑塊合并為一個更大的塊,因此容易發生碎片。 Heap_2適用于重復創建和刪除任務的應用程序,前提是分配給創建的任務和堆棧大小不變。
下圖演示了在創建、刪除和再次創建任務時最佳擬合算法的工作原理:
- A 顯示創建三個任務后的數組。 一個大的空閑塊保留在陣列的頂部。
- B 顯示其中一個任務被刪除后的數組。 數組頂部的大空閑塊仍然存在。 現在還有兩個較小的空閑塊,它們以前分配給已刪除任務的 TCB 和堆棧。
- C 顯示創建另一個任務后的情況。 創建任務導致對 pvPortMalloc() 的兩次調用,一次分配 新的 TCB,一次分配任務堆棧。 任務是使用 xTaskCreate() API 函數創建的。 對 pvPortMalloc() 的調用發生在 xTaskCreate() 內部。
Heap_2 不是確定性的,但比 malloc() 和 free() 的大多數標準庫實現要快。
Heap_3
Heap_3使用標準庫malloc()和free()函數,因此堆的大小由鏈接器配置定義,configTOTAL_HEAP_SIZE設置對其沒有影響,Heap_3通過暫時掛起FreeRTOS調度程序使malloc()和free()線程安全。
Heap_4
與heap_1和heap_2一樣,heap_4的工作原理是將數組細分為更小的塊,數組是靜態分配的,并有configTOTAL_HEAP_SIZE確定尺寸,heap_4使用first fit算法來分配內存,與heap_2冉,heap_4將合并相鄰的空閑內存塊,然后組合成一個更大的塊,從而最大限度的降低內存碎片的風險。
the first fit算法確保pvPortMalloc()使用第一個能夠容納請求字節數的空閑塊,例如,考慮以下場景:
堆包含三個空閑內存塊,按照它們在數組中出現的順序,分別為5字節、200字節和100字節,調用pvPortMalloc()來請求20字節的RAM,第一個適合請求的字節數的空閑塊是200字節,因此pvPortMalloc在返回指針之前將200字節拆分為一個20字節的塊和一個180字節的塊,返回的指針指向20字節的塊,新的180字節塊仍可以被pvPortMalloc調用。
Heap_4將合并相鄰的空閑塊組合成一個更大的塊,最大限度的降低碎片的風險,并使其適用于重復分配和釋放不同大小的RAM塊的應用程序。
下圖演示了具有內存合并的 heap_4 首次擬合算法的工作原理,如內存被分配和釋放:
- A 顯示創建三個任務后的數組。一個大的空閑塊保留在陣列的頂部。
- B 顯示其中一個任務被刪除后的數組。數組頂部的大空閑塊仍然存在。還有一個空閑塊,其中先前分配了已刪除任務的 TCB 和堆棧。請注意,與演示 heap_2 時不同,刪除 TCB 時釋放的內存和刪除堆棧時釋放的內存不會保留為兩個單獨的空閑塊,而是組合在一起以創建更大的單個空閑塊。
- C 顯示了創建 FreeRTOS 隊列后的情況。隊列是使用 xQueueCreate() API 函數創建的。 xQueueCreate() 調用 pvPortMalloc() 來分配隊列使用的 RAM。由于 heap_4 使用the first fit算法,pvPortMalloc() 將從第一個足夠大以容納隊列的空閑 RAM 塊分配 RAM,使用的是刪除任務時釋放的 RAM。然而,隊列不會消耗空閑塊中的所有 RAM,因此該塊被分成兩部分,未使用的部分仍然可用于未來對 pvPortMalloc() 的調用。
- D 顯示了直接從應用程序代碼調用 pvPortMalloc() 后的情況,而不是通過調用 FreeRTOS API 函數間接調用的情況。用戶分配的塊足夠小,可以放入第一個空閑塊中,該塊是分配給隊列的內存和分配給后續 TCB 的內存之間的塊。
刪除任務時釋放的內存現在已拆分為三個單獨的塊;第一個塊保存隊列,第二個塊保存用戶分配的內存,第三個塊保持空閑。 - E顯示隊列被刪除后的情況,自動釋放已分配給刪除隊列的內存。現在用戶分配塊的兩側都有空閑內存。
- F 顯示用戶分配的內存也被釋放后的情況。用戶分配塊已使用的內存已與任一側的空閑內存組合以創建更大的單個空閑塊。
Heap_4 不是確定性的,但比 malloc() 和 free() 的大多數標準庫實現要快。
設置heap_4的起始地址
有時,應用程序編寫者需要將 heap_4 使用的數組放置在特定的內存地址。 例如,FreeRTOS 任務使用的堆棧是從堆分配的,因此可能有必要確保堆位于快速內部內存中,而不是位于慢速外部內存中。
默認情況下,heap_4 使用的數組在 heap_4.c 源文件中聲明,其起始地址由鏈接器自動設置。 但是,如果 FreeRTOSConfig.h 中的 configAPPLICATION_ALLOCATED_HEAP 設置為 1,則該數組必須改為由使用 FreeRTOS 的應用程序聲明。 如果數組被聲明為應用程序的一部分,那么應用程序的編寫者可以設置它的起始地址。
如果 configAPPLICATION_ALLOCATED_HEAP 在 FreeRTOSConfig.h 中設置為 1,則必須在應用程序的源文件之一中聲明一個名為 ucHeap 并由 configTOTAL_HEAP_SIZE 設置大小的 uint8_t 數組。
Heap_5
heap_5用于分配和釋放內存的算法和heap_4相同,不同的是heap_5不限于從單個靜態聲明的數組分配內存,heap_5可以從多個獨立的內存空間分配內存,當運行 FreeRTOS 的系統提供的 RAM 沒有在系統內存映射中顯示為單個連續(沒有空間)塊時,Heap_5 很有用。
在撰寫本文時,heap_5 是唯一提供的內存分配方案,必須在調用 pvPortMalloc() 之前顯式初始化。 Heap_5 使用 vPortDefineHeapRegions() API 函數初始化。 使用 heap_5 時,必須在創建任何內核對象(任務、隊列、信號量等)之前調用 vPortDefineHeapRegions()。
vPortDefineHeapRegions()
vPortDefineHeapRegions用于指定每個單獨的內存區域的起始地址和大小,這些區域構成heap_5使用的總內存。
void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions );
每個單獨的內存區域由HeapRegion_t類型的結構體描述**,所有可用內存區域的描述**作為 HeapRegion_t 結構數組傳遞到 vPortDefineHeapRegions()。
typedef struct HeapRegion
{
/* The start address of a block of memory that will be part of the heap.*/
uint8_t *pucStartAddress;
/* The size of the block of memory in bytes. */
size_t xSizeInBytes;
} HeapRegion_t;
pxHeapRegions 指向 HeapRegion_t 結構數組開頭的指針。 數組中的每個結構都描述了內存的起始地址和長度,數組中的 HeapRegion_t 結構體必須按起始地址排序; 描述起始地址最低的內存區域的 HeapRegion_t 結構必須是數組中的第一個結構,描述起始地址最高的內存區域HeapRegion_t 結構必須是數組中的最后一個結構。數組的末尾由 HeapRegion_t 結構標記,該結構的 pucStartAddress 成員設置為 NULL。
例如,考慮下圖中所示的假設內存映射,其中包含三個獨立的RAM塊:RAM1、RAM2和RAM3,假設可執行代碼放置在只讀寄存器中,沒有顯示。
下面的代碼描述了整個RAM的三個塊
/* Define the start address and size of the three RAM regions. */
#define RAM1_START_ADDRESS ( ( uint8_t * ) 0x00010000 )
#define RAM1_SIZE ( 65 * 1024 )
#define RAM2_START_ADDRESS ( ( uint8_t * ) 0x00020000 )
#define RAM2_SIZE ( 32 * 1024 )
#define RAM3_START_ADDRESS ( ( uint8_t * ) 0x00030000 )
#define RAM3_SIZE ( 32 * 1024 )
/* Create an array of HeapRegion_t definitions, with an index for each of the three RAM regions, and terminating the array with a NULL address. The HeapRegion_t structures must appear in start address order, with the structure that contains the lowest start address appearing first. */
const HeapRegion_t xHeapRegions[] =
{
{ RAM1_START_ADDRESS, RAM1_SIZE },
{ RAM2_START_ADDRESS, RAM2_SIZE },
{ RAM3_START_ADDRESS, RAM3_SIZE },
{ NULL, 0 } /* Marks the end of the array. */
};
int main( void )
{
/* Initialize heap_5. */
vPortDefineHeapRegions( xHeapRegions );
/* Add application code here. */
}
雖然正確地描述了 RAM,但它沒有展示一個可用的示例,因為它將所有 RAM 分配給堆,沒有空閑 RAM 可供其他變量使用。
構建項目時,構建過程的鏈接階段會為每個變量分配一個 RAM 地址。 可供鏈接器使用的 RAM 通常由鏈接器配置文件(例如鏈接描述文件)描述。假設鏈接描述文件包含關于 RAM1 的信息,但不包含關于 RAM2 或 RAM3 的信息。 因此,鏈接器將變量放置在 RAM1 中,只留下地址 0x0001nnnn 以上的 RAM1 部分可供 heap_5 使用。 0x0001nnnn 的實際值將取決于所鏈接的應用程序中包含的所有變量的組合大小。 鏈接器讓所有 RAM2 和所有 RAM3 未使用,留下整個 RAM2 和整個 RAM3 可供 heap_5 使用。
如果使用上述 中所示的代碼,分配給地址 0x0001nnnn 下的 heap_5 的 RAM 將與用于保存變量的 RAM 重疊。為了避免這種情況,xHeapRegions[] 數組中的第一個 HeapRegion_t 結構可以使用 0x0001nnnn 的起始地址,而不是 0x00010000 的起始地址。**但是,這不是推薦的解決方案,**因為:
- 起始地址可能不容易確定。
- 鏈接器使用的 RAM 量可能會在未來的構建中發生變化,因此需要更新 HeapRegion_t 結構中使用的起始地址。
- 如果鏈接器使用的 RAM 和 heap_5 使用的 RAM 重疊,構建工具將不知道,因此無法警告應用程序編寫者。
下面 展示了一個更方便和可維護的示例。它聲明了一個名為 ucHeap 的數組。 ucHeap 是一個普通變量,因此它成為鏈接器分配給 RAM1 的數據的一部分。 xHeapRegions 數組中的第一個 HeapRegion_t 結構描述了 ucHeap 的起始地址和大小,因此 ucHeap 成為 heap_5 管理的內存的一部分。 ucHeap 的大小可以增加,直到鏈接器使用的 RAM 耗盡所有 RAM1
linker. */
#define RAM2_START_ADDRESS ( ( uint8_t * ) 0x00020000 )
#define RAM2_SIZE ( 32 * 1024 )
#define RAM3_START_ADDRESS ( ( uint8_t * ) 0x00030000 )
#define RAM3_SIZE ( 32 * 1024 )
/* Declare an array that will be part of the heap used by heap_5. The array will be placed in RAM1 by the linker. */
#define RAM1_HEAP_SIZE ( 30 * 1024 )
static uint8_t ucHeap[ RAM1_HEAP_SIZE ];
/* Create an array of HeapRegion_t definitions. Whereas in Listing 6 the first entry described all of RAM1, so heap_5 will have used all of RAM1, this time the first entry only describes the ucHeap array, so heap_5 will only use the part of RAM1 that contains the ucHeap array. The HeapRegion_t structures must still appear in start address order, with the structure that contains the lowest start address appearing first. */
const HeapRegion_t xHeapRegions[] =
{
{ ucHeap, RAM1_HEAP_SIZE },
{ RAM2_START_ADDRESS, RAM2_SIZE },
{ RAM3_START_ADDRESS, RAM3_SIZE },
{ NULL, 0 } /* Marks the end of the array. */
};
堆相關的函數
xPortGetFreeHeapSize
xPortGetFreeHeapSize函數返回堆中的空閑字節數,它可用于優化堆大小。 例如,如果 xPortGetFreeHeapSize() 在所有內核對象創建后返回 2000,那么 configTOTAL_HEAP_SIZE 的值可以減少 2000。
size_t xPortGetFreeHeapSize( void );
xPortGetMinimumEverFreeHeapSize
xPortGetMinimumEverFreeHeapSize返回自FreeRTOS應用程序開始執行以來,堆中存在最小未分配字節數。xPortGetMinimumEverFreeHeapSize返回值表明應用程序接近耗盡堆空間的程序, 例如,如果 xPortGetMinimumEverFreeHeapSize() 返回 200,那么在應用程序開始執行后的某個時間,它會在 200 字節內耗盡堆空間。
xPortGetMinimumEverFreeHeapSize() 僅在使用 heap_4 或 heap_5 時可用。
size_t xPortGetMinimumEverFreeHeapSize( void );
Malloc調用失敗的Hook函數
pvPortMalloc() 可以直接從應用程序代碼中調用。每次創建內核對象時,它也會在 FreeRTOS 源文件中調用。內核對象的例子包括任務、隊列、信號量和事件組。
像標準庫 malloc() 函數一樣,如果 pvPortMalloc() 因為請求大小的塊不存在而無法返回 RAM 塊,那么它將返回 NULL,則不會創建內核對象。
如果對 pvPortMalloc() 的調用返回 NULL,則所有示例堆分配方案都可以配置一個調用掛鉤(或回調)函數。如果在 FreeRTOSConfig.h 中將 configUSE_MALLOC_FAILED_HOOK 設置為 1,那么應用程序必須提供一個 malloc 失敗的鉤子函數。該函數可以以任何適合應用程序的方式實現,該函數如下:
void vApplicationMallocFailedHook( void );