1. 程式人生 > >FreeRTOS(19)---FreeRTOS 記憶體管理分析

FreeRTOS(19)---FreeRTOS 記憶體管理分析

FreeRTOS 記憶體管理分析

FreeRTOS 記憶體管理分析

記憶體管理對應用程式和作業系統來說都非常重要。現在很多的程式漏洞和執行崩潰都和記憶體分配使用錯誤有關。

FreeRTOS作業系統將核心與記憶體管理分開實現,作業系統核心僅規定了必要的記憶體管理函式原型,而不關心這些記憶體管理函式是如何實現的。這樣做大有好處,可以增加系統的靈活性:不同的應用場合可以使用不同的記憶體分配實現,選擇對自己更有利的記憶體管理策略。比如對於安全型的嵌入式系統,通常不允許動態記憶體分配,那麼可以採用非常簡單的記憶體管理策略,一經申請的記憶體,甚至不允許被釋放。在滿足設計要求的前提下,系統越簡單越容易做的更安全。再比如一些複雜應用,要求動態的申請、釋放記憶體操作,那麼也可以設計出相對複雜的記憶體管理策略,允許動態分配和動態釋放。

FreeRTOS核心規定的幾個記憶體管理函式原型為:

  1. void *pvPortMalloc( size_t xSize ) :記憶體申請函式
  2. void vPortFree( void *pv ) :記憶體釋放函式
  3. void vPortInitialiseBlocks( void ) :初始化記憶體堆函式
  4. size_t xPortGetFreeHeapSize( void ) :獲取當前未分配的記憶體堆大小
  5. size_t xPortGetMinimumEverFreeHeapSize( void ):獲取未分配的記憶體堆歷史最小值

FreeRTOS提供了5種記憶體管理實現,有簡單也有複雜的,可以應用於絕大多數場合。它們位於下載包目錄…\FreeRTOS\Source\portable\MemMang中,檔名分別為:heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c。我在

《FreeRTOS(2)—FreeRTOS 記憶體管理》這篇文章中介紹了這5種記憶體管理的特性以及各自應用的場合,今天我們要分析它們的實現方法。

FreeRTOS提供的記憶體管理都是從記憶體堆中分配記憶體的。預設情況下,FreeRTOS核心建立任務、佇列、訊號量、事件組、軟體定時器都是藉助記憶體管理函式從記憶體堆中分配記憶體。最新的FreeRTOS版本(V9.0.0及其以上版本)可以完全使用靜態記憶體分配方法,也就是不使用任何記憶體堆。

對於heap_1.c、heap_2.c和heap_4.c這三種記憶體管理策略,記憶體堆實際上是一個很大的陣列,定義為:

static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];

其中巨集configTOTAL_HEAP_SIZE用來定義記憶體堆的大小,這個巨集在FreeRTOSConfig.h中設定。

對於heap_3.c,這種策略只是簡單的包裝了標準庫中的malloc()和free()函式,包裝後的malloc()和free()函式具備執行緒保護。因此,記憶體堆需要通過編譯器或者啟動檔案設定堆空間。

heap_5.c比較有趣,它允許程式設定多個非連續記憶體堆,比如需要快速訪問的記憶體堆設定在片內RAM,稍微慢速訪問的記憶體堆設定在外部RAM。每個記憶體堆的起始地址和大小由應用程式設計者定義。

heap_1.c

這是5個記憶體管理策略中最簡單的一個,我們稱為第一個記憶體管理策略,它簡單到只能申請記憶體。是的,跟你想的一樣,一旦申請成功後,這塊記憶體再也不能被釋放。對於大多數嵌入式系統,特別是對安全要求高的嵌入式系統,這種記憶體管理策略很有用,因為對系統軟體來說,邏輯越簡單越容易兼顧安全。實際上,大多數的嵌入式系統並不需要動態刪除任務、訊號量、佇列等,而是在初始化的時候一次性建立好,便一直使用,永遠不用刪除。所以這個記憶體管理策略實現簡潔、安全可靠,使用的非常廣泛。我對這個對記憶體管理策略也情有獨鍾。

我們可以將第一種記憶體管理看作是切面包:初始化的記憶體就像一根完整的長棍麵包,每次申請記憶體,就從一端切下適當長度的麵包返還給申請者,直到麵包被分配完畢,就這麼簡單。

這個記憶體管理策略使用兩個區域性靜態變數來跟蹤記憶體分配,變數定義為:

static size_t xNextFreeByte = ( size_t ) 0;
static uint8_t *pucAlignedHeap = NULL;

其中,變數xNextFreeByte記錄已經分配的記憶體大小,用來定位下一個空閒的記憶體堆位置。因為記憶體堆實際上是一個大陣列,我們只需要知道已分配記憶體的大小,就可以用它作為偏移量找到未分配記憶體的起始地址。變數xNextFreeByte被初始化為0,然後每次申請記憶體成功後,都會增加申請記憶體的位元組數目。

變數pucAlignedHeap指向對齊後的記憶體堆起始位置。為什麼要對齊?這是因為大多數硬體訪問記憶體對齊的資料速度會更快。為了提高效能,FreeRTOS會進行對齊操作,不同的硬體架構對齊操作也不盡相同,對於Cortex-M3架構,進行8位元組對齊。
我們來看一下第一種記憶體管理策略對外提供的API函式。

記憶體申請:pvPortMalloc()

函式原始碼為:

void *pvPortMalloc( size_t xWantedSize )
{
void *pvReturn = NULL;
static uint8_t *pucAlignedHeap = NULL;


    /* 確保申請的位元組數是對齊位元組數的倍數 */
    #if( portBYTE_ALIGNMENT != 1 )
    {
        if( xWantedSize & portBYTE_ALIGNMENT_MASK )
        {
            xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
        }
    }
    #endif


    vTaskSuspendAll();
    {
        if( pucAlignedHeap == NULL )
        {
            /* 第一次使用,確保記憶體堆起始位置正確對齊 */
            pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) &ucHeap[ portBYTE_ALIGNMENT ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
        }


        /* 邊界檢查,變數xNextFreeByte是區域性靜態變數,初始值為0 */
        if( ( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE ) &&
            ( ( xNextFreeByte + xWantedSize ) > xNextFreeByte ) )
        {
            /* 返回申請的記憶體起始地址並更新索引 */
            pvReturn = pucAlignedHeap + xNextFreeByte;
            xNextFreeByte += xWantedSize;
        }
    }
    ( void ) xTaskResumeAll();


    #if( configUSE_MALLOC_FAILED_HOOK == 1 )
    {
        if( pvReturn == NULL )
        {
            extern void vApplicationMallocFailedHook( void );
            vApplicationMallocFailedHook();
        }
    }
    #endif


    return pvReturn;
}

函式一開始會將申請的記憶體數量調整到對齊位元組數的整數倍,所以實際分配的記憶體空間可能比申請記憶體大。比如對於8位元組對齊的系統,申請11位元組記憶體,經過對齊後,實際分配的記憶體是16位元組(8的整數倍)。

接下來會掛起所有任務,因為記憶體申請是不可重入的(使用了靜態變數)。

如果是第一次執行這個函式,需要將變數pucAlignedHeap指向記憶體堆區域第一個地址對齊處。我們上面說記憶體堆其實是一個大陣列,編譯器為這個陣列分配的起始地址是隨機的,可能不符合我們的對齊需要,這時候要進行調整。比如記憶體堆陣列ucHeap從RAM地址0x10002003處開始,系統按照8位元組對齊,則對齊後的記憶體堆如圖1-1所示:

在這裡插入圖片描述
圖1-1:記憶體堆大小與地址對齊示意圖
之後進行邊界檢查,檢視剩餘的記憶體堆是否夠分配,檢查xNextFreeByte + xWantedSize是否溢位。如果檢查通過,則為申請者返回有效的記憶體指標並更新已分配記憶體數量計數器xNextFreeByte(從指標pucAlignedHeap開始,偏移量為xNextFreeByte處的記憶體區域為未分配的記憶體堆起始位置)。比如我們首次呼叫記憶體分配函式pvPortMalloc(20),申請20位元組記憶體。根據對齊原則,我們會實際申請到24位元組記憶體,申請成功後,記憶體堆示意圖如圖1-2所示。
在這裡插入圖片描述
圖1-2:第一次分配記憶體後的記憶體堆空間示意圖

記憶體分配完成後,不管有沒有分配成功都恢復之前掛起的排程器。

如果記憶體分配不成功,這裡最可能是記憶體堆空間不夠用了,會呼叫一個鉤子函式vApplicationMallocFailedHook()。這個鉤子函式由應用程式提供,通常我們可以列印記憶體分配裝置資訊或者點亮也故障指示燈。

獲取當前未分配的記憶體堆大小:xPortGetFreeHeapSize()

函式用於返回未分配的記憶體堆大小。這個函式也很有用,通常用於檢查我們設定的記憶體堆是否合理,通過這個函式我們可以估計出最壞情況下需要多大的記憶體堆,以便合理的節省RAM。

對於第一個記憶體管理策略,這個函式實現十分簡單,原始碼如下:

size_t xPortGetFreeHeapSize( void )
{
    return ( configADJUSTED_HEAP_SIZE - xNextFreeByte );
}

從圖1-1和圖1-2我們知道,巨集configADJUSTED_HEAP_SIZE表示記憶體堆有效的大小,這個值減去已經分配出去的記憶體大小,正是我們需要的未分配的記憶體堆大小。

其它函式

第一個記憶體管理策略中還有兩個函式:vPortFree()和vPortInitialiseBlocks()。但實際上第一個函式什麼也不做;第二個函式僅僅將靜態區域性變數xNextFreeByte設定為0。

heap_2.c

第二種記憶體管理策略要比第一種記憶體管理策略複雜,它使用一個最佳匹配演算法,允許釋放之前已分配的記憶體塊,但是它不會把相鄰的空閒塊合成一個更大的塊(換句話說,這會造成記憶體碎片)。

這個記憶體管理策略用於重複的分配和刪除具有相同堆疊空間的任務、佇列、訊號量、互斥量等等,並且不考慮記憶體碎片的應用程式,不適用於分配和釋放隨機位元組堆疊空間的應用程式!

與第一種記憶體管理策略一樣,記憶體堆仍然是一個大陣列,定義為:

static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];

區域性靜態變數pucAlignedHeap指向對齊後的記憶體堆起始位置。地址對齊的原因在第一種記憶體管理策略中已經說明。假如記憶體堆陣列ucHeap從RAM地址0x10002003處開始,系統按照8位元組對齊,則對齊後的記憶體堆與第一個記憶體管理策略一樣,如圖2-1所示:
在這裡插入圖片描述
圖2-1:記憶體堆示大小與地址對齊示意圖

記憶體申請:pvPortMalloc()

與第一種記憶體管理策略不同,第二種記憶體管理策略使用一個連結串列結構來跟蹤記錄空閒記憶體塊,將空閒塊組成一個連結串列。結構體定義為:
typedef struct A_BLOCK_LINK
{
struct A_BLOCK_LINK *pxNextFreeBlock; /指向列表中下一個空閒塊/
size_t xBlockSize; /當前空閒塊的大小,包括連結串列結構大小/
} BlockLink_t;
兩個BlockLink_t型別的區域性靜態變數xStart和xEnd用來標識空閒記憶體塊的起始和結束。剛開始時,整個記憶體堆有效空間就是一個空閒塊,如圖2-2所示。因為要包含的資訊越來越多,我們必須捨棄一些資訊,捨棄的資訊可以在上一幅圖中找到。
在這裡插入圖片描述
圖2-2:記憶體堆初始化示意圖

圖2-2中的pvReturn是我自己增加的,用於接下來分析記憶體申請操作,堆疊初始化並沒有這個變數,也沒有對其操作的程式碼。從圖2-2中可以看出,整個有效空間組成唯一一個空閒塊,在空閒塊的起始位置放置了一個連結串列結構,用於儲存這個空閒塊的大小和下一個空閒塊的地址。由於目前只有一個空閒塊,所以空閒塊的pxNextFreeBlock指向連結串列xEnd,而連結串列xStart結構的pxNextFreeBlock指向空閒塊。這樣,xStart、空閒塊和xEnd組成一個單鏈表,xStart表示連結串列頭,xEnd表示連結串列尾。隨著記憶體申請和釋放,空閒塊可能會越來越多,但它們仍是以xStart連結串列開頭以xEnd連結串列結尾,根據空閒塊的大小排序,小的在前,大的在後,我們在記憶體釋放一節中會給出示意圖。

當申請N位元組記憶體時,實際上不僅需要分配N位元組記憶體,還要分配一個BlockLink_t型別結構體空間,用於描述這個記憶體塊,結構體空間位於空閒記憶體塊的最開始處。當然,和第一種記憶體管理策略一樣,申請的記憶體大小和BlockLink_t型別結構體大小都要向上擴大到對齊位元組數的整數倍。

我們看一下記憶體申請過程:首先計算實際要分配的記憶體大小,判斷申請的記憶體是否合法。如果合法則從連結串列頭xStart開始查詢,如果某個空閒塊的xBlockSize欄位大小能容得下要申請的記憶體,則從這塊記憶體取出合適的部分返回給申請者,剩下的記憶體塊組成一個新的空閒塊,按照空閒塊的大小順序插入到空閒塊連結串列中,小塊在前大塊在後。注意,返回的記憶體中不包括連結串列結構,而是緊鄰連結串列結構(經過對齊)後面的位置。舉個例子,如圖2-2所示的記憶體堆,當呼叫申請記憶體函式,如果記憶體堆空間足夠大,就將pvReturn指向的地址返回給申請者,而不是靜態變數pucAlignedHeap指向的記憶體堆起始位置!

當多次呼叫記憶體申請函式後(沒有呼叫記憶體釋放函式),記憶體堆結構如圖2-3所示。注意圖中的pvReturn仍是我自己增加上去的,pvReturn指向的位置返回給申請者。後面我們講記憶體釋放時,就是根據這個地址完成記憶體釋放工作的。
在這裡插入圖片描述
圖2-3:經過兩次記憶體分配後的記憶體堆示意圖

有了上面的這些基礎知識,再看記憶體申請函式原始碼就比較簡單了,我把需要注意的要點以註釋的方式放在原始碼中,不再單獨對這個函式做講解,值得注意的是函式中使用的一個靜態區域性變數xFreeBytesRemaining,它用來記錄未分配的記憶體堆大小。這個變數將提供給函式xPortGetFreeHeapSize()使用,以方便使用者估算記憶體堆使用情況。

void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
static BaseType_t xHeapHasBeenInitialised = pdFALSE;
void *pvReturn = NULL;


    /* 掛起排程器 */
    vTaskSuspendAll();
    {
        /* 如果是第一次呼叫記憶體分配函式,這裡先初始化記憶體堆,如圖2-2所示 */
        if( xHeapHasBeenInitialised == pdFALSE )
        {
            prvHeapInit();
            xHeapHasBeenInitialised = pdTRUE;
        }


        /* 調整要分配的記憶體值,需要增加上鍊表結構體空間,heapSTRUCT_SIZE表示經過對齊擴充套件後的結構體大小 */
        if( xWantedSize > 0 )
        {
            xWantedSize += heapSTRUCT_SIZE;


            /* 調整實際分配的記憶體大小,向上擴大到對齊位元組數的整數倍 */
            if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0 )
            {
                xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
            }
        }
        
        if( ( xWantedSize > 0 ) && ( xWantedSize < configADJUSTED_HEAP_SIZE ) )
        {
            /* 空閒記憶體塊是按照塊的大小排序的,從連結串列頭xStart開始,小的在前大的在後,以連結串列尾xEnd結束 */
            pxPreviousBlock = &xStart;
            pxBlock = xStart.pxNextFreeBlock;
            /* 搜尋最合適的空閒塊 */
            while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
            {
                pxPreviousBlock = pxBlock;
                pxBlock = pxBlock->pxNextFreeBlock;
            }


            /* 如果搜尋到連結串列尾xEnd,說明沒有找到合適的空閒記憶體塊,否則進行下一步處理 */
            if( pxBlock != &xEnd )
            {
                /* 返回記憶體空間,注意是跳過了結構體BlockLink_t空間. */
                pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + heapSTRUCT_SIZE );


                /* 這個塊就要返回給使用者,因此它必須從空閒塊中去除. */
                pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;


                /* 如果這個塊剩餘的空間足夠多,則將它分成兩個,第一個返回給使用者,第二個作為新的空閒塊插入到空閒塊列表中去*/
                if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
                {
                    /* 去除分配出去的記憶體,在剩餘記憶體塊的起始位置放置一個連結串列結構並初始化連結串列成員 */
                    pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );


                    pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
                    pxBlock->xBlockSize = xWantedSize;


                    /* 將剩餘的空閒塊插入到空閒塊列表中,按照空閒塊的大小順序,小的在前大的在後 */
                    prvInsertBlockIntoFreeList( ( pxNewBlockLink ) );
                }
                /* 計算未分配的記憶體堆大小,注意這裡並不能包含記憶體碎片資訊 */
                xFreeBytesRemaining -= pxBlock->xBlockSize;
            }
        }


        traceMALLOC( pvReturn, xWantedSize );
    }
    ( void ) xTaskResumeAll();


    #if( configUSE_MALLOC_FAILED_HOOK == 1 )
    {   /* 如果記憶體分配失敗,呼叫鉤子函式 */
        if( pvReturn == NULL )
        {
            extern void vApplicationMallocFailedHook( void );
            vApplicationMallocFailedHook();
        }
    }
    #endif


    return pvReturn;
}

記憶體釋放:vPortFree()

因為不需要合併相鄰的空閒塊,第二種記憶體管理策略的記憶體釋放也非常簡單:根據傳入的引數找到連結串列結構,然後將這個記憶體塊插入到空閒塊列表,更新未分配的記憶體堆計數器大小,結束。因為簡單,我們直接看原始碼。

void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;


    if( pv != NULL )
    {
        /* 根據傳入的引數找到連結串列結構 */
        puc -= heapSTRUCT_SIZE;


        /* 預防某些編譯器警告 */
        pxLink = ( void * ) puc;


        vTaskSuspendAll();
        {
            /* 將這個塊新增到空閒塊列表 */
            prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
            /* 更新未分配的記憶體堆大小 */
            xFreeBytesRemaining += pxLink->xBlockSize;
            
            traceFREE( pv, pxLink->xBlockSize );
        }
        ( void ) xTaskResumeAll();
    }
}

我們舉一個例子,將圖2-3 pvReturn指向的記憶體塊釋放掉,假設(configADJUSTED_HEAP_SIZE-40)遠大於要釋放的記憶體塊大小,釋放後的記憶體堆如圖2-4所示:
在這裡插入圖片描述
圖2-4:釋放記憶體後,記憶體堆示意圖

從圖2-4我們可以看出第二種記憶體管理策略的兩個特點:第一,空閒塊是按照大小排序的;第二,相鄰的空閒塊不會組合成一個大塊。

我們再接著引申討論一下這種記憶體管理策略的優缺點。通過對記憶體申請和釋放函式原始碼分析,我們可以看出它的一個優點是速度足夠快,因為它的實現非常簡單;第二個優點是可以動態釋放記憶體。但是它的缺點也非常明顯:由於在釋放記憶體時不會將相鄰的記憶體塊合併,所以這可能造成記憶體碎片。這就對其應用的場合要求極其苛刻:第一,每次建立或釋放的任務、訊號量、佇列等必須大小相同,如果分配或釋放的記憶體是隨機的,絕對不可以用這種記憶體管理策略;第二,如果申請和釋放的順序不可預料,也很危險。舉個例子,對於一個已經初始化的10KB記憶體堆,先申請48位元組記憶體,然後釋放;再接著申請32位元組記憶體,那麼一個本來48位元組的大塊就會被分為32位元組和16位元組的小塊,如果這種情況經常發生,就會導致每個空閒塊都可能很小,最終在申請一個大塊時就會因為沒有合適的空閒塊而申請失敗(並不是因為總的空閒記憶體不足)!

獲取未分配的記憶體堆大小:xPortGetFreeHeapSize()

函式用於返回未分配的記憶體堆大小。這個函式也很有用,通常用於檢查我們設定的記憶體堆是否合理,通過這個函式我們可以估計出最壞情況下需要多大的記憶體堆,以便進行合理的節省RAM。需要注意的是,這個函式返回值並不能函式原始碼為:

size_t xPortGetFreeHeapSize( void )
{
    return xFreeBytesRemaining;
}

區域性靜態變數xFreeBytesRemaining在記憶體申請和記憶體釋放函式中多次提到,它用來動態記錄未分配的記憶體堆大小。

heap_3.c

第三種記憶體管理策略簡單的封裝了標準庫中的malloc()和free()函式,採用的封裝方式是操作記憶體前掛起排程器、完成後再恢復排程器。封裝後的malloc()和free()函式具備執行緒保護。

第一種和第二種記憶體管理策略都是通過定義一個大陣列作為記憶體堆,陣列的大小由巨集configTOTAL_HEAP_SIZE指定。第三種記憶體管理策略與前兩種不同,它不再需要通過陣列定義記憶體堆,而是需要使用編譯器設定記憶體堆空間,一般在啟動程式碼中設定。因此巨集configTOTAL_HEAP_SIZE對這種記憶體管理策略是無效的。

記憶體申請:pvPortMalloc()

void *pvPortMalloc( size_t xWantedSize )
{
void *pvReturn;


    vTaskSuspendAll();
    {
        pvReturn = malloc( xWantedSize );
        traceMALLOC( pvReturn, xWantedSize );
    }
    ( void ) xTaskResumeAll();


    #if( configUSE_MALLOC_FAILED_HOOK == 1 )
    {
        if( pvReturn == NULL )
        {
            extern void vApplicationMallocFailedHook( void );
            vApplicationMallocFailedHook();
        }
    }
    #endif


    return pvReturn;
}

記憶體釋放:vPortFree()

void vPortFree( void *pv )
{
    if( pv )
    {
        vTaskSuspendAll();
        {
            free( pv );
            traceFREE( pv, 0 );
        }
        ( void ) xTaskResumeAll();
    }
}

heap_4.c

第四種記憶體分配方法與第二種比較相似,只不過增加了一個合併演算法,將相鄰的空閒記憶體塊合併成一個大塊。
與第一種和第二種記憶體管理策略一樣,記憶體堆仍然是一個大陣列,定義為:

static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];

記憶體申請:pvPortMalloc()

和第二種記憶體管理策略一樣,它也使用一個連結串列結構來跟蹤記錄空閒記憶體塊。結構體定義為:

typedef struct A_BLOCK_LINK
{
    struct A_BLOCK_LINK *pxNextFreeBlock;   /*指向列表中下一個空閒塊*/
    size_t xBlockSize;                      /*當前空閒塊的大小,包括連結串列結構大小*/
} BlockLink_t;

與第二種記憶體管理策略一樣,空閒記憶體塊也是以單鏈表的形式組織起來的,BlockLink_t型別的區域性靜態變數xStart表示連結串列頭,但第四種記憶體管理策略的連結串列尾儲存在記憶體堆空間最後位置,並使用BlockLink_t指標型別區域性靜態變數pxEnd指向這個區域(第二種記憶體管理策略使用靜態變數xEnd表示連結串列尾),如圖4-1所示。

第四種記憶體管理策略和第二種記憶體管理策略還有一個很大的不同是:第四種記憶體管理策略的空閒塊連結串列不是以記憶體塊大小為儲存順序,而是以記憶體塊起始地址大小為儲存順序,地址小的在前,地址大的在後。這也是為了適應合併演算法而作的改變。

在這裡插入圖片描述
圖4-1:記憶體堆初始化示意圖

從圖4-1中可以看出,整個有效空間組成唯一一個空閒塊,在空閒塊的起始位置放置了一個連結串列結構,用於儲存這個空閒塊的大小和下一個空閒塊的地址。由於目前只有一個空閒塊,所以空閒塊的pxNextFreeBlock指向指標pxEnd指向的位置,而連結串列xStart結構的pxNextFreeBlock指向空閒塊。xStart表示連結串列頭,pxEnd指向位置表示連結串列尾。

當申請x位元組記憶體時,實際上不僅需要分配x位元組記憶體,還要分配一個BlockLink_t型別結構體空間,用於描述這個記憶體塊,結構體空間位於空閒記憶體塊的最開始處。當然,和第一種、第二種記憶體管理策略一樣,申請的記憶體大小和BlockLink_t型別結構體大小都要向上擴大到對齊位元組數的整數倍。

我們先說一下記憶體申請過程:首先計算實際要分配的記憶體大小,判斷申請記憶體合法性,如果合法則從連結串列頭xStart開始查詢,如果某個空閒塊的xBlockSize欄位大小能容得下要申請的記憶體,則將這塊記憶體取出合適的部分返回給申請者,剩下的記憶體塊組成一個新的空閒塊,按照空閒塊起始地址大小順序插入到空閒塊連結串列中,地址小的在前,地址大的在後。在插入到空閒塊連結串列的過程中,還會執行合併演算法:判斷這個塊是不是可以和上一個空閒塊合併成一個大塊,如果可以則合併;然後再判斷能不能和下一個空閒塊合併成一個大塊,如果可以則合併!合併演算法是第四種記憶體管理策略和第二種記憶體管理策略最大的不同!經過幾次記憶體申請和釋放後,可能的記憶體堆如圖4-2所示:
在這裡插入圖片描述
圖4-2:經過數次記憶體申請和釋放後,某個記憶體堆示意圖

有了上面的基礎,我們再來看一下原始碼,我把需要注意的要點以註釋的方式放在原始碼中,不再單獨對這個函式做講解。函式中會用到幾個區域性靜態變數在這裡簡單說明一下:

  • xFreeBytesRemaining:表示當前未分配的記憶體堆大小
  • xMinimumEverFreeBytesRemaining:表示未分配記憶體堆空間歷史最小值。這個值跟xFreeBytesRemaining有很大區別,只有記錄未分配記憶體堆的最小值,才能知道最壞情況下記憶體堆的使用情況。
  • xBlockAllocatedBit:這個變數在第一次呼叫記憶體申請函式時被初始化,將它能表示的數值的最高位置1。比如對於32位系統,這個變數被初始化為0x80000000(最高位為1)。記憶體管理策略使用這個變數來標識一個記憶體塊是否空閒。如果記憶體塊被分配出去,則記憶體塊連結串列結構成員xBlockSize按位或上這個變數(即xBlockSize最高位置1),在釋放一個記憶體塊時,會把xBlockSize的最高位清零。
void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
void *pvReturn = NULL;


    vTaskSuspendAll();
    {
        /* 如果是第一次呼叫記憶體分配函式,則初始化記憶體堆,初始化後的記憶體堆如圖4-1所示 */
        if( pxEnd == NULL )
        {
            prvHeapInit();
        }


        /* 申請的記憶體大小合法性檢查:是否過大.結構體BlockLink_t中有一個成員xBlockSize表示塊的大小,這個成員的最高位被用來標識這個塊是否空閒.因此要申請的塊大小不能使用這個位.*/
        if( ( xWantedSize & xBlockAllocatedBit ) == 0 )
        {
            /* 計算實際要分配的記憶體大小,包含連結結構體BlockLink_t在內,並且要向上位元組對齊 */
            if( xWantedSize > 0 )
            {
                xWantedSize += xHeapStructSize;


                /* 對齊操作,向上擴大到對齊位元組數的整數倍 */
                if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
                {
                    xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
                    configASSERT( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) == 0 );
                }
            }


            if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) )
            {
                /* 從連結串列xStart開始查詢,從空閒塊連結串列(按照空閒塊地址順序排列)中找出一個足夠大的空閒塊 */
                pxPreviousBlock = &xStart;
                pxBlock = xStart.pxNextFreeBlock;
                while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
                {
                    pxPreviousBlock = pxBlock;
                    pxBlock = pxBlock->pxNextFreeBlock;
                }


                /* 如果最後到達結束標識,則說明沒有合適的記憶體塊,否則,進行記憶體分配操作*/
                if( pxBlock != pxEnd )
                {
                    /* 返回分配的記憶體指標,要跳過記憶體開始處的BlockLink_t結構體 */
                    pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize );


                    /* 將已經分配出去的記憶體塊從空閒塊連結串列中刪除 */
                    pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;


                    /* 如果剩下的記憶體足夠大,則組成一個新的空閒塊 */
                    if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
                    {
                        /* 在剩餘記憶體塊的起始位置放置一個連結串列結構並初始化連結串列成員 */
                        pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );
                        configASSERT( ( ( ( size_t ) pxNewBlockLink ) & portBYTE_ALIGNMENT_MASK ) == 0 );


                        pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
                        pxBlock->xBlockSize = xWantedSize;


                        /* 將剩餘的空閒塊插入到空閒塊列表中,按照空閒塊的地址大小順序,地址小的在前,地址大的在後 */
                        prvInsertBlockIntoFreeList( pxNewBlockLink );
                    }
                    
                    /* 計算未分配的記憶體堆空間,注意這裡並不能包含記憶體碎片資訊 */
                    xFreeBytesRemaining -= pxBlock->xBlockSize;
                    
                    /* 儲存未分配記憶體堆空間歷史最小值 */
                    if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining )
                    {
                        xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;
                    }


                    /* 將已經分配的記憶體塊標識為"已分配" */
                    pxBlock->xBlockSize |= xBlockAllocatedBit;
                    pxBlock->pxNextFreeBlock = NULL;
                }
            }
        }


        traceMALLOC( pvReturn, xWantedSize );
    }
    ( void ) xTaskResumeAll();


    #if( configUSE_MALLOC_FAILED_HOOK == 1 )
    {   /* 如果記憶體分配失敗,呼叫鉤子函式 */
        if( pvReturn == NULL )
        {
            extern void vApplicationMallocFailedHook( void );
            vApplicationMallocFailedHook();
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    #endif


    configASSERT( ( ( ( size_t ) pvReturn ) & ( size_t ) portBYTE_ALIGNMENT_MASK ) == 0 );
    return pvReturn;
}

記憶體釋放:vPortFree()

第四種記憶體管理策略的記憶體釋放也比較簡單:根據傳入的引數找到連結串列結構,然後將這個記憶體塊插入到空閒塊列表,需要注意的是在插入過程中會執行合併演算法,這個我們已經在記憶體申請中講過了。最後是將這個記憶體塊標誌為“空閒”、更新未分配的記憶體堆大小,結束。原始碼如下:

void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;


    if( pv != NULL )
    {
        /* 根據引數地址找出記憶體塊連結串列結構 */
        puc -= xHeapStructSize;
        pxLink = ( void * ) puc;


        /* 檢查這個記憶體塊確實被分配出去 */
        if( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 )
        {
            if( pxLink->pxNextFreeBlock == NULL )
            {
                /* 將記憶體塊標識為"空閒" */
                pxLink->xBlockSize &= ~xBlockAllocatedBit;


                vTaskSuspendAll();
                {
                    /* 更新未分配的記憶體堆大小 */
                    xFreeBytesRemaining += pxLink->xBlockSize;
                    traceFREE( pv, pxLink->xBlockSize );
                    /* 將這個記憶體塊插入到空閒塊連結串列中,按照記憶體塊地址大小順序 */
                    prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
                }
                ( void ) xTaskResumeAll();
            }
        }
    }
}

如圖4-2所示的記憶體堆示意圖,如果我們將32位元組的“已分配空間2”釋放,由於這個記憶體塊的上面和下面都是空閒塊,所以在將它插入到空閒塊連結串列的過程在中,會先和“剩餘空閒塊1”合併,合併後的塊再和“剩餘空閒塊2”合併,這樣組成一個大的空閒塊,如圖4-3所示:

在這裡插入圖片描述
圖4-3:記憶體釋放後,會和相鄰的空閒塊合併

獲取當前未分配的記憶體堆大小:xPortGetFreeHeapSize()

在記憶體申請和記憶體釋放函式中以及多次提到過變數xFreeBytesRemaining。它就是一個計數器,不能說明記憶體堆碎片資訊。

size_t xPortGetFreeHeapSize( void )
{
    return xFreeBytesRemaining;
}

獲取未分配的記憶體堆歷史最小值:xPortGetFreeHeapSize()

在記憶體申請中講解過變數xMinimumEverFreeBytesRemaining,這個函式很有用,通過這個函式我們可以估計出最壞情況下需要多大的記憶體堆,從而輔助我們合理的設定記憶體堆大小。
size_t xPortGetMinimumEverFreeHeapSize( void ) { return xMinimumEverFreeBytesRemaining; }

heap_5.c

第五種記憶體管理策略允許記憶體堆跨越多個非連續的記憶體區,並且需要顯示的初始化記憶體堆,除此之外其它操作都和第四種記憶體管理策略十分相似。

第一、第二和第四種記憶體管理策略都是利用一個大陣列作為記憶體堆使用,並且只需要應用程式指定陣列的大小(通過巨集configTOTAL_HEAP_SIZE定義),陣列定義由記憶體管理策略實現。第五種記憶體管理策略有些不同,首先它允許跨記憶體區定義多個記憶體堆,比如在片內RAM中定義一個記憶體堆,還可以在片外RAM再定義記憶體堆;其次,使用者需要指定每個記憶體堆區域的起始地址和記憶體堆大小、將它們放在一個HeapRegion_t結構體型別陣列中,並需要在使用任何記憶體分配和釋放操作前呼叫vPortDefineHeapRegions()函式初始化這些記憶體堆。

讓我們看一個例子:假設我們為記憶體堆分配兩個記憶體塊,第一個記憶體塊大小為0x10000位元組,起始地址為0x80000000;第二個記憶體塊大小為0xa0000位元組,起始地址為0x90000000。HeapRegion_t結構體型別陣列可以定義如下:

HeapRegion_t xHeapRegions[] =
 {
  	{ ( uint8_t * ) 0x80000000UL, 0x10000 }, 
  	{ ( uint8_t * ) 0x90000000UL, 0xa0000 }, 
  	{ NULL, 0 }                
 };

兩個記憶體塊要按照地址順序放入到陣列中,地址小的在前,因此地址為0x80000000的記憶體塊必須放陣列的第一個位置。陣列必須以使用一個NULL指標和0位元組元素作為結束,以便讓記憶體管理程式知道何時結束。

定義好記憶體堆陣列後,需要應用程式呼叫vPortDefineHeapRegions()函式初始化這些記憶體堆:將它們組成一個連結串列,以xStart連結串列結構開頭,以pxEnd指標指向的位置結束。我們看一下記憶體堆陣列是如何初始化的,以上面的記憶體堆陣列為例,初始化後的記憶體堆如圖5-1所示(32為平臺,sizeof(BlockLink_t)=8位元組)。
在這裡插入圖片描述
圖5-1:多個非連續記憶體區用作記憶體堆初始化示意圖

一旦記憶體堆初始化之後,記憶體申請和釋放都和第四種記憶體管理策略相同,不再單獨分析。