FreeRTOS 動態內存管理


以下轉載自安富萊電子: http://forum.armfly.com/forum.php

本章節為大家講解 FreeRTOS 動態內存管理,動態內存管理是 FreeRTOS 非常重要的一項功能,前面
章節講解的任務創建、 信號量、 消息隊列、 事件標志組、 互斥信號量、 軟件定時器組等需要的 RAM 空間
都是通過動態內存管理從 FreeRTOSConfig.h 文件定義的 heap 空間中申請的。
動態內存管理介紹
FreeRTOS 支持 5 種動態內存管理方案,分別通過文件 heap_1,heap_2,heap_3,heap_4 和 heap_5
實現,這 5 個文件在 FreeRTOS 軟件包中的路徑是:FreeRTOS\Source\portable\MemMang。 用戶創
建的 FreeRTOS 工程項目僅需要 5 種方式中的一種。
下面將這 5 種動態內存管理方式分別進行講解。

動態內存管理方式一 heap_1
heap_1 動態內存管理方式是五種動態內存管理方式中最簡單的,這種方式的動態內存管理一旦申請
了相應內存后,是不允許被釋放的。 盡管如此,這種方式的動態內存管理還是滿足大部分嵌入式應用的,
因為這種嵌入式應用在系統啟動階段就完成了任務創建、 事件標志組、 信號量、 消息隊列等資源的創建,
而且這些資源是整個嵌入式應用過程中一直要使用的,所以也就不需要刪除,即釋放內存。 FreeRTOS 的
動態內存大小在 FreeRTOSConfig.h 文件中進行了定義:
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) ) //單位字節
用戶通過函數 xPortGetFreeHeapSize 就能獲得 FreeRTOS 動態內存的剩余,進而可以根據剩余情況優化
動態內存的大小。 heap_1 方式的動態內存管理有以下特點:
項目應用不需要刪除任務、 信號量、 消息隊列等已經創建的資源。
具有時間確定性,即申請動態內存的時間是固定的並且不會產生內存碎片。
確切的說這是一種靜態內存分配,因為申請的內存是不允許被釋放掉的。
動態內存管理方式二 heap_2
與 heap_1 動態內存管理方式不同,heap_2 動態內存管理利用了最適應算法,並且支持內存釋放。
但是 heap_2 不支持內存碎片整理,動態內存管理方式四 heap_4 支持內存碎片整理。 FreeRTOS 的動態
內存大小在 FreeRTOSConfig.h 文件中進行了定義:
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) ) //單位字節
用戶通過函數 xPortGetFreeHeapSize 就能獲得 FreeRTOS 動態內存的剩余,但是不提供動態內存是
如何被分配成各個小內存塊的信息。 另外,就是用戶可以根據剩余情況優化動態內存的大小。 heap_2 方
式的動態內存管理有以下特點:
不考慮內存碎片的情況下,這種方式支持重復的任務、 信號量、 事件標志組、 軟件定時器等內部資源
的創建和刪除。
如果用戶申請和釋放的動態內存大小是隨機的,不建議采用這種動態內存管理方式,比如:
項目應用中需要重復的創建和刪除任務,如果每次創建需要動態內存大小相同,那么 heap_2 比
較適合,但每次創建需要動態內存大小不同,那么方式 heap_2 就不合適了,因為容易產生內存
碎片,內存碎片過多的話會導致無法申請出一個大的內存塊出來,這種情況使用 heap_4 比較合
適。
項目應用中需要重復的創建和刪除消息隊列,也會出現類似上面的情況,這種情況下使用 heap_4
比較合適。
直接的調用函數 pvPortMalloc() 和 vPortFree()也容易出現內存碎片。 如果用戶按一定順序成
對的申請和釋放,基本沒有內存碎片的,而不按順序的隨機申請和釋放容易產生內存碎片。
如果用戶隨機的創建和刪除任務、 消息隊列、 事件標志組、 信號量等內部資源也容易出現內存碎片。
heap_2 方式實現的動態內存申請不具有時間確定性,但是比 C 庫中的 malloc 函數效率要高。
大部分需要動態內存申請和釋放的小型實時系統項目可以使用 heap_2。 如果需要內存碎片的回收機
制可以使用 heap_4。
動態內存管理方式三 heap_3
這種方式實現的動態內存管理是對編譯器提供的 malloc 和 free 函數進行了封裝,保證是線程安全的。
heap_3 方式的動態內存管理有以下特點:
需要編譯器提供 malloc 和 free 函數。
不具有時間確定性,即申請動態內存的時間不是固定的。
增加 RTOS 內核的代碼量。
另外要特別注意一點,這種方式的動態內存申請和釋放不是用的 FreeRTOSConfig.h 文件中定義的
heap空間大小,而是用的編譯器設置的heap空間大小或者說STM32啟動代碼中設置的heap空間大小,
比如 MDK 版本的 STM32F103 工程中 heap 大小就是在這里進行的定義:

動態內存管理方式四 heap_4
與 heap_2 動態內存管理方式不同,heap_4 動態內存管理利用了最適應算法,且支持內存碎片的回
收並將其整理為一個大的內存塊。 FreeRTOS 的動態內存大小在 FreeRTOSConfig.h 文件中進行了定義:
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) ) //單位字節
heap_4 同時支持將動態內存設置在指定的 RAM 空間位置。
用戶通過函數 xPortGetFreeHeapSize 就能獲得 FreeRTOS 動態內存的剩余,但是不提供動態內存是
如何被分配成各個小內存塊的信息。 使用函數 xPortGetMinimumEverFreeHeapSize 能夠獲取從系統啟
動到當前時刻的動態內存最小剩余,從而用戶就可以根據剩余情況優化動態內存的大小。 heap_4 方式的
動態內存管理有以下特點:
可以用於需要重復的創建和刪任務、 信號量、 事件標志組、 軟件定時器等內部資源的場合。
隨機的調用 pvPortMalloc() 和 vPortFree(),且每次申請的大小都不同,也不會像 heap_2 那樣產
生很多的內存碎片。
不具有時間確定性,即申請動態內存的時間不是確定的,但是比 C 庫中的 malloc 函數要高效。
heap_4 比較實用,本教程配套的所有例子都是用的這種方式的動態內存管理,用戶的代碼也可以直
接調用函數 pvPortMalloc() 和 vPortFree()進行動態內存的申請和釋放。
動態內存管理方式五 heap_5
有時候我們希望 FreeRTOSConfig.h 文件中定義的 heap 空間可以采用不連續的內存區,比如我們希
望可以將其定義在內部 SRAM 一部分,外部 SRAM 一部分,此時我們就可以采用 heap_5 動態內存管理
方式。另外,heap_5 動態內存管理是在 heap_4 的基礎上實現的。
heap_5 動態內存管理是通過函數 vPortDefineHeapRegions 進行初始化的,也就是說用戶在創建任
務 FreeRTOS 的內部資源前要優先級調用這個函數 vPortDefineHeapRegions,否則是無法通過函數
pvPortMalloc 申請到動態內存的。
函數 vPortDefineHeapRegions 定義不同段的內存空間采用了下面這種結構體:

定義的時候要注意兩個問題,一個是內存段結束時要定義 NULL。另一個是內存段的地址是從低地址到高
地址排列。
用戶通過函數 xPortGetFreeHeapSize 就能獲得 FreeRTOS 動態內存的剩余,但是不提供動態內存是
如何被分配成各個小內存塊的信息。 使用函數 xPortGetMinimumEverFreeHeapSize 能夠獲取從系統啟
動到當前時刻的動態內存最小剩余,從而用戶就可以根據剩余情況優化動態內存的大小。

五種動態內存方式總結
五種動態內存管理方式簡單總結如下,實際項目中,用戶根據需要選擇合適的:
heap_1:五種方式里面最簡單的,但是申請的內存不允許釋放。
heap_2:支持動態內存的申請和釋放,但是不支持內存碎片的處理,並將其合並成一個大的內存塊。
heap_3:將編譯器自帶的 malloc 和 free 函數進行簡單的封裝,以支持線程安全,即支持多任務調
用。
heap_4:支持動態內存的申請和釋放,支持內存碎片處理,支持將動態內存設置在個固定的地址。
heap_5:在 heap_4 的基礎上支持將動態內存設置在不連續的區域上。

動態內存和靜態內存比較
靜態內存方式是從 FreeRTOS 的 V9.0.0 版本才開始有的,而我們本次教程使用的版本是 V8.2.3。所
以靜態內存方式我們暫時不做講解,等 FreeRTOS 教程版本升級時再做講解。 關於靜態內存方式和動態內
存方式的優缺點可以看官方的此貼說明:點擊查看
(制作此教程的時候,官方的 FreeRTOS V9.0.0 正式版本還沒有發布,所以采用的是當前最新的 V8.2.3)
動態內存 API 函數
動態內存的 API 函數在官方的在線版手冊上面沒有列出,其實使用也比較簡單,類似 C 庫的 malloc
和 free 函數,具體使用參看下面的實例說明。
實驗練兵場:

 聲明一個結構類型:

typedef struct Msg
{
    uint8_t  ucMessageID;
    uint16_t usData[2];
    uint32_t ulData[2];
}MSG_T;

消息隊列發送任務:

static void vTaskWork(void *pvParameters)
{
        MSG_T *ptMsg;
    uint8_t ucCount = 0;


    while(1)
    {
        if (key1_flag==1)
        {
            key1_flag=0;
        
        }
        /* K2鍵按下,向xQueue1發送數據 */
        if(key2_flag==1)
        {
                    key2_flag=0;
                    printf("=================================================\r\n");
                    printf("當前動態內存大小 = %d\r\n", xPortGetFreeHeapSize());
                    ptMsg = (MSG_T  *)pvPortMalloc(sizeof(MSG_T));
                 // ptMsg = (MSG_T  *)pvPortMalloc(32);
                    printf("申請動態內存后剩余大小 = %d\r\n", xPortGetFreeHeapSize());
                
                    ptMsg->ucMessageID = ucCount++; 
                    ptMsg->ulData[0] = ucCount++;
                    ptMsg->usData[0] = ucCount++;
                
                    /* 使用消息隊列實現指針變量的傳遞 */
                    if(xQueueSend(xQueue1,                  /* 消息隊列句柄 */
                                 (void *) &ptMsg,           /* 發送結構體指針變量ptMsg的地址 */
                                 (TickType_t)10) != pdPASS )
                    {
                        /* 發送失敗,即使等待了10個時鍾節拍 */
                        printf("K2鍵按下,向xQueue2發送數據失敗,即使等待了10個時鍾節拍\r\n");
                        vPortFree(ptMsg);
                        printf("釋放申請的動態內存后大小 = %d\r\n", xPortGetFreeHeapSize());
                    }
                    else
                    {
                        /* 發送成功 */
                        printf("K2鍵按下,向xQueue2發送數據成功\r\n");
                        /* 由於是低優先級任務向高優先級任務發送消息隊列,如果成功的話說明高優先級任務已經執行。
                           並獲得了消息隊列中的數據,所以我們可以在此處釋放動態內存,不會出現高優先級任務還沒有
                           獲得消息隊列數據,我們就將動態內存釋放掉了。
                        */                        
                        vPortFree(ptMsg);
                        printf("釋放申請的動態內存后大小 = %d\r\n", xPortGetFreeHeapSize());                        
                    }
                //    TIM_Mode_Config();
            
            
        }
    
        vTaskDelay(200);
    }
}

接收任務:

void vTaskBeep(void *pvParameters)
{
    MSG_T *ptMsg;
    BaseType_t xResult;
    const TickType_t xMaxBlockTime = pdMS_TO_TICKS(500); /* 設置最大等待時間為500ms */
    
    while(1)
    {
            xResult = xQueueReceive(xQueue1,                   /* 消息隊列句柄 */
                                                            (void *)&ptMsg,             /* 這里獲取的是結構體的地址 */
                                                            (TickType_t)xMaxBlockTime);/* 設置阻塞時間 */
            
            
            if(xResult == pdPASS)
            {
                /* 成功接收,並通過串口將數據打印出來 */
                printf("接收到消息隊列數據ptMsg->ucMessageID = %d\r\n", ptMsg->ucMessageID);
                printf("接收到消息隊列數據ptMsg->ulData[0] = %d\r\n", ptMsg->ulData[0]);
                printf("接收到消息隊列數據ptMsg->usData[0] = %d\r\n", ptMsg->usData[0]);
            }
            else
            {
                    /* 超時 */
                    BEEP_TOGGLE;
            }
    }                    
}

實驗現象展示:

那么問題就來了:

typedef struct Msg
{
  uint8_t ucMessageID;
  uint16_t usData[2];
  uint32_t ulData[2];
}MSG_T;

這個結構體類型無論是在4字節對齊還是8字節對齊的編譯器上,輸出都是16字節。keil默認4字節對齊。

申請之前,顯示:當前動態內存大小 = 23480

申請之后,顯示:申請動態內存后剩余大小 = 23456

奇怪了,怎么會減少了24個字節呢?明明只申請了16字節啊。

heap_4文件也就是我們所有實驗使用的堆內存文件,它使用一個鏈表結構來跟蹤記錄空閑內存塊。結構體定義為:

typedef struct A_BLOCK_LINK  

{  

  struct A_BLOCK_LINK *pxNextFreeBlock;   /*指向列表中下一個空閑塊*/  

  size_t xBlockSize;                      /*當前空閑塊的大小,包括鏈表結構大小*/  

} BlockLink_t;

與第二種內存管理策略一樣,空閑內存塊也是以單鏈表的形式組織起來的,BlockLink_t類型的局部靜態變量xStart表示鏈表頭,但第四種內存管理策略的鏈表尾保存在內存堆空間最后位置,並使用BlockLink_t指針類型局部靜態變量pxEnd指向這個區域(第二種內存管理策略使用靜態變量xEnd表示鏈表尾),如下圖所示。
第四種內存管理策略和第二種內存管理策略還有一個很大的不同是:第四種內存管理策略的空閑塊鏈表不是以內存塊大小為存儲順序,而是以內存塊起始地址大小為存儲順序,地址小的在前,地址大的在后。這也是為了適應合並算法而作的改變。

整個有效空間組成唯一一個空閑塊,在空閑塊的起始位置放置了一個鏈表結構,用於存儲這個空閑塊的大小和下一個空閑塊的地址。由於目前只有一個空閑塊,所以空閑塊的pxNextFreeBlock指向指針pxEnd指向的位置,而鏈表xStart結構的pxNextFreeBlock指向空閑塊。xStart表示鏈表頭,pxEnd指向位置表示鏈表尾。
        當申請x字節內存時,實際上不僅需要分配x字節內存,還要分配一個BlockLink_t類型結構體空間,用於描述這個內存塊,結構體空間位於空閑內存塊的最開始處。當然,申請的內存大小和BlockLink_t類型結構體大小都要向上擴大到對齊字節數的整數倍。

這個擴展怎么理解呢?我們的keil默認是4字節對齊的,那么內存地址開始處,其值一定要能被4整除,例如一個地址現在是0x01,那么我們存放一個int四字節的變量,並不能從地址0x01處開始,而必須地址擴展到0x04,這樣才可以整除4.這個在C語言中已經做過分析。有了這個之后,我們看源碼知道,還需要把BlockLink_t類型的結構放在我們申請的內存開始處,這就證明了,我們的消耗的堆內存,是等於字節對齊要求之后,BlockLink_t類型結構占用的字節  +   申請的字節數。

BlockLink_t類型結構的元素,第一個是個指針,在keil編譯器中,一個指針4個字節,第二個是個size_t類型的元素,siez_t在我們使用的環境下,是unsigned int的別名,也是占用4個字節,這樣,相當於我們實際消耗的堆內存,是申請的內存經過字節對齊之后,再加上8個字節的大小的。

現在舉例說明:

 現在我把申請ptMsg = (MSG_T  *)pvPortMalloc(sizeof(MSG_T));換成:

ptMsg = (MSG_T  *)pvPortMalloc(30);

輸出如下:

不是說申請內存加8字節碼?這里為什么還差2字節,申請前23480,申請后23440.注意,我前面說的是內存對齊之后,再加上8字節,我申請30字節的內存,會被擴展成32字節,這樣才能滿足四字節對齊的要求。

我們再測試,把申請的內存換成ptMsg = (MSG_T  *)pvPortMalloc(32);這樣的輸出,必然和上面一樣:

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM