FreeRTOS|【FreeRTOS】FreeRTOS之内存管理

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

  • 在小型的嵌入式系统中效率不高。
  • 会占用很多的代码空间。
  • 它们不是线程安全的。
  • 具有不确定性,每次执行的时间不同。
  • 会导致内存碎片。
  • 使链接器的配置变得复杂。
目录
文章目录
  • FreeRTOS之内存管理
  • 目录
    • 0. 【五种heap的特点】
    • 1. 【heap_1】
      • 1.1 [heap_1的特性]
      • 1.2 [heap从哪个地址开始呢?]
      • 1.3 [`__attribute__`机制]
        • 1.3.1 [什么是`__attribute__`?]
        • 1.3.2 [`__attribute__`语法格式]
        • 1.3.3 [attribute_-list]
      • 1.4 [`pvPortMalloc()`申请内存流程]
    • 2. 【heap_2】
      • 2.1 [heap_2的特性]
      • 2.2 [内存块]
      • 2.3 [内存申请流程]
        • 内存堆的初始化函数
        • 空闲内存块添加函数
        • 内存分配函数
        • 内存释放
    • 3. 【heap_3】
      • 3.1 [heap_3的特性]
        • 内存申请
        • 内存释放
    • 4. 【heap_4】
      • 4.1 [heap_4的特性]
      • 4.2 [内存申请流程]
        • 内存堆初始化函数
        • 空闲内存块的插入函数
        • 内存申请(内存分配)函数
        • 内存释放

0. 【五种heap的特点】
  • heap_1: 只申请不释放,适用于一旦创建好任务、信号量、队列就再也不会删除的应用。
  • heap_2: 使用内存块相关结构体管理内存,可以释放,但是申请内存不固定的话会产生内存碎片。
  • heap_3: 使用标准C库中的Malloc()与Free()。
  • heap_4: 在heap_2的基础上增加了内存合并功能,解决了内存碎片的问题。
  • heap_5: 在heap_4的基础上,实现了具有多段不同位置内存堆的内存。(待更新···)
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则不会溢出,可分配。同时更新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| |_________________| <内存块>

      FreeRTOS|【FreeRTOS】FreeRTOS之内存管理
      文章图片
2.3 [内存申请流程]
内存堆的初始化函数 prvHeapInit():
  1. 将内存堆ucHeap的可用起始地址pucAlignedHeap做字节对齐。
  2. 初始化结构体xStart和xEnd。此结构体是独立于内存块之外的。
  3. 把ucHeap当作一个超大的内存块,并且初始化这个内存块对应的BlockLink_t类型结构体。注意:第二步说到的结构体与内存块中的BlockLink_t不同,它是独立于内存块之外的结构体,这两个组成链表用于管理内存块。
空闲内存块添加函数 prvInsertBlockIntoFreeList():
  1. 查找要插入的空闲内存块的插入点(空闲内存块的排列是从小到大排列的)。
  2. 将内存块插入到插入点中。
FreeRTOS|【FreeRTOS】FreeRTOS之内存管理
文章图片

内存分配函数 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。
【FreeRTOS|【FreeRTOS】FreeRTOS之内存管理】FreeRTOS|【FreeRTOS】FreeRTOS之内存管理
文章图片

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记录可用内存标志位。
FreeRTOS|【FreeRTOS】FreeRTOS之内存管理
文章图片

空闲内存块的插入函数 prvInsertBlockIntoFreeList():
  1. 检查要插入的空闲内存块的插入点。
  2. 检查要插入链表中的内存块是否可以和链表中前一个内存块合并,如果可以就合并:
    假设pxIterator->xBlockSize=64,那么如果puc + pxIterator->xBlockSize == 此空闲内存块的首地址,则可以合并。
  3. 检查要插入的内存块可否与后一个内存块合并,如果可以就合并。过程与第二步类似。
FreeRTOS|【FreeRTOS】FreeRTOS之内存管理
文章图片

FreeRTOS|【FreeRTOS】FreeRTOS之内存管理
文章图片

内存申请(内存分配)函数 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()将内存块插入空闲内存链表中。
参考资料:
正点原子《FreeRTOS开发手册》
《FreeRTOS Reference Manual》

    推荐阅读