登录后台

页面导航

本文编写于 256 天前,最后修改于 256 天前,其中某些信息可能已经过时。

堆空间管理

FreeRTOS相关的动态内存分配

自v9.0.0内核以来,对象就可以在编译时静态分配,也可以运行时动态分配,例如任务,队列,信号量和事件组。为了让FreeRTOS能尽量使用简单,这些内核对象都不是在编译的时候静态分配,而是在运行时动态分配;每创建一个内核对象FreeRTOS就会分配相应RAM空间,每次删除对象时又会释放RAM空间。这个准则减少了设计和规划的工作,简单化接口函数,最小化RAM的占用。
动态内存分配是一个C语言的概念,而不是FreeRTOS或多任务特有的概念。但它却和FreeRTOS相关,因为内核对象是动态分配的,通常的编译器提供的动态内存分配一般都不适用于实时系统。

可以用标准C语言的malloc()和free()分配和释放内存,但可能不是很合适或者说恰当,原因如下:

  • 它们可能在小的嵌入式系统中不可用
  • 它们相关实现很大,需要很大的代码空间
  • 它们不是线程安全的
  • 它们是不确定的;不同情况下执行这个函数的时间差别很大
    容易引起碎片化
  • 它由编译链接器配置
  • 如果堆空间允许生长,占用其他变量空间,会导致调试错误变困难

动态内存管理选项

自V9.0.0以来就可以在编译时静态分配,也可以运行时动态分配。早期的FreeRTOS使用一个内存池进行分配调度,内存池中不同大小的内存块编译时就已经预先分配好,然后由内存分配函数返回。尽管它是实时系统的共用调度器,它用于给很多请求提供支持,然而它不能在小型嵌入式系统中足够高效的使用RAM空间,所以它被废弃了。
现在的FreeRTOS将内存分配,作为portable层的一部分(之前是属于内核层)。这是由于认识到,不同的嵌入式系统有不同动态内存分配和计时需求,一次一个单独动态内存分配算法只能适应部分程序。因此从内核的基础代码中移出了动态内存分配代码,而是让程序员选择合适的专属动态内存分配算法实现。
当FreeRTOS需要RAM,不需要调用malloc(),而是调用pvPortMalloc()。当准备释放时用vPortFree()代替free()。pvPortMalloc()函数像标准C语言的malloc()函数有相同的原型,vPortFree()也和free()有相同的原型。
pvPortMalloc()和vPortFree()是公用函数,因此可以在你的程序中直接调用。
如果堆中释放的RAM只有很小一块,而且它们彼此被分割开来,这样的堆就是碎片化的,到处都是碎片的可用RAM。这时就可能出现需要一个大的堆,但到处都是碎片空间,没有足够大的空间分配给这个块,即使碎片空间总的大小大于需要分配的空间,也不能顺利分配需要的空间块
FreeRTOS提供了5个pvPortMalloc()和vPortFree()的实现(heap_1.c到heap_5.c),这5个具体实现位于FreeRTOS/Source/portable/MemMang/目录中。

内存分配方案

heap_1

常用于小的嵌入式系统,只在开启调度器之前创建任务或其他内核对象。当用它时,只在程序开始处理实时函数之前,由内核动态分配内存,程序运行期间内存保持分配状态。意味着选择了这个方案就不用考虑一些更复杂的内存分配问题,比如确定性和碎片化,而是只需要考虑代码大小和简单性等属性。
heap_1.c是一个非常基础的pvPortMalloc()实现,没有实现vPortFree()。所以才会只在开启调度器之前分配,程序运行过程中都不会释放,考虑的情况相当基础。不能删除任务和其他对象。

一些商业为主或安全为主的系统可能会阻止使用动态内存分配,这时就可能会用到heap_1.c。关键系统不用动态内存分配是因为非确定性带来的不确定性,内存碎片化,和可能的分配失败。heap_1.c是确定的,而且不会使内存碎片化。
heap_1分配方案会将一个简单的数组细分为更小的块,通过调用pvPortMalloc()实现。这里的数组就是FreeRTOS的堆。
这个数组(堆)的总大小是在FreeRTOSConfig.h中由configTOTAL_HEAP_SIZE定义的。用这种方式定义一个大数组,会让程序看起来消耗很多RAM,甚至那些数组中没有被分配的内存也会被包含在这些RAM里面。

# 用heap_1创建任务,分配RAM的情况

-------------   -------------   -------------
|     A     |   |     B     |   |     C     |
|           |   |           |   |           |
|           |   |           |   |           |
|           |   |           |   |           |
|           |   |           |   |           |
|           |   |           |   |           |
|           |   |           |   |-----------|
|           |   |           |   |  Stack    |
|           |   |           |   |   TCB     |
|           |   |           |   |-----------|
|           |   |           |   |  Stack    |
|           |   |           |   |   TCB     |
|           |   |-----------|   |-----------|
|           |   |   Stack   |   |  Stack    |
|           |   |    TCB    |   |   TCB     |
-------------   -------------   -------------

heap_2

heap_2仍然保留在发布包中是为了兼容旧的FreeRTOS程序,但新版本的已经不推荐使用。可以用heap_4替代heap_2,heap_4是heap_2的升级版本。
heap_2.c也是通过细分一个configTOTAL_HEAP_SIZE长度的数组。它用一个合适的算法分配内存,不像heap_1那么低效率,它允许释放内存。同样这里的待分配内存是静态声名的,所以看起来程序很费RAM空间,即使那些还没有被分配的空间也占用内存。
这里合适的算法确保pvPortMalloc()使用的空闲块空间和申请的大小最接近。比如下面情形:

  • 堆空间有3个空闲块,分别是5字节,25字节和100字节
  • pvPortMalloc().申请一个20字节的RAM空间

这个请求可以匹配的最小空间就是那块25字节的空间,所以pvPortMalloc()会分割25字节块为20字节和5字节,然后返回20字节空间地址。新的5字节块依然是空闲状态。
不像heap_4,heap_2不会合并邻近的空闲空间成一个大的空闲空间,因此它更容易碎片化。如果在分配后可以顺序释放,碎片化就不是问题。heap_2适用于重复的创建和删除任务,而且创建任务分配的栈大小不改变。

-------------   -------------   -------------
|     A     |   |     B     |   |     C     |
|           |   |           |   |           |
|           |   |           |   |           |
|           |   |           |   |           |
|           |   |           |   |           |
|-----------|   |-----------|   |-----------|
|    Stack  |   |    Stack  |   |  Stack    |
|     TCB   |   |     TCB   |   |   TCB     |
|-----------|   |-----------|   |-----------|
|    Stack  |   |           |   |  Stack    |
|     TCB   |   |----–------|   |   TCB     |
|-----------|   |-----------|   |-----------|
|    Stack  |   |   Stack   |   |  Stack    |
|     TCB   |   |    TCB    |   |   TCB     |
-------------   -------------   -------------
                 free a task
  • A显示创建3个任务后的情况。一个大的空闲块在数组的顶部。
  • B显示删除一个任务后的情况。那个大的空闲空间还是在上面没变。现在就存在2个小的任务块和2个空闲任务块(一个是原来任务的TCB块,一个是原来任务的栈块。它们不会合并为一个大的空间块)。
  • C显示再次创建一个任务块后的情况。创建一个任务会导致两次调用pvPortMalloc(),一个用于分配新的TCB,一个用于分配任务栈。任务创建使用xTaskCreate()函数,pvPortMalloc()会在xTaskCreate()的中调用。
    每个TCB的大小都一样,所以最佳算法确保之前分配给TCB的RAM,在任务删除后。再次创建TCB块时会再次使用这个RAM空间。
    新创建任务的堆栈大小同之前删除任务的堆栈一样,这样算法就能确保之前被删除任务分配的堆栈完全可以用于新任务的堆栈。
    那一块最大的未被使用的空闲块还是在哪里,没有改变。
    heap_2不是确定的,但它比大多数的标准C函数的malloc()和free()的实现快。

heap_3

heap_3.c使用标准库的malloc()和free()函数,所以堆的尺寸由链接器配置决定,configTOTAL_HEAP_SIZE不会对它造成影响。
heap_3.c通过短暂的暂停FreeRTOS调度器使malloc()和free()线程安全。

heap_4

和heap_1、heap_2类似,heap_4这是通过细分数组为更小的块。和前面一样,这里的数组是静态分配的,由configTOTAL_HEAP_SIZE决定。所以会让程序看起来好像很费内存,即使在其中很多内存没有被分配使用之前。
heap_4用第一个适当的算法分配内存。不像heap_2,heap_4会将相邻的小空闲块合并成大的空闲块,这样可以减少内存的碎片化。

# 用heap_4分配和释放内存
---------  ---------  ---------  ---------  ---------  ---------
|   A   |  |   B   |  |   C   |  |   D   |  |   E   |  |       |
|       |  |       |  |       |  |       |  |       |  |       |
|       |  |       |  |       |  |       |  |       |  |       |
|       |  |       |  |       |  |       |  |       |  |       |
|       |  |       |  |       |  |       |  |       |  |       |
|-------|  |-------|  |-------|  |-------|  |-------|  |-------|
| Stack |  |  Stack|  | stack |  | Stack |  | Stack |  | Stack |
|       |  |       |  |       |  |       |  |       |  |       |
|  TCB  |  |   TCB |  |  TCB  |  |  TCB  |  |  TCB  |  |  TCB  |
|-------|  |-------|  |-------|  |-------|  |-------|  |-------|
| Stack |  |       |  |  free |  |  free |  |  free |  |  free |
|       |  |  free |  | space |  |  User |  |  User |  |       |
|  TCB  |  | space |  | Queue |  | Queue |  |  free |  | space |
|-------|  |-------|  |-------|  |-------|  |-------|  |-------|
| Stack |  | Stack |  |  Stack|  | Stack |  | Stack |  | Stack |
|       |  |       |  |       |  |       |  |       |  |       |
|  TCB  |  |  TCB  |  |   TCB |  |  TCB  |  |  TCB  |  |  TCB  |
---------  ---------  ---------  ---------  ---------  ---------
  • A展示数组中有3个已经创建的任务。和一个大的空闲空间在数组顶部。
  • B显示的是删除一个任务后的情况。顶部的大空闲空间还是保持原样。现在留出了一个之前分配的TCB和任务栈空间。注意不像heap_2,释放TCB后空间空闲,释放任务栈后空间又空闲出来。这个时候两个空闲空间会合并为一个,而heap_2的算法不会进行合并操作。heap_4会把两个相邻的空闲空间合并为一个大的空闲空间。
  • C显示FreeRTOS再创建一个队列后的样子。创建队列用xQueueCreate()函数。xQueueCreate()会调用pvPortMalloc()分配一块RAM给队列。因为使用heap_4的分配算法,pvPortMalloc()会从第一个足够大的空闲RAM分配空间,在图中,就是删除任务后释放的TCB和任务堆栈合并起来的空闲空间。队列没有占满整个块,所以这块空闲空间分割为两个部分,剩下多余的空闲空间对于pvPortMalloc()依然可以使用。
  • D显示用户程序直接调用pvPortMalloc()后的情况,这里不是有FreeRTOS内核调用的pvPortMalloc()。用户申请的空间比较小,小到C中剩余的空间都足够大,所以直接会匹配C中剩余的空闲空间。因为用户申请分配空间小,不足以占满C中剩余空间。所以这块剩余空间再次分割为2块。最后再次剩余的空间对于pvPortMalloc()还是可用的。
  • E展示的是删除队列后的情况。队列删除后会自动释放分配给队列的空间。这时在用户申请空间上方和下方都有一块空闲空间了。
  • F显示用户程序申请空间释放后的情况。一旦用户申请的那块空间被释放,之前的3块空间就会被合并为一块更大的空闲空间。这3块空闲空间分别为,按照D显示剩余的空闲空间,用户申请的那块空间,原来队列申请那块空间。

heap_4是不确定的,但比标准C的malloc()和free()要快。

默认情况下,heap_4使用的数组是在heap_4.c源文件中声名,它的起始地址自动有链接器设定。但如果FreeRTOSConfig.h中configAPPLICATION_ALLOC_HEAP宏在编译时设置为1,那么这个数组就需要使用FreeRTOS的程序定义。如果这个数组定义是程序的一部分,那么程序开发者就可以设定它的起始地址。
如果FreeRTOSConfig.h中的configAPPLICATION_ALLOC_HEAP设置为1,就需要在程序的源码文件中包括一个configTOTAL_HEAP_SIZE宏,指定ucHeap数组的长度。

// GCC编译器申明数组的语法,这会被用于heap_4的数组声名
uint8_t ucheap[configTOTAL_HEAP_SIZE] __attribute((section(".my_heap")));

heap_5

用heap_5的算法分配和释放内存和heap_4相同。但heap_5使用的数组不限于只能在编译阶段静态分配;heap_5可以从多个和分开的空间分配内存。当在使用FreeRTOS系统时,没有一个单独连续的空间块给系统使用时heap_5就很有用。
heap_5使用vPortDefineHeapRegions()函数初始化。当使用heap_5时,必须在创建任何内核对象(任务,队列,信号量等)之前调用vPortDefineHeapRegions()进行初始化。

vPortDefineHeapRegions() API接口

vPortDefineHeapRegions()函数用来指定每个分开内存的开始地址和尺寸大小,这些不同的内存组合起来成为一个总的内存给heap_5使用。

每个单独的内存区域都用一个HeapRegion_t的结构体。所有可用内存区域的描述都通过传递给vPortDefineHeapRegions()作为一个HeapRegion_t的数组。

// HeapRegion_t结构体
typedef_t struct HeapRegion {
    /* 内存块的起始地址,它将会成为堆的一部分 */
    uint8_t *pucStartAdress;
    /* 用字节表示的内存块大小 */
    size_t xSizeInByte;
}HeapRegion_t;
# vPortDefineHeapRegions()的参数
pxHeapRegions: 一个指向HeapRegion_t结构的数组指针。每个这个数组里面的结构体都描述了内存块的起始地址和内存长度,这个内存结构就是`heap_5`会用到的内存的一部分。
               这里数组中的HeapRegion_t结构必须按照起始地址排序;这个数组中起始地址最低的HeapRegion_t结构必须是这个数组中的第一个,数组中起始地址最高的HeapRegion_t结构必须是数组中的最后一个。
               数组结尾的哪个HeapRegion_t结构体的pucStartAdress成员需要设置为NULL。
/* 定义3个内存区域和大小
#define RAM1_START_ADDRESS      ((uint8_t *) 0x00010000)
#define RAM1_SIZE               (65 * 1024)
#define RAM2_START_ADDRESS      ((uint8_t *) 0x00020000)
#define RAM2_SIZE               (32 * 1024)
#define RAM3_START_ADDRESS      ((uint8_t *) 0x00030000)
#define RAM3_SIZE               (32 * 1024)
/* 创建一个HeapRegion_t数组,每个索性元素代表3个内存区域中的一个,数组的结尾用NULL结束。HeapRegion_t结构数组最低起始地址需要是第一个,最高起始地址需要是最后一个*/
const HeapRegion_t xHeapRegions[] = {
    {RAM1_START_ADDRESS, RAM1_SIZE},
    {RAM2_START_ADDRESS, RAM2_SIZE},
    {RAM3_START_ADDRESS, RAM3_SIZE},
    {NULL,               0        }
};

int main(void){
    /* 初始化heap_5的数组*/
    vPortDefineHeapRegions(xHeapRegions);

    /* 这里加程序代码 */
}

堆相关工具函数

xPortGetFreeHeapSize函数

调用xPortGetFreeHeapSize()会返回堆中剩余的空闲空间大小。可以用它来优化堆大小。比如,如果内核完成所有对象创建后xPortGetFreeHeapSize()返回2000,那么configTOTAL_HEAP_SIZE就可以减少2000。
当用heap_3管理堆空间时不能使用xPortGetFreeHeapSize()。

xPortGetMinimumEverFreeHeapSize函数

xPortGetMinimumEverFreeHeapSize()返回自FreeRTOS程序开始执行以来,没有被分配使用的堆空闲空间的最小字节数量。
只有在使用heap_4,heap_5管理堆空间时才可以使用xPortGetMinimumEverFreeHeapSize()函数。

malloc分配失败钩子函数

pvPortMalloc()可以在用户空间直接调用。也可以在每次创建内核对象时由FreeRTOS源码调用。内核对象包括任务,队列,信号量和事件组。它们都会在后面的章节介绍。
就像标准函数库的malloc(),如果因为请求尺寸的内存块不存在,使pvPortMalloc()不能返回一个内存块,那么它就会返回NULL。如果因为创建内核对象调用pvPortMalloc()或用户直接调用pvPortMalloc()返回NULL,那么这个内核对象就不能成功创建。
所有使用堆自动分配的方案都可以设置一个pvPortMalloc()返回NULL时的回调函数。
如果将FreeRTOSConfig.h中的configUSE_MALLOC_FAILED_HOOK设置为1,那么程序就要提供一个自动分配失败回调函数

// 自动分配失败回调函数名字和原型。
void vApplicationMallocFailedHook(void);
博主已关闭本页面的评论功能