天天看點

(二)Freertos記憶體管理動态記憶體配置設定方案範例

為了使FreeRTOS盡可能友善使用,任務、隊列、信号量和事件組這些核心對象不是在編譯時靜态配置設定的,而是在運作時動态配置設定的;FreeRTOS在每次建立核心對象時配置設定記憶體,在每次删除核心對象時釋放記憶體。這種政策減少了設計和規劃工作,簡化了API,并最小化了RAM占用。

動态記憶體配置設定是一個C程式設計概念,不是一個特定于FreeRTOS或多任務處理的概念。它與FreeRTOS相關,因為核心對象是動态配置設定的,而通用編譯器提供的動态記憶體配置設定方案并不适用于所有實時應用程式。雖然可以使用标準C庫malloc()和free()函數配置設定記憶體,但是會存在以下問題:

  1.  這兩個函數在小型嵌入式系統中可能不可用。
  2.  這兩個函數的具體實作可能會相對較大,會占用較多寶貴的代碼空間。
  3.  這兩個函數通常不具備線程安全特性。
  4.  這兩個函數具有不确定性。每次調用時的時間開銷都可能不同。
  5.  這兩個函數會産生記憶體碎片。
  6.  這兩個函數會使得連結器配置得複雜。
  7. 如果允許堆空間增長到由其他變量使用的記憶體中,則由此引起的錯誤難以調試。

動态記憶體配置設定方案範例

早期版本的FreeRTOS使用了一種記憶體池配置設定方案,在編譯時預先配置設定不同大小記憶體塊的池,然後由記憶體配置設定函數傳回。盡管這種方案在實時系統中比較常見,但它被證明是許多支援請求的來源,主要是因為它不能十分有效地使用RAM,使其在真正小型的嵌入式系統中可行——是以該方案被放棄了。

不同的嵌入式系統具有不同的記憶體配置和時間要求。是以單一的記憶體配置設定算法隻可能适合部分應用程式。是以,FreeRTOS現在将記憶體配置設定作為可移植層的一部分(而不是核心代碼的一部分)。此外,從核心代碼庫中删除動态記憶體配置設定使應用程式開發者能夠提供适合自己的特定實作。

當核心請求記憶體時,調用 pvPortMalloc()而不是直接調用 malloc();當釋放記憶體時,調用 vPortFree()而不是直接調用 free()。 pvPortMalloc()具有與 malloc()相同的函

數原型; vPortFree()也具有與 free()相同的函數原型。pvPortMalloc()和vPortFree()是公共函數,是以也可以從應用程式代碼調用。FreeRTOS提供了5個實作pvPortMalloc()和vPortFree()的示例。FreeRTOS應用程式可以使用其中一個示例實作,也可以自己去實作。

Heap_1

Heap_1.c 實作了一個非常基本的 pvPortMalloc()版本,而且沒有實作 vPortFree()。如果應用程式不需要删除任務,隊列或者信号量,則具有使用 heap_1 的潛質。 Heap_1總是具有确定性。這種配置設定方案是将 FreeRTOS 的記憶體堆空間看作一個簡單的數組。當調用pvPortMalloc()時,則将數組又簡單地細分為更小的記憶體塊。數組的總大小(位元組為機關)在 FreeRTOSConfig.h 中由 configTOTAL_HEAP_SIZE定義。以這種方式定義一個巨型數組會讓整個應用程式看起來耗費了許多記憶體——即使是在數組沒有進行任何實際配置設定之前。

需要為每個建立的任務在堆空間上配置設定一個任務控制塊(TCB)和一個棧空間。 下圖展示了 heap_1 是如何在任務建立時細分這個簡單數組的。從下圖中可以看到:

  • A 表示數組在沒有任何任務建立時的情形,這裡整個資料是空的。
  • B 表示數組在建立了一個任務後,B顯示array。
  • C 表示數組在建立了三個任務後的情形
(二)Freertos記憶體管理動态記憶體配置設定方案範例

Heap_2

Heap_2.c 也是使用了一個由 configTOTAL_HEAP_SIZE 定義大小的簡單數組。不同于 heap_1 的是, heap_2 采用了一個最佳比對算法來配置設定記憶體,并且支援記憶體釋放。由于聲明了一個靜态數組,是以會讓整個應用程式看起來耗費了許多記憶體——即使是在數組沒有進行任何實際配置設定之前。最佳比對算法保證 pvPortMalloc()會使用最接近請求大小的空閑記憶體塊。比如,考慮以下情形:

  • 堆空間中包含了三個空閑記憶體塊,分别為 5 位元組, 25 位元組和 100 位元組大小。
  • pvPortMalloc()被調用以請求配置設定 20 位元組大小的記憶體空間。

比對請求位元組數的最小空閑記憶體塊是具有25位元組大小的記憶體塊——是以pvPortMalloc()會将這個 25 位元組塊再分為一個 20 位元組塊和一個 5 位元組塊 ,然後傳回一個指向 20 位元組塊的指針。剩下的 5 位元組塊則保留下來,留待以後調用 pvPortMalloc()時使用。Heap_2.c 并不會把相鄰的空閑塊合并成一個更大的記憶體塊,是以會産生記憶體碎片

——如果配置設定和釋放的總是相同大小的記憶體塊,則記憶體碎片就不會成為一個問題。Heap_2.c 适合用于那些重複建立與删除具有相同棧空間任務的應用程式。

(二)Freertos記憶體管理動态記憶體配置設定方案範例

上圖展示了當任務建立,删除以及再建立過程中,最佳比對算法是如何工作的。從上圖可以看出:

  • A 表示數組在建立了三個任務後的情形。數組的頂部還剩餘一個大空閑塊。
  • B 表示數組在删除了一個任務後的情形。頂部的大空閑塊保持不變,并多出了兩個小的空閑塊,分别是被删除任務的 TCB 和任務棧。
  • C 表示數組在又建立了一個任務後的情形。建立一個任務會産生兩次調用pvPortMalloc(),一次是配置設定 TCB,一次是配置設定任務棧(調用 pvPortMalloc()發生在xTaskCreate() API 函數内部)。每個 TCB 都具有相同大小,是以最佳比對算法可以確定之前被删除的任務占用的 TCB 空間被重新配置設定用作新任務的 TCB 空間。建立任務的棧空間與之前被删除任務的棧空間大小相同,是以最佳比對算法會保證之前被删除任務占用的棧空間會被重新配置設定用作新任務的棧空間。數組頂部的大空閑塊依然保持不變。

Heap_2.c 雖然不具備确定性,但是比大多數标準庫實作的 malloc()與 free()更有效率。

Heap_3

簡單地調用了标準庫函數 malloc()和 free(),但是通過暫時挂起排程器使得函數調用備線程安全特性。此時的記憶體堆空間大小不受 configTOTAL_HEAP_SIZE 影響,而是由連結器配置決定。

Heap_4

與heap_1和heap_2一樣,heap_4的工作原理是将數組細分為更小的塊。與前面一樣,該數組是靜态聲明的,并由configTOTAL_HEAP_SIZE進行維數劃分,是以會使應用程式在從該數組實際配置設定任何記憶體之前就看起來消耗了大量RAM。

Heap_4使用first fit算法配置設定記憶體。與heap_2不同,heap_4将相鄰的空閑記憶體塊結合(合并)成一個更大的塊,進而最大限度地減少記憶體碎片的風險,并使其适用于重複配置設定和釋放不同大小RAM塊的應用程式。first fit算法確定pvPortMalloc()使用第一個足夠大的空閑記憶體塊來容納請求的位元組數。例如,考慮以下場景:

  • 堆包含三個空閑記憶體塊,按照它們在數組中出現的順序,分别為5個位元組、200個位元組和100個位元組。
  • 調用pvPortMalloc()請求20位元組的RAM。

第一個可以容納請求位元組數的空閑RAM塊是200位元組的塊,是以pvPortMalloc()将200位元組的塊分割為一個20位元組的塊和一個180位元組的塊,然後傳回一個指向20位元組塊的指針。新的180位元組塊仍然可以用于以後對pvPortMalloc()的調用。

(二)Freertos記憶體管理動态記憶體配置設定方案範例

上圖示範了在配置設定和釋放記憶體時,使用記憶體合并的heap_4的 first fit算法是如何工作的。如上圖所示:

  1. A顯示建立了三個任務後的數組。一個大的空閑塊仍然在數組的頂部。
  2. B顯示其中一個任務被删除後的數組。除了數組頂部的大空閑塊,還有一個空閑塊--先前在其中配置設定了已删除的任務的TCB和堆棧。注意,與示範heap_2時不同,删除TCB時釋放的記憶體和删除堆棧時釋放的記憶體并不是作為兩個單獨的空閑塊保留,而是合并起來建立一個更大的空閑塊。
  3. C顯示建立FreeRTOS隊列後的情況。隊列使用xQueueCreate() API函數建立,xQueueCreate()調用pvPortMalloc()來配置設定隊列使用的RAM。由于heap_4使用一種first fit算法,pvPortMalloc()将從第一個足夠容納隊列的空閑RAM塊配置設定RAM,這是剛才删除任務時釋放的RAM。然而,隊列并沒有消耗空閑塊中的所有RAM,是以塊被分成兩個,未使用的部分将來仍然可以被pvPortMalloc()調用。
  4. D顯示了從應用程式代碼直接調用pvPortMalloc()後的情況,而不是通過調用FreeRTOS API函數間接調用。使用者配置設定的塊足夠小,可以容納第一個空閑塊,這是配置設定給隊列的記憶體和配置設定給下一個TCB的記憶體之間的塊。删除任務時釋放的記憶體現在被分成三個獨立的塊;第一個塊儲存隊列,第二個塊儲存使用者配置設定的記憶體,第三個塊保持空閑狀态。
  5. E顯示隊列被删除後的情況,自動釋放已經配置設定給被删除隊列的記憶體。現在在使用者配置設定的塊的兩邊都有空閑的記憶體。
  6. F顯示使用者配置設定的記憶體也被釋放後的情況。已經被使用者配置設定的塊所使用的記憶體與任意一邊的空閑記憶體相結合,以建立一個更大的空閑塊。

Heap_4不是确定性的,但它比malloc()和free()的大多數标準庫實作都要快。

Heap_5

heap_5用于配置設定和釋放記憶體的算法與heap_4所使用的算法相同。與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 );

參數說明:pxHeapRegions 

    指向HeapRegion_t結構數組開頭的指針。數組中的每個結構描述了使用heap_5時将成為堆一部分的記憶體區域的起始位址和長度。
    數組中的HeapRegion_t結構必須按起始位址排序;最低起始位址的記憶體區域的HeapRegion_t結構必須是數組中的第一個結構,描述具有最高起始位址的記憶體區域的HeapRegion_t結構
必須是數組中的最後一個結構。
    數組的結尾由HeapRegion_t結構标記,該結構的pucStartAddress成員設定為NULL。
           

每個單獨的記憶體區域都由類型為HeapRegion_t的結構體來描述。所有可用記憶體區域的描述作為數組傳遞給vPortDefineHeapRegions() HeapRegion_t結構。

typedef struct HeapRegion

{

       uint8_t *pucStartAddress;

       size_t xSizeInBytes;

} HeapRegion_t;

舉例來說,考慮下圖中所示的假設記憶體映射,它包含三個獨立的RAM塊:RAM1、RAM2和RAM3。它假定可執行代碼被放置在隻讀記憶體中,這裡沒有顯示。

(二)Freertos記憶體管理動态記憶體配置設定方案範例

以下代碼顯示了一個HeapRegion_t結構數組,它們一起描述了全部RAM:

#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 )

const HeapRegion_t xHeapRegions[] =

{

    { RAM1_START_ADDRESS, RAM1_SIZE },

    { RAM2_START_ADDRESS, RAM2_SIZE },

    { RAM3_START_ADDRESS, RAM3_SIZE },

    { NULL,                                                     0 } 

};

int main( void )

{

    vPortDefineHeapRegions( xHeapRegions );

}

 雖然以上代碼清楚的描述了RAM,但它将所有RAM配置設定給堆,沒有留下任何RAM可供其他變量使用。建構項目時,連結階段會為每個變量配置設定一個RAM位址。由連結器配置檔案描述了連結器可用的RAM通常,例如連結器腳本。在上圖 B中,假定連結器腳本包含了RAM1上的資訊,但不包含RAM2或RAM3上的資訊。是以,連接配接器在RAM1中放置了變量,隻留下RAM1中位址0x0001nnnn以上的部分供heap_5使用。0x0001nnnn的實際值将取決于所連結的應用程式中包含的所有變量的組合大小。連結器未使用RAM2和RAM3部分,讓heap_5可以使用RAM2和RAM3。

如果使用以上代碼,則配置設定給heap_5的RAM位址在0x0001nnnn之下将與用于儲存變量的RAM重疊。為了避免這種情況,首先數組中的HeapRegion_t結構可以使用的起始位址為0x0001nnnn,而不是起始位址0x00010000。然而,這不是一個最佳的方案,因為:

  1. 起始位址可能不容易确定。
  2. 連結器所使用的RAM數量可能會在以後的建構中發生變化,是以需要更新HeapRegion_t結構中使用的起始位址。
  3. 如果連結器使用的RAM和heap_5使用的RAM重疊,建構工具是不會知道的,是以将不會發出警告資訊。

以下代碼示範了一個更友善和可維護的示例。它聲明了一個ucHeap的數組。ucHeap是一個普通變量,是以它成為連結器配置設定給RAM1的資料的一部分。xHeapRegions數組中的第一個HeapRegion_t結構描述了ucHeap的起始位址和大小,是以ucHeap成為heap_5管理的記憶體的一部分。如上圖c所示,ucHeap的大小可以增加,直到連接配接器使用的RAM消耗掉所有的RAM1。

#define RAM2_START_ADDRESS

#define RAM2_SIZE

( ( uint8_t * ) 0x00020000 )

( 32 * 1024 )

#define RAM3_START_ADDRESS

#define RAM3_SIZE

( ( uint8_t * ) 0x00030000 )

( 32 * 1024 )

#define RAM1_HEAP_SIZE                                                                                                         ( 30 * 1024 )

static uint8_t ucHeap[ RAM1_HEAP_SIZE ];

const HeapRegion_t xHeapRegions[] =

{

      { ucHeap, RAM1_HEAP_SIZE },

      { RAM2_START_ADDRESS, RAM2_SIZE },

      { RAM3_START_ADDRESS, RAM3_SIZE },

      { NULL,                                                     0 }

};

這樣做的優點在于: 

  1. 沒有必要使用寫死的起始位址。
  2. 在HeapRegion_t結構中使用的位址将由連結器自動設定,即使連結器使用的RAM數量在以後的建構中發生變化,也不用擔心。
  3. 配置設定給heap_5的RAM不會與連結器放置到RAM1中的資料重疊。
  4. 如果ucHeap太大,應用程式就會連結失敗。

一些與堆記憶體相關的函數

xPortGetFreeHeapSize()

傳回在調用該函數時堆中的空閑位元組數。它可以用來優化堆大小。例如,如果xPortGetFreeHeapSize()在建立所有核心對象之後傳回2000,那麼configTOTAL_HEAP_SIZE的值可以減少2000。

xPortGetMinimumEverFreeHeapSize()

傳回的值表明應用程式已經接近耗盡堆空間。例如,如果xPortGetMinimumEverFreeHeapSize()傳回200,那麼表示在應用程式開始執行後的某個時間,它距離堆空間耗盡不到200個位元組。