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为任意整数。
- packed:
- 函数声明
__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():
- 将内存堆ucHeap的可用起始地址pucAlignedHeap做字节对齐。
- 初始化结构体xStart和xEnd。此结构体是独立于内存块之外的。
- 把ucHeap当作一个超大的内存块