【FreeRTOS学习笔记】CHA2-堆内存管理

目录
1. 预备知识
2. 内存分配方案示例
3. 与堆相关的实用函数


  1. 预备知识
  • 从FreeRTOS V9.0.0 版本开始,可以完全静态分配FreeRTOS 应用程序,不再需要包含堆内存管理器。但是本章讨论动态内存分配,FreeRTOS 在每次创建内核对象时分配RAM,在每次删除内核对象时释放RAM。这种策略减少了系统设计和规划的工作量,简化了API,并最大限度地减少了RAM地占用。
  • 可以使用标准C语言库的 malloc() 函数和 free() 函数来分配内存,但由于一些原因,这些函数可能并不适合:
    • 在小型嵌入式系统中,malloc() 函数和 free() 函数并不总是可用的。
    • 实现的malloc () 函数和 free() 函数可能相对较大,占用了宝贵的代码空间。
    • malloc () 函数和 free() 函数很少是线程安全的。
    • malloc () 函数和 free() 函数不是确定的,执行函数所需时间会因为不同的调用而有差异。
    • malloc () 函数和 free() 函数可能会出现内存碎片化的情况。(比如,堆内的空闲RAM分散成彼此独立的小块,虽然可能总的空闲RAM大小是够的,但是因为其不连续,所以仍无法被分配给某个内存块。)
    • malloc () 函数和 free() 函数会使链接器配置复杂化。
  • FreeRTOS现在将内存分配视为可移植层部分,因为不同的嵌入式系统有不同的动态内存分配和时间要求。FreeRTOS使用pvPortMalloc() 和 vPortfree() 来分配和释放RAM。
  • FreeRTOS 提供了5个pvPortMalloc() 和 vPortfree() 的实现案例,它们分别定义在heap_1.c, …, heap_5.c 源文件中,全部位于 FreeRTOS/Source/portable/MemMang 目录下。
  1. 内存分配方案示例

(1)heap_1

  • heap_1.c 实现了基本的 pvPortMalloc() 函数,并且没有实现vPortFree() 函数。所以,从不删除任务或其他内核对象的应用程序可能会使用heap_1(有些关键系统是禁止使用动态内存分配的,因为存在非确定性)。heap_1 总是确定性的,而且不会引起内存碎片化。
  • 每个创建的任务需要一个任务控制块(TaskControlBlock, TCB)和一个从堆中分配的栈。下图演示了每次创建任务时都会从heap_1数组中分配RAM。在这里插入图片描述

(2)heap_2

  • heap_2.c 的工作原理也是对一个大小定义为 configTOTAL_HEAP_SIZE 的数组细分。heap_2使用最佳匹配算法来分配内存,并且允许释放内存。同样数组也是静态声明的,所以会使应用程序看起来消耗大量RAM。比如,
    • 堆中包含3个自由内存块,分别为5字节、25字节和100字节。
    • 为请求20字节的RAM,调用pvPortMalloc() 函数。
      适合所请求字节数的最小空闲RAM块是25字节的内存块,所以pvPortMalloc() 函数将25字节的内存块分割成一个20字节、一个5字节的内存块(过于理想状态,为了便于解释),然后返回一个指向20字节内存块的指针。
  • 图中的C,此次创建任务引起两次调用pvPortMalloc() 函数,一次是分配新的TCB,另一次是分配任务栈。任务是使用 xTaskCreate() API函数创建的,对 pvPortMalloc()函数的调用发生在xTaskCreate() API函数内部。 在这里插入图片描述

(3)heap_3

  • heap_3 使用标准库的 malloc() 函数和 free() 函数,所以堆的大小是由链接器配置定义的,configTOTAL_HEAP_SIZE 的设置对其没有影响。
  • heap_3 通过暂停 FreeRTOS调度器使得 malloc() 函数和 free() 函数线程安全。

(4)heap_4

  • 与heap_1, heap_2 一样,heap_4 的数组也是静态声明的,大小由configTOTAL_HEAP_SIZE 定义。
  • heap_4 使用首次匹配算法,确保pvPortMalloc() 函数使用第一个空闲内存块。
  • heap_4 可以将相邻的空闲块合并。在这里插入图片描述
  • 默认情况下,heap_4 使用的数组是在heap_4.c 源文件中声明的,其起始地址是由链接器自动设置的。但是,如果将 FreeRTOSConfig.h 中的 configAPPLICATION_ALLOCATED_HEAP 编译配置常量设置为1,那么这个数组便由 FreeRTOS的应用程序来声明。
/* 使用 GCC 语法声明 heap_4 将使用的数组,并将数组放入名为 my_heap 的内存区 */
uint8_t Heap [ configTOTAL_HEAP_SIZE ]__attribute__(section("my_heap"));
/* 使用 IAR 语法声明 heap_4 将使用的数组,并将数组放入0x20000000 处 */
uint8_t ucHeap [ configTOTAL_HEAP_SIZE ] @ 0x20000000;

(5)heap_5

  • heap_5 用于分配和释放内存的算法与 heap_4 相同,但是可以从多个内存空间中分配内存。
  • vPortDefineHeapRegions() API 函数用于指定每个独立内存区域的起始地址和大小。
void vPortDefineHeapRegions (const HeapRegion_t* const pxHeaRegions);
typedef struct HeapRegion
{
	/* 内存块的起始地址,该内存块将成为堆的一部分。 */
	uint8_t *pucStartAddress;
	/* 内存块的大小,以字节为单位。 */
	size_t xSizeInBytes;
} HeapRegion_t;

举例来说:

在这里插入图片描述

/* 图A,由HeapRegion_t 结构体组成的数组,这些结构体共同描述了3个RAM块的全部内容 */
/* 定义3个RAM块的起始地址和大小。 */
#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个RAM块各有一个索引,数组以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. */
  vPortDefineHeapRegion(xHeapRegions);
  /* 此处添加应用程序代码。 */
}
  • 图A所示,已经把所有的RAM都分配给了堆,没有空闲的RAM供其他变量使用。
  • 构建工程时,构建过程的链接阶段会给每个变量分配RAM地址。可供链接器使用的RAM通常由链接器配置文件描述。如图.B,链接器脚本包含了RAM1的信息,只留下RAM1中 0x0001nnnn以上的部分供 heap_5 使用。如果仍使用上述代码,分配给heap_5 的0x0001nnnn以下的RAM将与存放变量的RAM重叠,为了避免这种情况,可以使 xHeapRegions[] 数组的第一个 HeapRegion_t 结构体使用起始地址 0x0001nnnn,但是并不推荐:
    • 起始地址可能不容易确定。
    • 未来的构建中,链接器使用的 RAM大小可能会改变,因此需要更新 HeapRegion_t 结构体的起始地址。
    • 如果链接器使用的RAM和heap_5使用的RAM重叠,则构建工具不知道,也就不会提醒编程人员。
  • 针对这个问题,下面的代码使用了更方便、更容易维护的例子。
/* 由HeapRegion_t 结构体组成的数组,描述了全部RAM2和RAM3,但只描述了部分RAM1。 */

/* 定义链接器未使用的两个RAM区域的起始地址和大小。 */
#define RAM2_START_ADDRESS	((uint8_t*) 0x00020000)
#define RAM2_SIZE							(32* 1024)
#define RAM3_START_ADDRESS	((uint8_t*) 0x00030000)
#define RAM3_SIZE							(32 * 1024)
/* 声明数组,该数组将成为 heap_5 使用的堆的一部分。该数组将被链接器放在RAM1中。 */
#define RAM1_HEAP_SIZE (30 * 1024)
static uint8_t ucHeap[RAM!_HEAP_SIZE];
/* 创建HeapRegion_t 类型的数组。heap_5 将只使用RAM1 中包含 ucHeap 数组的部分。 */
const HeapRegion_t xHeapRegions[] = 
{
	{ucHeap            , RAM1_HEAP_SIZE },
	{RAM2_START_ADDRESS, RAM2_SIZE},
	{RAM3_START_ADDRESS, RAM3_SIZE},
	{NULL			   , 0        } /* 标记数组的结束。 */
};
  • 上述代码所演示技术的优点是:
    • 不需要硬编码的起始地址。
    • HeapRegion_t 结构体使用的地址将由链接器自动设置,因此即使未来构建工程时链接器使用的RAM大小发生了变化,也将始终正确。
    • 分配给 heap_5 的RAM不可能与链接器放入 RAM1 的数据重叠。
    • 如果 ucHeap 太大,则应用程序将无法链接。
  1. 与堆相关的实用函数
  • xPortGetFreeHeapSize() API 函数。
    返回堆中可用的字节数,可用来优化堆的大小。例如,在创建全部内核对象后 xPortGetFreeHeapSize()API函数返回200,那么 configTOTAL_HEAP_SIZE 的值就可以减少200。
size_t xPortGetFreeHeapSize(void);
  • xPortGetMinimumEverFreeHeapSize() API函数。
    返回自从 FreeRTOS 应用程序开始执行以来,堆中曾经存在的未分配字节的最小数量。例如,如果 xPortGetMinimumEverFreeHeapSize()API函数返回200,那么表明自从应用程序开始执行以来的某个时间,应用程序离耗尽堆空间只有200字节。此API函数只允许heap_4, heap_5 使用。
size_t  xPortGetMinimumEverFreeHeapSize(void);
  • malloc 失败的钩子函数
    所有堆分配方案的示例都可以配置成如果调用 pvPortMalloc()函数时返回NULL,就调用钩子(或回调)函数。
void vApplicationMallocFailedHook(void);
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值