【FreeRTOS】内存管理

FreeRTOS之内存管理

既然标准C库中的Malloc()与Free()也可以实现内存动态管理,为何FreeRTOS还要实现一套内存管理机制?原因如下:

  • 在小型的嵌入式系统中效率不高。
  • 会占用很多的代码空间。
  • 它们不是线程安全的。
  • 具有不确定性,每次执行的时间不同。
  • 会导致内存碎片。
  • 使链接器的配置变得复杂。

0. 【五种heap的特点】

  • heap_1: 只申请不释放,适用于一旦创建好任务、信号量、队列就再也不会删除的应用。
  • heap_2: 使用内存块相关结构体管理内存,可以释放,但是申请内存不固定的话会产生内存碎片。
  • heap_3: 使用标准C库中的Malloc()与Free()。
  • heap_4: 在heap_2的基础上增加了内存合并功能,解决了内存碎片的问题。
  • heap_5: 在heap_4的基础上,还支持管理多段不连续的内存,并将这些内存用链表连接起来。它更适用于芯片外加RAM的情况。

1. 【heap_1】

1.1 [heap_1的特性]

  • heap_1只有内存申请没有内存释放,适用于一旦创建好任务、信号量、队列就再也不会删除的应用,实际上大多数FreeRTOS应用都是这样的。
  • 具有确定性(执行所花费的时间大多数都是一样的),而且不会导致内存碎片。
  • 代码实现和内存分配过程都很简单,内存是从一个静态数组中分配的,适用于不需要动态分配内存的应用。

1.2 [heap从哪个地址开始呢?]

  • 在heap_1.c中,static uint8_t ucHeap[configTOTAL_HEAP_SIZE]分配了一个数组给堆,但是这个地址从哪里开始呢?这是由编译器(比如GCC)决定的。
  • 这个堆的首地址不一定是以8字节对齐,要想以8字节对齐需要使用gcc的__attribute__机制。

1.3 [__attribute__机制]

具体使用方法点击移步至

1.3.1 [什么是__attribute__?]

__attribute__机制是GCC的一大特色,与编译器相关。此机制可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)。

1.3.2 [__attribute__语法格式]
    __attribute__ ((attribute-list))
1.3.3 [attribute_-list]
  • 数据声明
    • packed:__attribute__ ((packed))的作用是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐,是GCC特有的语法。
    • aligned:__attribute__ ((aligned(n)))指定内存n字节对齐,n为任意整数。
  • 函数声明
    • __attribute__ (((noreturn))告诉编译器此函数不会返回给调用者,以便编译器在优化时去掉不必要的函数返回代码。
    • __attribute__ ((weak))将函数转为虚函数,弱符号,告诉编译器如果有定义同名的函数强符号函数则使用同名函数,否则使用此函数,用在防止某必要函数未被定义的情况。注意:weak属性只会在静态库(.o .a)中生效,动态库(.so)中不会生效。

1.4 [pvPortMalloc()申请内存流程]

  • 检查是否字节对齐,FreeRTOS默认8字节对齐。
    • 若对齐直接进行下一步
    • 若不对齐,将字节补齐(可以被8整除),申请的字节大小只能大于传进来的值。
  • 根据内存堆ucHeap计算出以8字节对齐的可用起始地址pucAlignedHeap
  • 检查内存够不够分配想要的大小,puAlignedHeap可用地址起始位置,
    xNextFreeByte可用位置减去可用起始位置的偏移量;xWantedSize想要分配的字节大小。xNextFreeByte+xWantedSize<configADJUSTED_HEAP_SIZE则不会溢出,可分配。同时更新xNextFreeByte
  • 返回申请到的内存首地址。

GNU C 的一大特色就是__attribute__

2. 【heap_2】

2.1 [heap_2的特性]

  • 适用于可能会重复的删除任务、队列、信号量等的应用中,要注意有内存碎片产生!
  • 有内存块结构;与heap_1相比将内存分为内存块用链表链接接起来实现内存的分配和释放,而heap_1就是一整片数组。
  • 如果分配和释放的内存n大小是随机的,不匹配的,那么就要慎重使用。此种情况使用heap_4是最好的。
  • 具有不确定性,但是也远比标准C中的malloc()free()效率高!

总结:heap_2基本上适用于大多数的需要动态分配内存的工程中,而heap_4更是具有将内存碎片合并成一个大的空闲内存块(内存碎片回收)的功能。

2.2 [内存块]

  • 同heap_1一样,heap_2整个内存堆为ucHeap[],大小为configTOTAL_HEAP_SIZE。可以通过函数xPortGetFreeHeapSize()来获取剩余的内存大小。
  • 为了实现内存释放,heap_2引入了内存块的概念,每分出去一段内存就是一个内存块,剩下的一大段内存也是一个内存块,每次分的内存块大小可以不确定。
  • 为了管理内存块还引入了一个链表结构:(这是每个内存块里面都要有的链表结构体,此结构占8个字节)
    •   typedef struct A_BLOCK_LINK
        {
            struct A_BLOCK_LINK *pxNextFreeBlock;
            size_t xBlockSize;  // 此值的最高位表示当前内存块是否被使用,1为使用,0为未使用
        } BlockLink_t;
      
       _________________  ___
      | pxNextFreeBlock |  |
      |-----------------| 8 bytes
      | xBlockSize=24   |  |
      |-----------------| _|_
      |                 |
      |     16 bytes    |
      |_________________|
            <内存块>
      
      内存块图

2.3 [内存申请流程]

内存堆的初始化函数

prvHeapInit():

  1. 将内存堆ucHeap的可用起始地址pucAlignedHeap做字节对齐。
  2. 初始化结构体xStart和xEnd。此结构体是独立于内存块之外的。
  3. 把ucHeap当作一个超大的内存块,并且初始化这个内存块对应的BlockLink_t类型结构体。注意:第二步说到的结构体与内存块中的BlockLink_t不同,它是独立于内存块之外的结构体,这两个组成链表用于管理内存块。
空闲内存块添加函数

prvInsertBlockIntoFreeList():

  1. 查找要插入的空闲内存块的插入点(空闲内存块的排列是从小到大排列的)。
  2. 将内存块插入到插入点中。

插入内存块

内存分配函数

pvPortMalloc()内部流程:

  1. 检查内存堆是否已经初始化,如果内存堆未初始化,则先调用prHeapInit()初始化内存堆。
    • 内存堆的初始化,prvHeapInit()。
  2. 检查要申请的内存大小是否大于0。而实际需要申请的大小要包含结构体BlockLink_t的大小,即xWantedSize += heapSTRUCT_SIZE,最终对xWantedSize做字节对齐。
  3. 从链表头xStart开始寻找满足要求的可用内存块。
    • 先判断需要申请的内存块大小是否合理,如果想要申请的内存大于0,且小于内存堆的容量,则从xStart第一个内存块开始遍历,找到大小满足的内存块,然后返回申请到的内存块可用首地址(此地址是跳过所选内存块结构体的地址)。
    • 判断申请到的内存块是否过大,即申请到的内存块大小减去所需大小的值超过了阈值heapMINIMUN_BLOCK_SIZE。如果内存块大于所申请的内存大小,则将内存块分割为两块,前面的内存块给应用程序使用,并将后面剩余的内存块通过函数prvInsertBlockIntoFreeList()插入到可用内存块链表中。
    • 更新内存剩余大小xFreeBytesRemaining。
内存释放

vPortFree():

  1. 调用函数prvInsertBlockIntoFreeList()将要释放的内存块添加至空闲内存块链表中。
  2. 更新变量xFreeBytesRemaining。

初始化后的内存堆

3. 【heap_3】

3.1 [heap_3的特性]

使用标准C库进行内存申请。

内存申请

pvPortMalloc():

  1. 调用函数vTaskSuspendAll()关闭任务调度器。
  2. 调用标准C库里面的malloc()函数来申请内存。
  3. 调用函数xTaskResumeAll()恢复任务调度器。
内存释放

vPortFree():

  1. 调用函数vTaskSuspendAll()关闭任务调度器。
  2. 调用标准C库里面的free()函数来释放内存。
  3. 调用函数xTaskResumeAll()恢复任务调度器。

4. 【heap_4】

4.1 [heap_4的特性]

heap_4比heap_2多了内存碎片整理再分配的特点,其余基本相同。

4.2 [内存申请流程]

内存堆初始化函数

prvHeapInit():

  1. 将内存堆ucHeap的可用起始地址pucAlignedHeap做字节对齐。
  2. 初始化结构体xStart。此结构体是独立于内存块之外的。xStart->pxNextFreeBlock指向可用首地址pucAlignedHeap。
  3. 初始化pxEnd,这里与heap有些区别:在heap_2中,pxEnd与xStart一样都是一个独立于内存块之外的结构体。但是在heap_4中,结构体指针pxEnd是指向内存块最后的一部分区域,也就是pxEnd在内存块当中。
  4. 初始化pxFirstFreeBlock。
  5. 初始化xMinimumEverFreeBytesRemaining(内存最小剩余大小)和xFreeBytesRemaining(可用内存块的大小)。
  6. 初始化xBlockAllocatedBit记录可用内存标志位。

初始化内存堆有别于heap_2

空闲内存块的插入函数

prvInsertBlockIntoFreeList():

  1. 检查要插入的空闲内存块的插入点。
  2. 检查要插入链表中的内存块是否可以和链表中前一个内存块合并,如果可以就合并:
    假设pxIterator->xBlockSize=64,那么如果puc + pxIterator->xBlockSize == 此空闲内存块的首地址,则可以合并。
  3. 检查要插入的内存块可否与后一个内存块合并,如果可以就合并。过程与第二步类似。

插入空闲内存块
合并内存块

内存申请(内存分配)函数

pvPortMalloc():

  1. 判断pvPortMalloc()是否为第一次调用,如果是则调用函数prvHeapInit()初始化内存堆。
  2. 判断所需内存大小是否满足要求,内存大小不能超过0x7fff ffff。因为xBlockSize最高位是记录内存块是否被使用的, if (xWangtedSize & xBlockAllocatedBit) == 0。表明当前内存块未使用。
  3. 从链表头xStart开始寻找满足要求的可用内存块。与heap_2基本相同,可参考heap_2。
  4. 获取返回给应用层代码的可用内存首地址,注意要跳过结构体BlockLink_t。
  5. 判断申请到的内存块是否过大,即申请到的内存块大小减去所需大小的值超过了阈值heapMINIMUN_BLOCK_SIZE。如果内存块大于所申请的内存大小,则将内存块分割为两块,前面的内存块给应用程序使用,并将后面剩余的内存块通过函数prvInsertBlockIntoFreeList()插入到可用内存块链表中。
  6. 申请到的内存块其结构体中的成员变量xBlockSize | xBlockAllocatedBit (0x8000000)。表示此内存块已经被使用。
内存释放

xPortFree():

  1. 判断要释放的内存块是否被使用。if((xBlockSize & xBlockAllocatedBit) != 0)
    • 如果正在使用,将xBlockSize最高位清零,表示将此内存块未分配 xBlockSize &= ~xBlockAllocatedBit。
    • 掉用prvInsertBlockIntoFreeList()将内存块插入空闲内存链表中。

5. 【heap_5】

5.1 [heap_5的特性]

heap_5相较于heap_4在内存堆初始化时有区别,它支持管理多段不连续的内存,并将这些内存用链表连接起来。其余的分配释放与heap_4相同。它更适用于芯片外加RAM的情况。
使用heap5在内存堆初始化时必须先调用vPortDefineHeapRegions()把多块不连续内存链接起来一块初始化。

5.2 [内存申请流程详解](附代码)

vPortDefineHeapRegions():

/* Used by heap_5.c. */
typedef struct HeapRegion
{
	uint8_t *pucStartAddress;  /* 指向内存块首地址 */
	size_t xSizeInBytes;       /* 此内存块大小 */
} HeapRegion_t;

eg:有2块内存要用heap5管理,地址0x80000000,大小0x10000,地址0x90000000,大小0xa0000,则如下定义该结构体数组,注意地址顺序要从小到大,最后要以{NULL,0}结尾(源码是以0做判断结束循环)

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

代码详解:

void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions )
{
    /* 定义将可用内存块串起来的结构体指针 */
    BlockLink_t * pxFirstFreeBlockInRegion = NULL, * pxPreviousFreeBlock;
    size_t xAlignedHeap;
    size_t xTotalRegionSize, xTotalHeapSize = 0;
    BaseType_t xDefinedRegions = 0;
    size_t xAddress;
    const HeapRegion_t * pxHeapRegion;

    /* Can only call once! */
    configASSERT( pxEnd == NULL );
    /* 获取heap区域的地址*/
    pxHeapRegion = &( pxHeapRegions[ xDefinedRegions ] );

    while( pxHeapRegion->xSizeInBytes > 0 )
    {
        xTotalRegionSize = pxHeapRegion->xSizeInBytes;

        /* 地址字节对齐调整 */
        xAddress = ( size_t ) pxHeapRegion->pucStartAddress;

        if( ( xAddress & portBYTE_ALIGNMENT_MASK ) != 0 )
        {
            xAddress += ( portBYTE_ALIGNMENT - 1 );
            xAddress &= ~portBYTE_ALIGNMENT_MASK;

            /* Adjust the size for the bytes lost to alignment. */
            xTotalRegionSize -= xAddress - ( size_t ) pxHeapRegion->pucStartAddress;
        }

        xAlignedHeap = xAddress;

        /* 如果第一次进入此循环,则设置xStart */
        if( xDefinedRegions == 0 )
        {
            /* xStart用于保存指向空闲块列表中第一项的指针。void类型转换用于防止编译器发出警告 */
            xStart.pxNextFreeBlock = ( BlockLink_t * ) xAlignedHeap;
            xStart.xBlockSize = ( size_t ) 0;
        }
        else
        {
            /* Should only get here if one region has already been added to the
             * heap. */
            configASSERT( pxEnd != NULL );

            /* 确保当前循环的内存块地址大于上次循环的内存块末端地址,确保内存是向后增长的 */
            configASSERT( xAddress > ( size_t ) pxEnd );
        }

        /* 记住前面区域中结束标记的位置(如果有的话)。 */
        pxPreviousFreeBlock = pxEnd;

        /* pxEnd is used to mark the end of the list of free blocks and is
         * inserted at the end of the region space. */
        xAddress = xAlignedHeap + xTotalRegionSize;
        xAddress -= xHeapStructSize;
        xAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
        pxEnd = ( BlockLink_t * ) xAddress;
        pxEnd->xBlockSize = 0;
        pxEnd->pxNextFreeBlock = NULL;

        /* 设置pxHeapRegions中第一个位置的内存块的大小 */
        pxFirstFreeBlockInRegion = ( BlockLink_t * ) xAlignedHeap;
        pxFirstFreeBlockInRegion->xBlockSize = xAddress - ( size_t ) pxFirstFreeBlockInRegion;
        /* 先设置下一个内存块的地址为自己的末尾地址 */
        pxFirstFreeBlockInRegion->pxNextFreeBlock = pxEnd;

        /* 如果pxPreviousFreeBlock不为NULL,当前循环不是第一次循环时进入 */
        if( pxPreviousFreeBlock != NULL )
        {
            /* 设置上个内存块中用于连接下一个内存块的地址的指针,这时pxFirstFreeBlockInRegion已经更新 */
            pxPreviousFreeBlock->pxNextFreeBlock = pxFirstFreeBlockInRegion;
        }

        /* 重置内存堆总大小*/
        xTotalHeapSize += pxFirstFreeBlockInRegion->xBlockSize;

        /* 开始第二个内存块区域的初始化 */
        xDefinedRegions++;
        pxHeapRegion = &( pxHeapRegions[ xDefinedRegions ] );
    }
    
    /* 设置内存堆最小剩余空间与当前剩余空间*/
    xMinimumEverFreeBytesRemaining = xTotalHeapSize;
    xFreeBytesRemaining = xTotalHeapSize;

    /* Check something was actually defined before it is accessed. */
    configASSERT( xTotalHeapSize );
}

参考资料:
正点原子《FreeRTOS开发手册》
《FreeRTOS Reference Manual》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值