FreeRTOS 教程指南 学习笔记 第二章 内存管理
FreeRTOS 教程指南 學習筆記 第二章 內存管理
一、簡介
本書的以下章節將介紹內核對象,如任務、隊列、信號量和事件組。為了使FreeRTOS盡可能容易使用,這些內核對象不是在編譯時靜態分配,而是在運行時動態分配;FreeRTOS在每次創建內核對象時分配RAM,并在每次刪除內核對象時釋放RAM。此策略減少了設計和規劃工作,簡化了API,并最小化了RAM占用。
本章討論了動態內存分配。動態內存分配是一個C語言編程的概念,而不是一個特定于FreeRTOS或多任務處理的概念。它與FreeRTOS相關,因為內核對象是動態分配的,而且由通用編譯器提供的動態內存分配方案并不總是適合于實時應用程序。
二、動態內存分配及其與FreeRTOS的相關性
內存可以使用標準的C庫malloc()和免費的()函數進行分配,但它們可能不適合,或不適合,因為以下一個或多個原因:
- 它們并不總是在小型嵌入式系統上可用。
- 它們的實現可以相對較大,占用有價值的代碼空間。
- 它們很少是線程安全的。
- 它們不是確定性的;執行函數所花費的時間與調用不同。
- 他們可能會是碎片化的。
- 它們可以使連接器的配置復雜化。
- 如果允許堆空間增長到其他變量使用的內存中,它們可能成為難以調試錯誤的來源
三、用于動態內存分配的選項
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目錄中。
本章旨在讓讀者能夠很好地理解:
- FreeRTOS何時進行RAM分配。
- 介紹FreeRTOS提供的五個內存分配方案示例。
- 如何選擇內存分配方案。
四、內存分配方案的示例
Heap_1
對于小型專用嵌入式系統,通常在調度程序啟動之前只創建任務和其他內核對象。在這種情況下,只有在應用程序開始執行任何實時功能之前,內核才能動態地分配內存,并且內存會保持分配到應用程序的整個生命周期。這意味著所選擇的分配方案不必考慮任何更復雜的內存分配問題,如決定論和碎片化,而可以只考慮代碼大小和簡單性等屬性。
Heap_1.c實現了一個非常基本的pvPortMalloc()版本,而沒有實現vPortFree()。從未刪除任務或其他內核對象的應用程序有可能使用heap_1方式。一些原本會禁止使用動態內存分配的商業關鍵和安全關鍵系統也有可能使用heap_1。關鍵系統通常禁止動態內存分配,因為存在內存碎片和失敗的分配相關的不確定性——但Heap_1總是確定性的,不能將內存碎片分割。
heap_1分配方案將一個簡單的數組細分為更小的塊,作為對pvPortMalloc()的調用。該數組被稱為FreeRTOS堆。數組的總大小(以字節為單位)由FreeRTOSConfig.h中的定義configTOTAL_HEAP_SIZE設置。以這種方式定義一個大數組會使應用程序消耗大量RAM——甚至在從數組分配任何內存之前。每個已創建的任務都需要從堆中分配一個任務控制塊(TCB)和一個堆棧。圖5演示了heap_1如何在創建任務時細分簡單數組。
- A顯示了在創建任何任務之前的數組——整個數組都是空閑的。
- B顯示了創建一個任務后的數組。
- C表示在創建了三個任務后的數組。
Heap_2
Heap_2保留在FreeRTOS發行版中以實現向后兼容,但不建議在新設計中使用它。請考慮使用heap_4而不是heap_2,因為heap_4提供了增強的功能。Heap_2.c同樣通過細分一個由configTOTAL_HEAP_SIZE標注的數組來工作。它使用最佳匹配算法來分配內存,與heap_1不同,它確實允許釋放內存。同樣,數組是靜態聲明的,因此將使應用程序可能消耗大量RAM,甚至在數組的任何內存被分配之前。最佳擬合算法確保pvPortMalloc()使用大小最接近請求字節數的空閑內存塊。例如,考慮以下場景:
- 堆包含三個可用內存塊,分別為5字節、25字節和100字節。
- pvPortMalloc()以請求20字節的RAM。
適合最小內存請求的內存塊是25字節塊,所以在返回一個20字節塊的指針前,pvPortMalloc()25字節塊分割分成一個20字節和一個5 字節塊。新的5字節塊仍然留作將來其他分配對pvPortMalloc()的調用。
與heap_4不同,Heap_2不將相鄰的自由塊組合成一個更大的塊,所以它更容易破碎。但是,如果分配和隨后釋放的塊總是相同的大小,碎片不是問題。Heap_2適用于重復創建和刪除任務的應用程序,前提是分配給已創建任務的堆棧的大小不會更改。
圖6演示了最佳匹配算法在創建、刪除任務,然后再次創建任務時,是如何工作。請參見圖6:
- A顯示了在創建了三個任務后的數組。一個很大的自由塊仍然保留在數組的頂部。
- B顯示了刪除其中一個任務后的數組。位于數組頂部的較大的自由塊保留了下來。現在還有兩個較小的空閑塊以前分配給TCB和刪除任務的堆棧
- C表示在創建另一個任務后的情況。創建任務導致了對pvportMalloc()的兩個調用,一個用于分配一個新的TCB,另一個用于分配任務棧。任務是使用xTaskCreate()API函數創建的,詳細描述見第3.4節。對pvPortMalloc()的調用發生在xTaskCreate()的內部。
每個TCB的大小完全相同,因此最佳擬合算法確保先前分配給已刪除任務的TCB的RAM塊被重用,以分配新任務的TCB。分配給新創建的任務的堆棧大小與分配給之前刪除的任務的棧大小相同,因此最佳匹配算法可以確保重用之前分配給已刪除任務的棧的內存塊來分配新任務的棧。位于數組頂部的較大的未分配塊保持不變。
Heap_2不是確定性的,但比大多數malloc()和free()的標準庫執行都要快。
Heap_3
Heap_3.c使用標準庫malloc()和free()函數,因此堆的大小由鏈接器配置確定,而configTOTAL_HEAP_SIZE設置沒有影響。Heap_3通過暫時暫停FreeRTOS調度程序,使malloc()和free()線程安全。線程安全,和調度程序暫停,都是在第7章,資源管理中討論的主題。
Heap_4
和heap_1和heap_2一樣,heap_4的工作原理是將一個數組細分為更小的塊。與前面一樣,數組是靜態聲明的,并由configTOTAL_HEAP_SIZE標注,因此將使應用程序似乎消耗大量RAM,甚至在實際從數組分配任何內存之前。
==Heap_4使用第一個擬合算法來分配內存。與heap_2不同,heap_4將合并相鄰的空閑內存塊成為一個更大的內存塊,從而將內存碎片化的風險降到最低。==第一個擬合算法確保pvPortMalloc()使用第一個足夠大空閑內存塊來確保滿足請求的字節數。例如,考慮以下場景:
- 堆包含三個可用內存塊,按照它們在數組中出現的順序,分別為5字節、200字節和100字節。
- pvPortMalloc()以請求20字節的RAM。
第一個適合請求的自由RAM塊是200字節塊,因此pvPortMalloc()將200字節塊分成一個20字節塊和一個180字節塊,然后返回一個指向20字節塊的指針。新的180字節塊仍然可用于未來調用pvPortMalloc()。Heap_4將相鄰的自由塊合并成一個更大的塊,最大限度地減少了碎片化的風險,并使其適合于重復分配和釋放不同大小的RAM塊的應用程序。
圖7展示了在分配和釋放內存時,heap_4匹配算法合并內存是如何工作的。參見圖7:
- A顯示了在創建了三個任務后的數組。一個很大的自由塊仍然保留在數組的頂部。
- B顯示刪除其中一個任務后的數組。大的自由塊在數組的頂部還在。還有一個空閑塊,是之前分配各被刪除任務的TCB和棧。注意,不像當heap_2,刪除TCB和棧被刪除時釋放的內存,不再作為兩個獨立的空閑塊,而是組合成一個新的自由塊。
- C顯示創建了一個FreeRTOS隊列后的情況。隊列使用xQueueCreate()API函數創建,詳見第4.3節。xQueueCreate()調用pvPortMalloc()來分配隊列使用的RAM。由于heap_4使用第一個擬合算法,pvPortMalloc()將從第一個足以保存隊列的空閑RAM塊中進行分配,即圖7中刪除任務時釋放的RAM。但是,該隊列并不消耗空閑塊中的所有RAM,因此該塊被分成兩個,并且未使用的部分仍然可用于將來調用pvPortMalloc()時進行分配。
- D顯示了應用程序代碼直接調用pvPortMalloc()后,而不是通過調用FreeRTOS API函數間接調用的情況。用戶程序所需要分配的空間足夠小,第一個自由塊(隊列和下一個TCB之間)就可以滿足。當任務被刪除時釋放的內存現在已經被分成三個單獨的塊;第一個塊保存隊列,第二個塊保存用戶分配的內存,第三個塊保持空閑。
- E顯示隊列被刪除后的情況,這會自動釋放已分配給隊列的內存。現在在用戶分配的塊的兩邊都有空閑內存。
- F顯示了用戶分配的內存也被釋放后的情況。用戶分配的塊所使用的內存與兩邊的空閑內存相結合,以創建一個更大的單個空閑塊。
Heap_4不是確定性的,但比大多數malloc()和free()的標準庫執行都要快
設置Heap_4所使用的數組的起始地址
本節包含高級級別的信息。使用Heap_4并不需要閱讀或理解本節。
有時,應用程序作者需要將heap_4使用的數組放置在一個特定的內存地址上。例如,FreeRTOS任務使用的棧是從堆中分配的,因此可能有必要確保堆位于快速的內部內存中,而不是緩慢的外部內存中。
默認情況下,heap_4使用的數組在heap_4.c源文件中聲明,其起始地址由鏈接器自動設置。但是,如果在FreeRTOSConfig.h中將configAPPLICATION_ALLOCATED_HEAP編譯時配置常量設置為1,那么該數組必須由使用FreeRTOS的應用程序聲明。如果該數組被聲明為應用程序的一部分,那么應用程序作者就可以設置其起始地址。
如果在FreeRTOSConfig.h中configAPPLICATION_ALLOCATED_HEAP設置為1,那么應用程序的源文件中必須聲明一個名為ucHeap的uint8_t數組,并由configTOTAL_HEAP_SIZE設置大小。
將變量放置在特定內存地址所需的語法取決于正在使用的編譯器,因此請參閱編譯器的文檔。下面是兩個編譯器的示例:
- 示例2顯示了GCC編譯器聲明該數組所需的語法,并將該數組放在一個名為.my_heap的內存塊中。
- 示例3顯示了IAR編譯器聲明該數組所需的語法,并將該數組放在絕對內存地址0x20000000處。
Heap_5
heap_5用于分配和釋放內存的算法與heap_4相同。與heap_4不同的是,heap_5并不局限于從單個靜態的數組中分配內存;heap_5可以從多個獨立且分散的內存空間中分配內存。當RAM在系統的內存表中不顯示為單個連續塊時,heap_5將非常有用。
在編寫代碼時,heap_5是唯一在調用pvPortMalloc()前必須顯式的初始化的內存分配方法。Heap_5使用vPortDefineHeapRegions()API函數初始化。當使用heap_5時,必須在創建任何內核對象(任務、隊列、信號量等)之前調用vPortDefineHeapRegions()。
vPortDefineHeapRegions() API函數
vPortDefineHeapRegions()用于指定每個單獨內存區域的起始地址和大小,這些區域共同構成了heap_5所使用的總內存。
//The vPortDefineHeapRegions() API function prototype void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions );每個單獨的存儲區域都由HeapRegion_t類型的結構體來描述。所有可用內存區域的描述作為HeapRegion_t結構體組成的數組傳遞到vPortDefineHeapRegions()中。
//The HeapRegion_t structure /*返回值pxHeapRegions:指向HeapRegion_t結構體組成的數組開頭的指針。數組中的每個結構體都描述了在使用heap_5時將成為堆的一部分的內存區域的起始地址和長度。數組中的HeapRegion_t結構體必須按起始地址排序;描述具有最低起始地址的內存區域的HeapRegion_t結構必須是數組中的第一個結構,而描述具有最高起始地址的內存區域的HeapRegion_t結構必須是數組中的最后一個結構。數組的末端由一個HeapRegion_t結構標記,該結構的啟動起始地址成員設置為NULL。*/ 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;舉例來說,考慮圖8A中所示的假設內存映射,它包含三個獨立的RAM塊:RAM1、RAM2和RAM3。假設可執行代碼被放置在只讀內存中,但沒有顯示。
示例6顯示了一個HeapRegion_t結構體數組,它們一起描述了三個RAM塊。
雖然示例6正確地描述了RAM,但它沒有演示一個可用的示例,因為它將所有RAM分配給堆,沒有留下任何RAM供其他變量使用。
當編譯項目時,編譯過程的鏈接階段將為每個變量分配一個RAM地址。鏈接器可供使用的RAM通常由鏈接器配置文件來描述,例如鏈接器腳本。在圖8B中,假設鏈接器腳本包含了關于RAM1的信息,但不包含關于RAM2或RAM3的信息。因此,鏈接器在RAM1中放置了變量,只留下RAM1中地址0x0001 nnnn以上的部分可供heap_5使用。0x0001 nnnn的實際值將取決于被鏈接的應用程序中所包含的所有變量的組合大小。鏈接器使所有RAM2和所有RAM3都未使用,使得整個RAM2和整個RAM3可供heap_5使用。
如果使用了示例6中所示的代碼,那么在地址0x0001 nnnn下面分配給heap_5的RAM將與用于保存變量的RAM重疊。為了避免這種情況,xHeapRegions[]數組中的第一個HeapRegion_t結構可以使用起始地址0x0001 nnnn,而不是起始地址0x00010000。但是,這并不是一個推薦的解決方案,因為:
- 起始地址可能不容易確定。
- 鏈接器所使用的RAM的數量可能會在未來的構建中發生變化,因此需要更新到HeapRegion_t結構中所使用的起始地址。
- 如果鏈接器使用的RAM和heap_5使用的RAM重疊,構建工具將不知道,因此不能警告應用程序作者。
示例7展示了一個更方便和可維護的示例。它聲明了一個名為ucHeap的數組。ucHeap是一個普通變量,因此它成為鏈接器分配給RAM1的數據的一部分。xHeapRegion數組中的第一個HeapRegion_t結構描述了ucHeap的起始地址和大小,因此ucHeap成為由heap_5管理的內存的一部分。ucHeap的大小可以增加,直到連接器使用的RAM消耗掉了所有的RAM1,如圖8C所示。
//Listing 7. An array of HeapRegion_t structures that describe all of RAM2, all of RAM3, but only part of RAM1 /* Define the start address and size of the two RAM regions not used by the 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. */ };示例7中展示的技術的有如下優點:
- 不需要使用硬編碼的起始地址。
- 在HeapRegion_t結構中使用的地址將由鏈接器自動設置,因此將始終是正確的,即使鏈接器使用的RAM數量在未來的編譯中發生變化。
- 分配給heap_5的RAM不可能將由鏈接器放置到RAM1中的數據重疊。
- 如果ucHeap的大小太大,則該應用程序將不會進行鏈接。
Heap相關實用程序功能
The xPortGetFreeHeapSize() API Function
xPortGetFreeHeapSize()API函數返回在調用該函數時堆中的空閑字節數。它可以用于優化堆的大小。例如,如果xPortGetFreeHeapSize()在創建了所有內核對象后返回2000,那么configTOTAL_HEAP_SIZE的值可以減少到2000。當使用heap_3時,xPortGetFreeHeapSize()不可用。
/*返回值size_t:調用xPortGetFreeHeapSize()時堆中未分配的字節數。*/ size_t xPortGetFreeHeapSize( void );The xPortGetMinimumEverFreeHeapSize() API Function
xPortGetMinimumEverFreeHeapSize()API函數返回自FreeRTOS應用程序開始執行以來堆中存在的最小未分配的最小字節數。函數的返回值表明應用程序離堆內存溢出有多近。例如,如果返回值為200,那么,在應用程序開始執行后的某個時候,它距離堆空間耗盡不到200字節。
xPortGetMinimumEverFreeHeapSize()只有在使用heap_4或heap_5時才可用。
宏失效回調函數
可以直接從應用程序代碼中調用pvPortMalloc()。在FreeRTOS源文件中,每次創建內核對象(任務、隊列、信號量和事件組)時都會調用它。
就像標準庫Malloc()函數一樣,如果pvPortMalloc()因為請求的塊大小不存在,則將返回NULL。如果因為應用程序作者正在創建一個內核對象而執行pvPortMalloc(),并且返回NULL,則該內核對象將無法創建。所有示例堆分配方案都可以配置一個回調函數,當pvPortMalloc()返回為NULL時將調用該回調函數。
如果在FreeRTOSConfig.h中將configUSE_MALLOC_FAILED_HOOK設置為1,則應用程序必須提供一個宏失效回調函數,該函數具有示例10所示的名稱和原型。該函數可以以任何適合于該應用程序的方式來實現。
總結
以上是生活随笔為你收集整理的FreeRTOS 教程指南 学习笔记 第二章 内存管理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 计算机视觉引论
- 下一篇: eplan2.5安装教程