天天看點

精通FreeRTOS實時時鐘核心Chapter2

第二章 堆記憶體管理

從FreeRTOS V9.0.0開始,FreeRTOS可以完全靜态配置,而不用包含一個堆記憶體管理器。

2.1章節介紹

前提

FreeRTOS是作為一系列C源碼給出的,是以一個合格的C程式員是使用FreeRTOS的先決條件。是以,此章節假定讀者都熟悉以下概念:

  • 一個C項目是如何編譯的,包含不同的編譯和連結步驟。
  • 什麼是棧,什麼是堆。
  • 标準C庫malloc()和free()函數。

變化的記憶體配置設定和其對于FreeRTOS的現實意義

FreeRTOS V9.0.0核心中,核心對象可以在一段時間内配置設定為靜态的,或在運作時作為動态的。本書的一下章節将會介紹核心對象例如任務,隊列,信号和事件組。為了使FreeRTOS盡可能用起來簡單,這些核心對象都不是在編譯時間靜态配置設定的,而是在運作時動态配置設定。FreeRTOS在核心對象被建立時配置設定RAM,在核心對象被删除時回收記憶體。這一原則減少了設計難度,簡化了API,最小化記憶體占用。

本章讨論動态記憶體配置設定。動态記憶體配置設定時C語言程式設計的概念,并不是FreeRTOS或多任務處理特有的概念。它與FreeEROS相關是因為記憶體對象動态配置設定和動态記憶體配置設定方案是由目标編譯器提供,并不總是适合于實時應用。

記憶體可以使用标準C庫的 malloc() 和 free() 函數來配置設定,但在以下情況下可能并不适用:

  • 在小型嵌入式系統可能并不适用。
  • 實作方案可能會相當大,占用很多寶貴的代碼空間。
  • 很少情況下線程安全。
  • 并不具有确定性,使用大多數時間執行這個函數将會與印象其确定性。
  • 受到碎片化影響:如果存放堆的空閑RAM如果被分成互相分裂的小塊,則堆就被認為是碎片。如果堆是碎片,如果堆内單個空閑塊不夠大來包含這個塊,即使所有分裂的空閑塊總量是要配置設定的塊的很多倍,嘗試配置設定記憶體塊也将會失敗。
  • 使連結配置複雜化。
  • 如果堆空間允許使用他們自己的變量去增加記憶體,則有可能在調試時産生錯誤。

動态記憶體配置設定選項

FreeRTOS V9.0.0核心中,核心對象可以在一段時間内配置設定為靜态的,或在運作時作為動态的。早版本的FreeRTOS使用記憶體池配置設定政策。借此,不同大小的記憶體卡在編譯時預先配置設定,然後由記憶體配置設定函數傳回。盡管在實時時鐘系統中普遍這樣使用,但它被證明使許多主要問題的原因,因為對于非常小的實時系統來說它不能足夠有效的使用RAM。是以我們放棄了該政策。

Free RTOS目前把記憶體配置設定當作portable層的一個元件(與核心層相反)。不同的嵌入式系統有不同的記憶體配置設定政策和時間上的需求,這是一個普遍認知的事實。是以單一的動态記憶體配置設定算法将隻适合一些應用。此外,把動态記憶體配置設定從核心代碼中移除使得程式員可以在适當的時候使用他們獨特的解決方案。

在FreeRTOS請求RAM時,不是調用 malloc() ,而是調用pvPortMalloc() 。當RAM被釋放時,不使調用 free() 而是調用vPortFree() 。pvPortMalloc() 與标準C庫的 malloc()有相同的原型函數,而 vPortFree() 同 free() 也一樣。

pvPortMalloc() 和 vPortFree() 都是共有函數,是以在應用代碼中可以被調用。

FreeRTOS帶有5個舍利子來說明pvPortMalloc() 和 vPortFree()的應用,在本章中都有詳細說明。

五個例子分别定義在 heap_1.c, heap_2.c, heap_3.c, heap_4.c, heap_5.c,這五個檔案中,存放在FreeRTOS/Source/portable/MemMang目錄下。

範圍

本章旨在讓讀者在以下幾個方面有更好的了解:

  • FreeRTOS什麼時候配置設定RAM。
  • 五個示例記憶體配置設定方案使怎麼在FreeRTOS上實作的。
  • 選擇哪種記憶體配置設定方案。

2.2 記憶體配置設定方案

去掉堆記憶體管理器,FreeRTOS V9.0.0應用可以完全使用靜态的記憶體配置設定方法。

Heap_1

在小型專業嵌入式系統中,在排程器啟動之前隻建立任務和其他核心對象使很普遍的現象。在此情況下,在應用開始實作實時系統功能後,記憶體才開始被核心動态配置設定。是以在應用程式的生命周期中,記憶體早已經被配置設定。這就意味着選擇記憶體配置設定政策沒有考慮到任何更複雜的記憶體配置設定事件,例如決定性和碎片化,而不是隻考慮到想代碼尺寸和簡化的貢獻。

Heap_1.c 實作了一個 pvPortMalloc() 非常基礎的版本,并且沒有實作vPortFree()。那些從不删除任務和其他核心元件的應用程式建議使用 heap_1 記憶體配置設定方案。

一些商業上重要的,和安全性重要的會禁止使用動态記憶體配置設定系統有使用 heap_1 的可能。因為非決定性和記憶體的碎片化和錯誤配置設定所導緻的不确定性,重要的系統通常禁止動态記憶體配置設定。但 Heap_1 總是決定性的,并且不會使記憶體割裂。

heap_1 配置設定政策調用pvPortMalloc()把單一數組再分成更小的塊。這個數組被稱為FreeRTOS heap。

數組的總長(bytes)由FreeRTOSConfig.h中的 configTOTAL_HEAP_SIZE 定義。用此方法定義的大型數組看起來像是程式使用了很多RAM——即使數組中沒有配置設定任何記憶體。

每個建立好的任務都有一個任務控制子產品(TCB)和一個從堆中配置設定出來的棧。圖5将說明heap_1使如何像應用建立一樣細分單個數組的。

  • A 顯示的是沒有任何任務被建立時——整個數組是空的。
  • B 顯示的是有一個任務被建立後的數組。
  • C 顯示的是由3個任務被建立後的數組。
精通FreeRTOS實時時鐘核心Chapter2

Heap_2

Heap_2保留在FreeRTOS發行包中是為了向後相容。但它不建議使用在新設計中。建議使用heap_4而非heap_2,因為heap_4提供增強的設計。

Heap_2.c仍然通過再配置設定一個由configTOTAL_HEAP_SIZE定義大小的數組起作用。不像heap_2.c,它使用最佳适應算法去配置設定記憶體,它确實允許記憶體釋放。

重申,數組都是靜态申明,是以會讓記憶體應用看起來消耗量很多RAM,即使數組中沒有任何記憶體被占用。

最佳适應算法確定 pvPortMalloc() 使用最接近需求的大小配置設定記憶體。例如,考慮如下可能發生的情況:

  • 堆包含3個空閑記憶體塊,分别是5bytes,25bytes和100bytes。
  • pvPortMalloc()被調用申請20bytes的RAM.

适合請求數目的最小的空閑記憶體塊是25-bytes塊,是以 pvPortMalloc() 把25-byte塊撕成一個20bytes的塊和一個5bytes的塊。(這是一個簡化說法,因為heap_2會在堆中存放塊大小資訊,是以分開後兩個塊實際可用大小會小于25)。在傳回一個20bytes塊的指針後。新的5-byte空間仍然在以後可以被pvPortMalloc() 函數調用。

不像 heap_4.c,heap_2.c不會把相鄰的空閑塊連接配接池一個更大的整體,是以它更容易收到碎片化的影響。然而,在塊被配置設定并且随後總是以相同大小釋放的情況下,碎片化并不是問題。Heap_2 适合于那些重複建立和删除任務并為任務提供相同大小堆棧的應用。

精通FreeRTOS實時時鐘核心Chapter2

圖6說明了最佳适應算法在建立任務,删除任務和重新建立任務時時怎麼工作的。參考圖6

  1. A 顯示了在3個任務被建立後,A的空閑塊留在了數組的頂部。
  2. B 顯示了在一個任務被删除後,空閑塊在數組頂部。還有兩個小的空閑塊在之前配置設定給被删除的TCB和棧位置。
  3. C 顯示了在有一個任務被建立情況下的占用。建立任務使得調用了兩次pvPortMalloc(),一個配置設定了新的TCB,而另一個給任務配置設定了堆棧。通過使用xTaskCreate() API建立任務,在3.4章節會詳細介紹。在xTaskCreate()中會調用pvPortMalloc()。每個TCB都是相同大小,是以最佳适應算法確定之前配置設定的RAM塊可以被新任務的TCB重新利用。被配置設定給新任務的堆棧的大小和之前被删除任務的大小完全一樣,是以最佳适應算法把已删除的任務使用過的堆棧配置設定給新任務。頂部的最大未配置設定塊并未被使用。

heap_2 并不是确定性的,但是他比标準庫的 malloc() 和 free() 要更快。

Heap_3

Heap_3.c使用标準庫的 malloc() 和 free()函數,是以堆大小是由連接配接器配置,此時configTOTAL_HEAP_SIZE設定無效。

Heap_3通過暫時挂起FreeRTOS排程器使得 malloc() 和 free()線程安全。線程安全和排程器挂起都是第七章資源管理中要談到的要點。

Heap_4

像heap_1和heap_2一樣,heap_4也是通過給數組再劃分成更小的塊來工作的。正如之前的一樣,數組是靜态申明,長度由configTOTAL_HEAP_SIZE定義,是以也會導緻看起來程式消耗了大部分RAM,實際上在記憶體沒有再配置設定之前都是空的。

heap_4使用首次适應算法配置設定記憶體。不像heap_2一樣,heap_4把相鄰的空閑塊連接配接成單個更大的塊,這使得記憶體碎片化的風險最小化。

首次适應算法使得 pvPortMalloc() 使用最前面的足夠大滿去要求的記憶體空閑塊。例如,考慮如下可能發生的情況:

  • 堆含有3個空閑塊,從他們再數組中出現的順序看,分别是5bytes,200bytes和100bytes的大小。
  • pvPortMalloc()被調用請求20bytes的記憶體。

第一個能滿足RAM請求大小的空閑塊就是200bytes大小的塊,是以pvPortMalloc()把200-byte那一塊撕裂成一個20bytes的塊和一個180bytes的塊。(這裡簡化處理,因為heap_4再堆中存儲了塊大小資訊,是以實際兩塊能用的記憶體總和會小于200bytes)傳回20bytes塊指針後,新生成的180bytes塊在以後仍能被 pvPortMalloc() 請求調用。

heap_4會把相鄰的未占用的塊拼成一個更大的塊,來最小化碎片化的風險,并使其更容易被應用重複配置設定,并且釋放不同大小的空間。

精通FreeRTOS實時時鐘核心Chapter2

圖7說明了heap_4的首次适應算法怎麼完成記憶體拼接工作,記憶體是怎麼配置設定和釋放的。參考圖7:

  1. A 顯示了在3個任務被建立後數組的占用。在數組的頂部有一個大的空塊。
  2. B 顯示了一個任務被删除後數組的情況。數組頂部的打孔快保留。此外被删除的任務的TCB和堆棧所占用的地方也出現了一個空塊。注意到,不像heap_2中解釋的一樣,記憶體在TCB和堆棧被删除時就釋放了,沒有留下兩個分開的空塊,而是組合成一個更大的空塊。
  3. C 顯示了在FreeRTOS隊列被建立後的情況。隊列由xQueueCreate() API函數建立,在4.3章節将會詳細講述該API。xQueueCreate()調用pvPortMalloc() 來配置設定RAM為隊列使用。由于heap_4使用了首次适應算法,pvPortMalloc() 将會從之前沒足夠的的空記憶體塊中建立隊列。而在圖7中,就是任務被删除留下的空塊。隊列沒有占用掉該塊的所有空間,故該空間被分裂成兩個部分。未被占用的部分仍可調用pvPortMalloc()進行配置設定。
  4. D 顯示了pvPortMalloc()直接被使用者代碼調用而不是被FreeRTOS的API函數調用後的情況。使用者配置設定的塊足夠小來适合第一個空塊,這個空塊位于隊列和TCB的空間。
  5. E 顯示了随後隊列被删除的情況,隊列所占用到的記憶體被自動删除。現在,使用者配置設定的塊兩邊都是空閑記憶體。
  6. 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,那麼uint8_t數組以ucHeap形式調用,由configTOTAL_HEAP_SIZE設定大小,必須在應用代碼中申明。

文法要求把變量放在具體的記憶體位址取決于所使用的編譯器,是以需要參考你的編譯器文檔。以下給出兩個編譯器的例子:

  • 表2給出了GCC編譯器申明數組的文法,并且指定數組存放在 .my_heap 的記憶體塊中。
  • 表3給出了IAR編譯器申明數組的文法,并且把數組放在絕對位址0x20000000。
    精通FreeRTOS實時時鐘核心Chapter2

Heap_5

heap_5中使用的配置設定和釋放記憶體的算法與heap_4中的完全一緻。差別于heap_4,heap_5不限定從一個靜态申明的數組中配置設定記憶體。heap_5可以從許多個分散開的記憶體塊中配置設定記憶體。當不像是FreeRTOS運作在系統的非單個記憶體塊中時,适用于heap_5配置設定方案。

在寫程式時,heap_5是唯一在pvPortMalloc()被調用前需要預先明确初始化的記憶體配置設定方案。heap_5通過調用vPortDefineHeapRegions() API函數初始化。當用到heap_5記憶體配置設定方案時,vPortDefineHeapRegions()必須在任何核心對象(任務 隊列 信号 等)前被調用。

vPortDefineHeapRegions() API函數

vPortDefineHeapRegions()被用于設定每個分散的記憶體區域的起始位址和塊大小然後打包一起給heap_5使用。

精通FreeRTOS實時時鐘核心Chapter2

每個分開的記憶體區域都由一個HeapRegion_t類型的結構體描述。所有可用的記憶體區域作為一個HeapRegion_t結構體數組傳入vPortDefineHeapRegions()。

精通FreeRTOS實時時鐘核心Chapter2
參數名/傳回值 描述
pxHeapRegions 一個指向HeapRegion_t結構體數組起始位址的指針。數組中的每個結構體都描述了一段記憶體區域的起始位址和空間大小,而這段記憶體就可以被heap_5所用到。數組中的HeapRegion_t結構體必須通過起始位址排序;低位元組的記憶體區域所對應的HeapRegion_t結構體必須放在數列的前面。HeapRegion_t結構體數組的末位結構體的pucStartAddress必須被設定成NULL。

順便說下例子,假設記憶體表像圖8A 顯示的一樣,包含3塊分開的記憶體塊:RAM1,RAM2,RAM3。并且假設執行代碼是以隻讀的形式存放在記憶體中并且不顯示的。

精通FreeRTOS實時時鐘核心Chapter2

清單6顯示了HeapRegion_t結構體一起描述3塊RAM作為一個整體。

精通FreeRTOS實時時鐘核心Chapter2

盡管清單6正确描述了RAM,但并不是作為一個可以直接用的例子。因為它把所有RAM都留給了堆,沒有留任何空間給其他變量。

在一個工程編譯時,在編譯的連結階段會給每個變量配置設定一個RAM位址。在連接配接器能通過一個連結配置文檔描述時,例如一個連結腳本,RAM就能正常使用了。在圖8B假定連結腳本包含了RAM1中的資訊,但是沒有包含RAM2和RAM3的資訊。是以連接配接器把變量都放在了RAM1中,把RAM1裡0x0001nnnn以上的位址留給heap_5用。0x0001nnnn實際的值将取決于連接配接器實際連接配接的所有變量大小。并且連接配接器沒有用到RAM2和RAM3,那麼整個RAM2區域和RAM3區域對于heap_5都是可用的。

如果使用清單6中的代碼,由heap_5配置設定的位址在0x0001nnnn以下的記憶體就會與靜态變量重疊。為了避免這一現象,結構體數組的第一個HeapRegion_t結構體可以使用0x0001nnnn作為起始位址,而不是使用0x00010000。但是不推薦這樣做,因為:

  • 起始位址不那麼容易确定。
  • 連接配接器使用到的RAM的數目有可能在以後的版本中變化,每次都必須更新HeapRegion_t結構體中的起始位址。
  • 編譯方式未知,是以不能提醒開發者,RAM是不是被連接配接器和heap_5重複調用。

清單7給出了一種更友善和更易于維護的例子。它申明了一個ucHeap的數組。ucHeap是一個一般變量,是以它會變連接配接器成配置設定在RAM1中的資料。結構體數組的第一個HeapRegion_t結構體描述了起始位址和ucHeap的尺寸。是以,ucHeap變成了heap_5管理的記憶體的一部分。ucHeap的大小可以被增加,直到連接配接器調用的記憶體吃完了整個RAM1,正如圖8C中顯示的一樣。

精通FreeRTOS實時時鐘核心Chapter2

清單7中用到的技術的優點:

  1. 沒必要使用一個寫死作為起始位址。
  2. 在HeapRegion_t結構體使用的位址将會被連接配接器自動設定,并且總是正确的,即使在後續的版本中RAM使用數量發生變化。
  3. 對于RAM來說,heap_5和連接配接器重複使用RAM将變得不可能。
  4. 在ucHeap過大的時候,程式将不會連結。

2.3 與堆相關的實用函數

xPortGetFreeHeapSize() API函數

xPortGetFreeHeapSize() API函數在被調用時,會傳回堆中空閑位元組的數量。它可以用來充分利用堆空間。例如,如果在核心對象被建立後xPortGerFreeHeapSize()傳回2000,那麼configTOTAL_HEAL_SIZE就可以被減少到2000。

在使用heap_3時,該函數不可用。

精通FreeRTOS實時時鐘核心Chapter2

xPortGetMinimumEverFreeHeapSize() API函數

xPortGetMinimumEverFreeHeapSize() API函數傳回從FreeRTOS應用程式開始執行以來,未被配置設定的RAM的最小值。

該函數傳回的值用于訓示應用程式占用了多少堆空間。例如,如果自從應用程式啟動以來,xPortGetMinimumEverFreeHeapSize()傳回值為200,那就說明應用程式距離花光heap空間還差200bytes。

xPortGetMinimumEverFreeHeapSize()隻在heap_4和heap_5中可用。

精通FreeRTOS實時時鐘核心Chapter2
精通FreeRTOS實時時鐘核心Chapter2

配置設定記憶體失敗鈎子函數

pvPortMalloc()可以被應用程式代碼直接調用。它在每次建立一個核心對象時被FreeRTOS源代碼調用。核心對象例如任務,隊列,信号和事件組——在本書的後續章節都會講到。

就像标準庫malloc()函數一樣,如果pvPortMalloc()因為請求大小的塊不存在而不能傳回塊位址的話,那就會傳回一個NULL。如果因為開發者建立核心對象時候pvPortMalloc()被調用執行并傳回一個NULL,那麼這個核心對象就沒能被建立。

所有給出的heap配置設定方案都能配置在pvPortMalloc()傳回NULL時調用一個鈎子(回調)函數。

如果FreeRTOSConfig.h中的configUSE_MALLOC_FAILED_HOOK 被設定成1,那麼應用必須提供一個申請記憶體失敗鈎子函數,其函數名和原型在清單10中給出。在應用的合适地方都能被正常執行。

精通FreeRTOS實時時鐘核心Chapter2