内存管理是一个系统基本组成部分,FreeRTOS 中大量使用到了内存管理,比如创建任务、信号量、队列等的时候会自动从堆中申请内存。用户应用层代码也可以 FreeRTOS 提供的内存管理函数来动态的使用内存。

简介

FreeRTOS 创建任务、队列、信号量等的时候有两种方法,一种是动态的申请所需的 RAM。一种是由用户自行定义所需的 RAM,这种方法也叫静态方法,使用静态方法的函数一般以“Static”结尾,比如任务创建函数 xTaskCreateStatic(),使用此函数创建任务的时候需要由用户定义任务堆栈。
使用动态内存管理的时候 FreeRTOS 内核在创建任务、队列、信号量的时候会动态的申请RAM。标准 C 库中的 malloc()和 free()还是也可以实现动态内存管理,但是如下原因限制了其使用:

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

不同的嵌入式系统对于内存分配和时间要求不同,因此一个内存分配算法可能仅作为一个应用的子集。所以 FreeRTOS 将内存分配作为移植层的一部分,这样 FreeRTOS 使用者就可以使用自己的合适的内存分配方法。
当内核需要 RAM 的时候可以使用 pvPortMalloc()来替代 malloc()申请内存,不使用内存的时候可以使用 vPortFree()函数来替代 free()函数释放内存。函数 pvPortMalloc()、vPortFree()与函数 malloc()、free()的函数原型类似。
FreeRTOS 提供了 5 种内存分配方法,FreeRTOS 使用者可以其中的某一个方法,或者自己的内存分配方法。这 5 种方法是 5 个文件,分别为:heap_1.cheap_2.cheap_3.cheap_4.cheap_5.c。路径:FreeRTOS->Source->portable->MemMang

内存碎片

在看 FreeRTOS 的内存分配方法之前我们先来看一下什么叫做内存碎片,看名字就知道是小块的、碎片化的内存。
18.内存管理 - 图1
内存碎片是内存管理算法重点解决的一个问题,否则的话会导致实际可用的内存越来越少,最终应用程序因为分配不到合适的内存而奔溃!FreeRTOS 的heap_4.c就给我们提供了一个解决内存碎片的方法,那就是将内存碎片进行合并组成一个新的可用的大内存块。

FreeRTOS 内存分配方法

heap_1.c

动 态 内 存 分 配 需 要 一 个 内 存 堆 , FreeRTOS 中 的 内 存 堆 为 ucHeap[] ,大小为configTOTAL_HEAP_SIZE,这个前面讲 FreeRTOS 配置的时候就讲过了。不管是哪种内存分配方法,它们的内存堆都为ucHeap[],而且大小都是configTOTAL_HEAP_SIZE
heap_1.c文件有如下定义。

  1. #if( configAPPLICATION_ALLOCATED_HEAP == 1 )
  2. extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; //需要用户自行定义内存堆
  3. #else
  4. static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; //编译器决定
  5. #endif

当宏configAPPLICATION_ALLOCATED_HEAP为1的时候需要用户自行定义内存堆,否则的话由编译器来决定,我们默认都是由编译器来决定的。如果自己定义的话就可以将内存堆定义到外部SRAM或者SDRAM中。
heap_1.c仅实现了内存申请函数pvPortMalloc(),并未实现内存释放函数pvFree(),可以看一下 pvFree()的源码,如下:

  1. void vPortFree( void *pv )
  2. {
  3. ( void ) pv;
  4. configASSERT( pv == NULL );
  5. }

如果使用heap_1.c,一旦申请内存成功就不允许释放!但是heap_1.c的内存分配过程简单,如此看来 heap_1.c似乎毫无任何使用价值啊。千万不能这么想,有很多小型的应用在系统一开始就创建好任务、信号量或队列等, 而在程序运行的整个过程中都不会删除,那么这个时候使用heap_1.c就很合适的。
heap_1.c实现起来就是当需要 RAM 的时候就从一个大数组(内存堆)中分一小块出来,大数组(内存堆)的容量为configTOTAL_HEAP_SIZE,前面已经说了。使用函数xPortGetFreeHeapSize()可以获取内存堆中剩余内存大小。
特性

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

    heap_2.c

    heap_2.c 提供了一个更好的配算法,不像 heap_1.c 那样,heap_2.c 提供了内存释放函数。heap_2.c 不会把释放调的内存块合并成一个大块,这样有一个缺点,随着你不断的申请内存,内存堆就会被分为很多个大小不一的内存(块),也就是会导致内存碎片。
    特性

  • 可以使用在那些可能会重复的删除任务、队列、信号量等的应用中,要注意有内存碎片产生!

  • 如果分配和释放的内存大小是随机的,那么就要慎重使用了。
  • 如果应用中的任务、队列、信号量和互斥信号量具有不可预料性(如所需的内存大小不能确定,每次所需的内存都不相同,或者说大多数情况下所需的内存都是不同的)的话可能会导致内存碎片。
  • 具有不可确定性,但是也远比标准 C 中的 mallo()和 free()效率高。

    heap_3.c

    这个分配方法是对标准 C 中的函数 malloc()和 free()的简单封装,FreeRTOS 对这两个函数做了线程保护。

    1. void *pvPortMalloc( size_t xWantedSize )
    2. {
    3. void *pvReturn;
    4. //挂起所有任务
    5. vTaskSuspendAll();
    6. {
    7. //调用函数 malloc()来申请内存
    8. pvReturn = malloc( xWantedSize );
    9. traceMALLOC( pvReturn, xWantedSize );
    10. }
    11. //恢复所有的任务
    12. ( void ) xTaskResumeAll(); (3)
    13. #if( configUSE_MALLOC_FAILED_HOOK == 1 )
    14. {
    15. if( pvReturn == NULL )
    16. {
    17. extern void vApplicationMallocFailedHook( void );
    18. vApplicationMallocFailedHook();
    19. }
    20. }
    21. #endif
    22. return pvReturn;
    23. }
    24. void vPortFree( void *pv )
    25. {
    26. if( pv )
    27. {
    28. //挂起所有任务
    29. vTaskSuspendAll();
    30. {
    31. //调用函数 free()释放内存
    32. free( pv );
    33. traceFREE( pv, 0 );
    34. }
    35. ( void ) xTaskResumeAll(); (6)
    36. }
    37. }

    特性

  • 需要编译器提供一个内存堆,编译器库要提供 malloc()和 free()函数。比如 STM32 的话可以通过修改启动文件中的 Heap_Size 来修改内存堆的带下。
    18.内存管理 - 图2

  • 具有不确定性
  • 可能会增加代码量。

    heap_4.c

    heap_4.c 提供了一个最优的匹配算法,不像 heap_2.c,heap_4.c 会将内存碎片合并成一个大的可用内存块,它提供了合并算法。函数
    xPortGetMinimumEverFreeHeapSize()用来返回堆栈历史(从上电起到现在)最小剩余大小,可以通过这个返回值来帮助我们调整内存堆的大小。
    特性

  • 用在那些需要重复创建和删除任务、队列、信号量和互斥信号量等的应用中。

  • 不会像 heap_2.c 那样产生严重的内存碎片,即使分配的内存大小是随机的。
  • 具有不确定性,但是远比 C 标准库中的 malloc()和 free()效率高。

    heap_5.c

    heap_5.c 使用了和 heap_4.c 相同的合并算法,内存管理实现起来基本相同,但是 heap_5.c允许内存堆跨越多个不连续的内存段。比如 STM32 的内部 RAM 可以作为内存堆,但是 STM32内部 RAM 比较小,遇到那些需要大容量 RAM 的应用就不行了,如音视频处理。不过 STM32可以外接 SRAM 甚至大容量的 SDRAM,如果使用 heap_4.c 的话你就只能在内部 RAM 和外部SRAM 或 SDRAM 之间二选一了,使用 heap_5.c 的话就不存在这个问题,两个都可以一起作为内存堆来用。
    不过在使用 heap_5.c 之前需要先调用函数 vPortDefineHeapRegions ()来对其初始化,在vPortDefineHeapRegions()未执行完之前禁止调用任何可能会调用 pvPortMalloc()的 API 函数!比如创建任务、信号量、队列等函数。函数 vPortDefineHeapRegions()只有一个参数,参数是一个HeapRegion_t 类型的数组,HeapRegion 为一个结构体,此结构体在 portable.h 中有定义。
    1. typedef struct HeapRegion
    2. {
    3. uint8_t *pucStartAddress; //内存块的起始地址
    4. size_t xSizeInBytes; //内存段大小
    5. } HeapRegion_t;
    heap_5.c 允许内存堆跨越多个不连续的内存段,这些不连续的内存段就是由结构体 HeapRegion_t 来定义的。比如以 STM32F429 为例,现在有三个内存段:CCM、内部 SRAM、外部 SDRAM,起始分别为:0X10000000、0X20000000、0XC0000000,大小分别为:64KB、192KB、32MB,那么数组就如下:
    1. HeapRegion_t xHeapRegions[] =
    2. {
    3. { ( uint8_t * ) 0X10000000UL, 0x10000 }, //CCM 内存,起始地址 0X10000000,大小 64KB
    4. { ( uint8_t * ) 0X20000000UL, 0x30000 },//内部 SRAM 内存,起始地址 0X20000000,
    5. //大小为 192KB
    6. { ( uint8_t * ) 0XC0000000UL, 0x2000000},//外部 SDRAM 内存,起始地址 0XC0000000,
    7. //大小为 32MB
    8. { NULL, 0 } //数组结尾
    9. };
    数组中成员顺序按照地址从低到高的顺序排列,而且最后一个成员必须使用 NULL。数组准备好以后就可以调用函数 vPortDefineHeapRegions()完成初始化。

    内存管理相关的API

    | 函数 | 描述 | | —- | —- | | pvPortMalloc() | 分配内存 | | pvFree() | 释放内存 | | xPortGetFreeHeapSize() | 获取剩余的内存大小 | | xPortGetMinimumEverFreeHeapSize() | 返回堆栈历史(从上电起到现在)最小剩余大小 | | vPortDefineHeapRegions () | 初始化管理多区域内存 |