RTOS内核需要使用 RAM 来为每次创建任务、队列、互斥量、信号量、事件组、软件定时器分配内存,内存分配可以由 freeRTOS 的API动态自动从堆上创建,也可以由开发者自己分配。

如果RTOS对象是动态创建的,标准C库中的malloc()free()函数有时可以达到目的,但是:

  • 在某些嵌入式系统中可能不可用
  • 标准库函数占用代码空间
  • 不是线程安全的
  • 不是精确的,会导致总体执行时间降低

以上问题导致,使用标准C库中的malloc()free()不太合适。

嵌入式/实时系统存在多种多样的RAM和时序,因此,单一的RAM 分配算法不可能满足所有的应用。

为了解决这个问题,freeRTOS将内存分配API放在了适配层,这意味着,可以做到跟具体处理器及其架构相关,开发者可以根据实际情况实现适合自己系统的内存分配算法。当RTOS内核需要RAM时,调用适配的pvPortMalloc()来获取分配的内存,使用pvPortFree()释放内存。

freeRTOS提供集中堆内存管理机制,用以管理堆内存,当然,开发者也可以实现自己的堆内存管理机制,甚至是同时使用两种堆内存管理机制,例如,使用两种内存管理机制,将任务等RTOS对象放在速度较快的内部RAM中,如51单片机的idate。而将用户的应用程序数据放在速度较慢的外部RAM,如51的xdate

freeRTOS源代码中的内存分配机制实现

在freeRTOS的源代码里,提供了5个示例内存分配机制,开发者可以根据这5个分配机制的不同特点选择最适合自己应用的。

这5个内存分配实现源代码位于路径Source/Portable/MemMang下的五个文件中(heap_x.c , x = 1 , 2 , 3 , 4 , 5),这5个文件在应用工程中只能包含一个,RTOS的内核将使用这5个文件提供的分配机制管理内存。这5个文件特点如下:

  • heap_1.c 最简单的,不允许释放已分配的内存
  • heap_2.c 允许释放已分配内存,但是不允许合并相邻的空块
  • heap_3.c malloc()free()的线程安全版本
  • heap_4.c 合并相邻的空快,避免内存碎片,包括绝对地址定位选项
  • heap_5.cheap_4.c的基础上,允许堆内存跨越多个不同的RAM段

heap_1.c

这是最简单的一种实现机制,不允许释放已经分配的内存。鉴于这种特性,heap_1.c适合具有很大内存的嵌入式系统,因为所有的内存分配在整个系统的生命周期内都是保持不变的,永远不会被释放。

实现方式很简单,通过分割一个数组给所需要的变量,将原来数组占用的RAM分配给变量。这个数组的大小通过confifTOTAL_HEAP_SIZE设置,使用configAPPLICATION_ALLOCATED_HEAP设置这个数组在RAM中的地址。

在开发过程中,可以使用xPortGetFreeHeapSize()API函数返回空闲的数组内存大小,开发者可以根据这个返回值确定系统实际所需堆内存大小,从而优化configTOTAL_HEAP_SIZE的大小。

heap_1.c特点:

  • 如果你的应用不会删除任务、队列等RTOS对象,则使用这个机制是合适的
  • 总是精确的(总是花费相同的时间去执行),并且不会导致内存碎片
  • 从一个已经静态分配好的数组中分配内存是很简单的,这意味着,这很适合那些不允许真正的动态内存分配的系统

heap_2.c

相比heap_1.c,允许释放以及分配的内存,但是不会合并相邻的空块,这意味着会造成内存碎片。

堆内存的大小通过confifTOTAL_HEAP_SIZE设置,使用configAPPLICATION_ALLOCATED_HEAP设置这个堆在RAM中的地址。

在开发过程中,可以使用xPortGetFreeHeapSize()API函数返回空闲的数组内存大小,开发者可以根据这个返回值确定系统实际所需堆内存大小,从而优化configTOTAL_HEAP_SIZE的大小。但是不会提供堆内存中的碎片信息。

特点:

  • 可以使用在那些需要删除任务、队列等的系统,但是,会产生内存碎片
  • 不应该使用在需要随机分配和释放内存的应用中。经常随机分配和释放内存会导致内存中的碎片逐渐变多,即使空闲内存足够多,但是因为内存碎片,可能仍然会导致内存分配失败
  • 会导致内存碎片,而且碎片分布无法预测
  • 不精确,但是比标准C库中的库函数高效

因此,heap_2.c适合用在那些每次分配和释放内存大小都是固定的应用中。

heap_3.c

malloc()free()的线程安全版本,大多数情况下,需要编译器支持。

特点:

  • 需要链接器设置堆,以及由编译器库文件提供malloc()free()的实现
  • 不精确
  • 会增加代码空间占用

注意configTOTAL_HEAP_SIZE设置在使用heap_3.c时无效。

heap_4.c

相比heap_2.c,实现了碎片整理的功能,会合并相邻的空块。

堆内存的大小通过confifTOTAL_HEAP_SIZE设置,使用configAPPLICATION_ALLOCATED_HEAP设置这个堆在RAM中的地址。

在开发过程中,可以使用xPortGetFreeHeapSize()API函数返回空闲的数组内存大小,开发者可以根据这个返回值确定系统实际所需堆内存大小,从而优化configTOTAL_HEAP_SIZE的大小。

特点:

  • 可以使用在经常删除任务、队列等的应用中
  • 即使分配和释放的内存大小都是随机的,也不会造成很多内存碎片
  • 不精确,但是比标准C库函数高效

heap_4.c在那些需要在应用中直接使用适配层的内存分配机制,而不是调用API函数的应用中尤其有用。

heap_5.c

heap_4.c的基础上,增加对堆内存跨越多个RAM段的支持。

heap_5 使用vPortDefineHeapRegions()完成初始化,在初始化完成之前,堆内存不可用。在创建RTOS对象(队列、信号量等)时,会隐式的调用pvPortMalloc()。因此,初始化对于使用heap_5是很重要的。

vPortDefineHeapRegions接收一个参数,这个参数是个heapRegion_t类型的结构体数组,heapRegion_t定义在portable.h文件中:

  1. typedef struct HeapRegion
  2. {
  3. /* 作为堆内存的一部分的RAM的开始地址.*/
  4. uint8_t *pucStartAddress;
  5. /* RAM大小. */
  6. size_t xSizeInBytes;
  7. } HeapRegion_t;

堆内存使用一个零大小的空区域作为结束,并且内存范围的定义必须是按照从低地址到高地址的方式安排。下面的例子展示了这个机制:

  1. /* 堆内存由两块RAM组成,第三部分为结束标志 */
  2. const HeapRegion_t xHeapRegions[] =
  3. {
  4. { ( uint8_t * ) 0x80000000UL, 0x10000 },
  5. { ( uint8_t * ) 0x90000000UL, 0xa0000 },
  6. { NULL, 0 } /* 数组结束. */
  7. };
  8. /* 传递结构体数组到初始化函数. */
  9. vPortDefineHeapRegions( xHeapRegions );

同样,可以使用xPortGetFreeHeapSize()获取可用堆内存大小,并据此优化堆内存大小。