FreeRTOS源码解析——第三章 内存管理

2023-05-16

FreeRTOS源码解析

第一章 FreeRTOS 整体架构
第二章 FreeRTOS 编程规范
第三章 FreeRTOS 内存管理
第四章 FreeRTOS 任务管理
第五章 FreeRTOS 消息队列
第六章 FreeRTOS 软件定时器
第七章 FreeRTOS 信号量
第八章 FreeRTOS 互斥量
第九章 FreeRTOS 任务通知
第十章 FreeRTOS 事件组


FreeRTOS源码解析——第三章 内存管理

  • FreeRTOS源码解析
  • 前言
  • 一、内存管理的作用
  • 二、FreeRTOS的内存策略
    • 2.1 静态内存
    • 2.2 动态内存
    • 2.3 heap文件
  • 三、heap_4.c 详解
    • 3.1 数据结构
    • 3.2 数据类型
    • 3.3 函数解析
      • 3.3.1 内存初始化 prvHeapInit
      • 3.3.2 内存申请 pvPortMalloc
      • 3.3.3 内存释放 pvPortFree
      • 3.3.4 插入节点 prvInsertBlockIntoFreeList
      • 3.3.5 其它函数
  • 总结


前言

本章主要介绍FreeRTOS的内存管理。简要说明内存管理的作用,FreeRTOS自带的五个heap文件。详细解析 heap4.c的实现。
需要具备一定的链表知识。


一、内存管理的作用

嵌入式软件开发中,有一定代码规模的软件产品,会自己进行内存管理。针对操作系统而言,内存管理也是其核心功能。基本思路是将一段已知的内存块给到对应的软件产品,作为它的内存池。内存管理将软件产品中需要操作内存的,都限制在这个内存池中。这样能在复杂的系统中,便于内存的规划和问题定位。

二、FreeRTOS的内存策略

FreeRTOS支持静态内存和动态内存,其选择取决于应用场景。

2.1 静态内存

  • 静态内存一般用在系统对可靠性要求非常高的场景。静态内存是编译器自动分配在栈区;分配时间可以认为固定为0,且大小固定。但是内存使用效率低。
  • 在FreeRTOS的内核中,静态内存接口在功能模块后面增加后缀“static”单独实现(configSUPPORT_STATIC_ALLOCATION设置为 1),需要显示的将内存传到对应的功能模块中,如下:
    TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,
                                    const char * const pcName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
                                    const uint32_t ulStackDepth,
                                    void * const pvParameters,
                                    UBaseType_t uxPriority,
                                    StackType_t * const puxStackBuffer,
                                    StaticTask_t * const pxTaskBuffer ) PRIVILEGED_FUNCTION;

puxStackBuffer 即为创建任务时,给到当前任务的静态内存。

  • 静态内存来自FreeRTOS官方的优点,翻译如下:
    * FreeRTOS对象可以放置在特定的内存位置
    * 最大的RAM空间可以在程序链接阶段确定,而不是在程序运行阶段确定
    * 应用开发者不需要关心对内存分配失败的处理(失败时编译器会报出来)
    * 可以弥补FreeRTOS不能使用动态内存的应用(FreeRTOS允许静态内存和动态内存同时存在)

2.2 动态内存

  • 动态内存一般用在系统对可靠性要求不高的场景。动态内存在运行过程中被分配,一般分配的耗时不固定。但是内存的使用效率很高。
  • 针对动态内存,FreeRTOS规定了统一的内存接口,在内核或者应用开发中都调用定好的内存接口。基于接口,用户可以使用官方的动态内存策略,也可以自己实现。这样将内存管理和FreeRTOS的内核实现通过接口完全隔离,使得系统更加的灵活。 在不同的应用场景中我们根据需要去选择不同的接口实现即可。configSUPPORT_DYNAMIC_ALLOCATION设置为 1
    接口如下:
/*
 - Map to the memory management routines required for the port.
 */
void * pvPortMalloc( size_t xSize ) ;
void vPortFree( void * pv ) ;
void vPortInitialiseBlocks( void ) ;
size_t xPortGetFreeHeapSize( void ) ;
  • 动态内存来自FreeRTOS官方的优点,翻译如下:
    - 创建一个对象时,需要的参数更少。RTOS 的API自动进行内存的分配(静态内存需要显式传入提前申请好的内存)
    - 如果对象被删除,其占用的内存可以重新被RTOS使用。可以减小应用的最大RAM需求。
    - 可以选择不同的内存算法策略或自己实现,去最好地适应应用程序。

2.3 heap文件

如果FreeRTOS是动态的创建对象,FreeRTOS认为直接使用 C标准库的 malloc 和 free 存在如下问题:

  • 它们在嵌入式系统中并不是总是可用的
  • 它们可能会消耗大量的代码空间
  • 它们不是线程安全的
  • 它们是不确定性的

FreeRTOS提供了5个 heap文件,简要说明如下:

  • heap_1.c: 只实现了 pvPortMalloc ,不允许内存的释放,申请时间确定。适用于不需要释放内存的场景,相当于是“静态内存”
  • heap_2.c: 允许内存的释放,申请内存时用了最佳匹配算法
  • heap_3.c: 只是对 malloc 和 free进行简单封装,以使的在FreeRTOS中使用时安全的
  • heap_4.c: 在heap_2的基础上,在释放内存的时候增加了相邻内存块的合并算法,减少碎片的产生
  • heap_5.c: 在heap_4的基础上,能够跨多个非相邻的内存区域。
    除了 heap_1.c ,申请内存都是不确定的,但是比标准C库要好很多。

三、heap_4.c 详解

默认大家已经掌握单向链表的知识。链表中的一个节点就代表了一个一定大小的内存块。

3.1 数据结构

核心是通过一个单向链表,将 ucHeap 这块内存池进行切割(申请内存时)和碎片合并(释放内存时)。申请内存时从链表中找到合适的节点以后从链表中删除,释放内存时将对应的节点重新插入到链表中。

/* Define the linked list structure.  This is used to link free blocks in order
 * of their memory address. */
typedef struct A_BLOCK_LINK
{
    struct A_BLOCK_LINK * pxNextFreeBlock; /*<< The next free block in the list. */
    size_t xBlockSize;                     /*<< The size of the free block. */
} BlockLink_t;


/* Allocate the memory for the heap. */
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
  • 每个内存块都有一个 BlockLink_t 作为头。单向链表只记录未被分配的内存块,已经被分配的内存通过指针(申请内存时的指针)找到,当被释放的时候重新放回链表中
  • xBlockSize 的最高位作为标记位, 其余bit记录当前内存块的大小(包括头)。pxNextFreeBlock
    在分配内存时用来进行合适的内存块的查找,在释放内存的时候进行内存块的合并。
  • 被分配的内存 ,xBlockSize 最高位为1, pxNextFreeBlock = NULL
  • 被释放的或未被使用的内存, xBlockSize 最高位为0, pxNextFreeBlock 按内存地址升序排序
  • ucHeap[ configTOTAL_HEAP_SIZE ] 为FreeRTOS能使用的动态内存池,configTOTAL_HEAP_SIZE 根据系统需要设置
  • 每个内存节点模型如下
    内存模型

3.2 数据类型

/* The size of the structure placed at the beginning of each allocated memory
 * block must by correctly byte aligned. */
static const size_t xHeapStructSize = ( sizeof( BlockLink_t ) + ( ( size_t ) ( portBYTE_ALIGNMENT - 1 ) ) ) & ~( ( size_t ) portBYTE_ALIGNMENT_MASK );

/* The size of the structure placed at the beginning of each allocated memory
 * block must by correctly byte aligned. */
static const size_t xHeapStructSize = ( sizeof( BlockLink_t ) + ( ( size_t )
/* Create a couple of list links to mark the start and end of the list. */
PRIVILEGED_DATA static BlockLink_t xStart, * pxEnd = NULL;

/* Keeps track of the number of calls to allocate and free memory as well as the
 * number of free bytes remaining, but says nothing about fragmentation. */
PRIVILEGED_DATA static size_t xFreeBytesRemaining = 0U;
PRIVILEGED_DATA static size_t xMinimumEverFreeBytesRemaining = 0U;
PRIVILEGED_DATA static size_t xNumberOfSuccessfulAllocations = 0;
PRIVILEGED_DATA static size_t xNumberOfSuccessfulFrees = 0;

/* Gets set to the top bit of an size_t type.  When this bit in the xBlockSize
 * member of an BlockLink_t structure is set then the block belongs to the
 * application.  When the bit is free the block is still part of the free heap
 * space. */
PRIVILEGED_DATA static size_t xBlockAllocatedBit = 0;
  • xHeapStructSize 为每个内存块的节点信息占用的内存大小
  • xStart、pxEnd 是链表的起点和终点,它们表征了内存块链表的起点和终点。每次对内存块链表的操作都是从 xStart起,pxEnd终。
  • 其余的静态全局变量从注释和命名可以知道是干嘛的

3.3 函数解析

3.3.1 内存初始化 prvHeapInit

  • ucHeap 作为FreeRTOS的内存池,通过对 xStart、pxEnd、pxFirstFreeBlock的初始化,创建一个表示未使用的内存块的链表。
  • prvHeapInit流程和要点如流程图所示:

内存管理-初始化

  • 源码如下:
static void prvHeapInit( void ) /* PRIVILEGED_FUNCTION */
{
    BlockLink_t * pxFirstFreeBlock;
    uint8_t * pucAlignedHeap;
    size_t uxAddress;
    size_t xTotalHeapSize = configTOTAL_HEAP_SIZE;

    /* Ensure the heap starts on a correctly aligned boundary. */
    uxAddress = ( size_t ) ucHeap;

    if( ( uxAddress & portBYTE_ALIGNMENT_MASK ) != 0 )
    {
        uxAddress += ( portBYTE_ALIGNMENT - 1 );
        uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
        xTotalHeapSize -= uxAddress - ( size_t ) ucHeap;
    }

    pucAlignedHeap = ( uint8_t * ) uxAddress;

    /* xStart is used to hold a pointer to the first item in the list of free
     * blocks.  The void cast is used to prevent compiler warnings. */
    xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
    xStart.xBlockSize = ( size_t ) 0;

    /* pxEnd is used to mark the end of the list of free blocks and is inserted
     * at the end of the heap space. */
    uxAddress = ( ( size_t ) pucAlignedHeap ) + xTotalHeapSize;
    uxAddress -= xHeapStructSize;
    uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
    pxEnd = ( void * ) uxAddress;
    pxEnd->xBlockSize = 0;
    pxEnd->pxNextFreeBlock = NULL;

    /* To start with there is a single free block that is sized to take up the
     * entire heap space, minus the space taken by pxEnd. */
    pxFirstFreeBlock = ( void * ) pucAlignedHeap;
    pxFirstFreeBlock->xBlockSize = uxAddress - ( size_t ) pxFirstFreeBlock;
    pxFirstFreeBlock->pxNextFreeBlock = pxEnd;

    /* Only one block exists - and it covers the entire usable heap space. */
    xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
    xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;

    /* Work out the position of the top bit in a size_t variable. */
    xBlockAllocatedBit = ( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 );
}

3.3.2 内存申请 pvPortMalloc

  • 内存申请是在链表中找出满足大小的节点,如果该节点内存比预期的大一定数量,则进行分割。分割出来的新节点继续加入到链表中,等待被申请。
  • pvPortMalloc 流程和要点如流程图所示(可以结合源码和注释理解):

内存管理-申请内存

  • 源码如下:
void * pvPortMalloc( size_t xWantedSize )
{
    BlockLink_t * pxBlock, * pxPreviousBlock, * pxNewBlockLink;
    void * pvReturn = NULL;

    vTaskSuspendAll();
    {
        /* If this is the first call to malloc then the heap will require
         * initialisation to setup the list of free blocks. */
        if( pxEnd == NULL )
        {
            prvHeapInit();
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }

        /* Check the requested block size is not so large that the top bit is
         * set.  The top bit of the block size member of the BlockLink_t structure
         * is used to determine who owns the block - the application or the
         * kernel, so it must be free. */
        if( ( xWantedSize & xBlockAllocatedBit ) == 0 )
        {
            /* The wanted size must be increased so it can contain a BlockLink_t
             * structure in addition to the requested amount of bytes. */
            if( ( xWantedSize > 0 ) &&
                ( ( xWantedSize + xHeapStructSize ) >  xWantedSize ) ) /* Overflow check */
            {
                xWantedSize += xHeapStructSize;

                /* Ensure that blocks are always aligned. */
                if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
                {
                    /* Byte alignment required. Check for overflow. */
                    if( ( xWantedSize + ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) ) )
                            > xWantedSize )
                    {
                        xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
                        configASSERT( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) == 0 );
                    }
                    else
                    {
                        xWantedSize = 0;
                    }
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            else
            {
                xWantedSize = 0;
            }

            if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) )
            {
                /* Traverse the list from the start (lowest address) block until
                 * one of adequate size is found. */
                pxPreviousBlock = &xStart;
                pxBlock = xStart.pxNextFreeBlock;

                while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
                {
                    pxPreviousBlock = pxBlock;
                    pxBlock = pxBlock->pxNextFreeBlock;
                }

                /* If the end marker was reached then a block of adequate size
                 * was not found. */
                if( pxBlock != pxEnd )
                {
                    /* Return the memory space pointed to - jumping over the
                     * BlockLink_t structure at its start. */
                    pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize );

                    /* This block is being returned for use so must be taken out
                     * of the list of free blocks. */
                    pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;

                    /* If the block is larger than required it can be split into
                     * two. */
                    if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
                    {
                        /* This block is to be split into two.  Create a new
                         * block following the number of bytes requested. The void
                         * cast is used to prevent byte alignment warnings from the
                         * compiler. */
                        pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );
                        configASSERT( ( ( ( size_t ) pxNewBlockLink ) & portBYTE_ALIGNMENT_MASK ) == 0 );

                        /* Calculate the sizes of two blocks split from the
                         * single block. */
                        pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
                        pxBlock->xBlockSize = xWantedSize;

                        /* Insert the new block into the list of free blocks. */
                        prvInsertBlockIntoFreeList( pxNewBlockLink );
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }

                    xFreeBytesRemaining -= pxBlock->xBlockSize;

                    if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining )
                    {
                        xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }

                    /* The block is being returned - it is allocated and owned
                     * by the application and has no "next" block. */
                    pxBlock->xBlockSize |= xBlockAllocatedBit;
                    pxBlock->pxNextFreeBlock = NULL;
                    xNumberOfSuccessfulAllocations++;
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }

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

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

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

3.3.3 内存释放 pvPortFree

  • 内存释放是将分配出去的节点,重新插入到内存块链表中。内存块节点是按地址升序排序的,为了方便在释放内存时进行内存块的合并。
  • pvPortFree流程和要点如流程图所示(可以结合源码和注释理解):

内存管理-释放内存

  • 源码如下:
void vPortFree( void * pv )
{
    uint8_t * puc = ( uint8_t * ) pv;
    BlockLink_t * pxLink;

    if( pv != NULL )
    {
        /* The memory being freed will have an BlockLink_t structure immediately
         * before it. */
        puc -= xHeapStructSize;

        /* This casting is to keep the compiler from issuing warnings. */
        pxLink = ( void * ) puc;

        /* Check the block is actually allocated. */
        configASSERT( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 );
        configASSERT( pxLink->pxNextFreeBlock == NULL );

        if( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 )
        {
            if( pxLink->pxNextFreeBlock == NULL )
            {
                /* The block is being returned to the heap - it is no longer
                 * allocated. */
                pxLink->xBlockSize &= ~xBlockAllocatedBit;

                vTaskSuspendAll();
                {
                    /* Add this block to the list of free blocks. */
                    xFreeBytesRemaining += pxLink->xBlockSize;
                    traceFREE( pv, pxLink->xBlockSize );
                    prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
                    xNumberOfSuccessfulFrees++;
                }
                ( void ) xTaskResumeAll();
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
}

3.3.4 插入节点 prvInsertBlockIntoFreeList

  • 这个函数是链表的操作,是内存管理的核心函数,在内存申请和释放都有用到。主要是实现链表的升序排序,基于升序排序进行相邻内存节点的合并,减少碎片的产生
  • 流程和要点如流程图所示(可以结合源码和注释理解):

内存管理-插入节点

  • 源码如下:
static void prvInsertBlockIntoFreeList( BlockLink_t * pxBlockToInsert ) /* PRIVILEGED_FUNCTION */
{
    BlockLink_t * pxIterator;
    uint8_t * puc;

    /* Iterate through the list until a block is found that has a higher address
     * than the block being inserted. */
    for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock )
    {
        /* Nothing to do here, just iterate to the right position. */
    }

    /* Do the block being inserted, and the block it is being inserted after
     * make a contiguous block of memory? */
    puc = ( uint8_t * ) pxIterator;

    if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert )
    {
        pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
        pxBlockToInsert = pxIterator;
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }

    /* Do the block being inserted, and the block it is being inserted before
     * make a contiguous block of memory? */
    puc = ( uint8_t * ) pxBlockToInsert;

    if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock )
    {
        if( pxIterator->pxNextFreeBlock != pxEnd )
        {
            /* Form one big block from the two blocks. */
            pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize;
            pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock;
        }
        else
        {
            pxBlockToInsert->pxNextFreeBlock = pxEnd;
        }
    }
    else
    {
        pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
    }

    /* If the block being inserted plugged a gab, so was merged with the block
     * before and the block after, then it's pxNextFreeBlock pointer will have
     * already been set, and should not be set here as that would make it point
     * to itself. */
    if( pxIterator != pxBlockToInsert )
    {
        pxIterator->pxNextFreeBlock = pxBlockToInsert;
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }
}

3.3.5 其它函数

  • size_t xPortGetFreeHeapSize( void ) : 获取FreeRTOS系统中剩下的空闲内存的大小
  • xPortGetMinimumEverFreeHeapSize : 获取FreeRTOS系统中动态分配的最小内存块的大小
  • void vPortGetHeapStats( HeapStats_t * pxHeapStats ) : 获取当前FreeRTOS 动态内存堆的状态,HeapStats_t 由很多的变量组成,记录内存管理中的一些信息。比如当前空闲内存链表的内存大小,节点中的最大,最小内存等。对于动态内存的申请或者调试都是很有用的。

总结

  1. pvPortMalloc时分割的内存块,不会发生合并,直接走插入流程 (理论就是这样:分割下来的是一块大内存的后面,只能和后面的 合并,如果能合并就不会分开)
  2. 只有在 pvPortFree的时候才会发生内存合并
  3. xPortGetFreeHeapSize 和 vPortGetHeapStats 对debug内存问题有一定的帮助。 比如可以调用他们确认系统中有无内存泄露(有泄露的话空闲内存量会一直减小)
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

FreeRTOS源码解析——第三章 内存管理 的相关文章

随机推荐