FreeRTOS学习笔记(9)——内存管理

一、基本概念

FreeRTOS 操作系统将内核与内存管理分开实现,操作系统内核仅规定了必要的内存管理函数原型,而不关心这些内存管理函数是如何实现的,所以在 FreeRTOS 中提供了多种内存分配算法(分配策略),但是上层接口(API)却是统一的。这样做可以增加系统的灵活性:用户可以选择对自己更有利的内存管理策略,在不同的应用场合使用不同的内存分配策略。

在嵌入式程序设计中内存分配应该是根据所设计系统的特点来决定选择使用动态内存分配还是静态内存分配算法,一些可靠性要求非常高的系统应选择使用静态的,而普通的业务系统可以使用动态来提高内存使用效率。静态可以保证设备的可靠性但是需要考虑内 存上限,内存使用效率低,而动态则是相反。

每当任务,队列或是信号量被创建时,内核需要进行动态内存分配。虽然可以调用标准的 malloc()与 free()库函数,但必须承担以下若干问题:

  1. 这两个函数在小型嵌入式系统中可能不可用,小型嵌入式设备中的 RAM 不足。
  2. 这两个函数的具体实现可能会相对较大,会占用较多宝贵的代码空间。
  3. 这两个函数通常不具备线程安全特性。
  4. 这两个函数具有不确定性。每次调用时的时间开销都可能不同。
  5. 这两个函数会产生内存碎片。
  6. 这两个函数会使得链接器配置得复杂。

FreeRTOS 对内存管理做了很多事情,FreeRTOS 的 V9.0.0 版本为我们提供了 5 种内存管理算法,分别是 heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c,源文件存放于 FreeRTOS\Source\portable\MemMang 路径下,在使用的时候选择其中一个添加到我们的工程中去即可。

FreeRTOS 的内存管理模块通过对内存的申请、释放操作,来管理用户和系统对内存的使用,使内存的利用率和使用效率达到最优,同时最大限度地解决系统可能产生的内存碎片问题。

二、内存管理方案

2.1 heap_1.c

heap_1.c 管理方案是 FreeRTOS 提供所有内存管理方案中最简单的一个,它只能申请内存而不能进行内存释放,并且申请内存的时间是一个常量,这样子对于要求安全的嵌入式设备来说是最好的,因为不允许内存释放,就不会产生内存碎片而导致系统崩溃,但是也有缺点,那就是内存利用率不高,某段内存只能用于内存申请的地方,即使该内存只使用一次,也无法让系统回收重新利用。

实际上,大多数的嵌入式系统并不会经常动态申请与释放内存,一般都是在系统完成的时候,就一直使用下去,永不删除,所以这个内存管理方案实现简洁、安全可靠,使用的非常广泛。

内 存 分 配 时 需 要 的 总 的 堆 空 间 由 文 件 FreeRTOSConfig.h 中 的 宏 ·configTOTAL_HEAP_SIZE 配置,单位为字。通过调用函数 xPortGetFreeHeapSize() 我们可以知道还剩下多少内存没有使用,但是并不包括内存碎片。这样一来我们可以实时的调整和优化 configTOTAL_HEAP_SIZE 的大小。

heap_1.c 实现了一个非常基本的 pvPortMalloc() 版本,而且没有实现 vPortFree()。

heap1.c 方案具有以下特点:

  • 用于从不删除任务、队列、信号量、互斥量等的应用程序(实际上大多数使用 FreeRTOS 的应用程序都符合这个条件)。
  • 函数的执行时间是确定的并且不会产生内存碎片。

2.2 heap_2.c

heap_2.c 方案与 heap_1.c 方案采用的内存管理算法不一样,它采用一种最佳匹配算法(best fit algorithm),比如我们申请 100 字节的内存,而可申请内存中有三块对应大小 200 字节, 500 字节和 1000 字节大小的内存块,按照算法的最佳匹配,这时候系统会把 200 字节大小的内存块进行分割并返回申请内存的起始地址,剩余的内存则插回链表留待下次申请。heap_2.c 方案支持释放申请的内存,但是它不能把相邻的两个小的内存块合成一个大的内存块,对于每次申请内存大小都比较固定的,这个方式是没有问题的,而对于每次申请并不是固定内存大小的则会造成内存碎片,后面要讲解的 heap_4.c 方案采用的内存管理算法能解决内存碎片的问题,可以把这些释放的相邻的小的内存块合并成一个大的内存块。

同样的,内存分配时需要的总的内存堆空间由文件 FreeRTOSConfig.h 中的宏 configTOTAL_HEAP_SIZE 配置,单位为字。通过调用函数 xPortGetFreeHeapSize() 我们可以知道还剩下多少内存没有使用,但是并不包括内存碎片,这样一来我们可以实时的调整和优化 configTOTAL_HEAP_SIZE 的大小。

heap_2.c 方案具有以下特点:

  • 可以用在那些反复的删除任务、队列、信号量、等内核对象且不担心内存碎片的应用程序。
  • 如果我们的应用程序中的队列、任务、信号量、等工作在一个不可预料的顺序,这样子也有可能会导致内存碎片。
  • 具有不确定性,但是效率比标准 C 库中的 malloc 函数高得多。
  • 不能用于那些内存分配和释放是随机大小的应用程序。

2.3 heap_3.c

heap_3.c 方案只是简单的封装了标准 C 库中的 malloc()和 free()函数,并且能满足常用的编译器。重新封装后的 malloc()和 free()函数具有保护功能,采用的封装方式是操作内存前挂起调度器、完成后再恢复调度器。

heap_3.c 方案具有以下特点:

  • 需要链接器设置一个堆,malloc()和 free()函数由编译器提供。
  • 具有不确定性。
  • 很可能增大 RTOS 内核的代码大小。

要 注意的是在 使 用 heap_3.c 方 案 时 , FreeRTOSConfig.h 文件中的 configTOTAL_HEAP_SIZE 宏定义不起作用。在 STM32 系列的工程中,这个由编译器定义的堆都在启动文件里面设置,单位为字节,我们具体以 STM32F10x 系列为例

2.4 heap_4.c

heap_4.c 方案与 heap_2.c 方案一样都采用最佳匹配算法来实现动态的内存分配,但是不一样的是 heap_4.c 方案还包含了一种合并算法,能把相邻的空闲的内存块合并成一个更大的块,这样可以减少内存碎片。heap_4.c 方案特别适用于移植层中可以直接使用 pvPortMalloc()和 vPortFree()函数来分配和释放内存的代码。

内 存 分 配 时 需 要 的 总 的 堆 空 间 由 文 件 FreeRTOSConfig.h 中 的 宏 ·configTOTAL_HEAP_SIZE 配置,单位为字。通过调用函数 xPortGetFreeHeapSize() 我们可以知道还剩下多少内存没有使用,但是并不包括内存碎片。这样一来我们可以实时的调整和优化 configTOTAL_HEAP_SIZE 的大小。

heap_4.c 方案的空闲内存块也是以单链表的形式连接起来的,BlockLink_t 类型的局部静态变量 xStart 表示链表头,但 heap_4.c 内存管理方案的链表尾部则保存在内存堆空间最后位置,并使用 BlockLink_t 指针类型局部静态变量 pxEnd 指向这个区域(而 heap_2.c 内存管理方案则使用 BlockLink_t 类型的静态变量 xEnd 表示链表尾)

heap_4.c 内存管理方案的空闲块链表不是以内存块大小进行排序的,而是以内存块起始地址大小排序,内存地址小的在前,地址大的在后,因为 heap_4.c 方案还有一个内存合并算法,在释放内存的时候,假如相邻的两个空闲内存块在地址上是连续的,那么就可以合并为一个内存块,这也是为了适应合并算法而作的改变。

heap_4.c 方案具有以下特点:

  • 可用于重复删除任务、队列、信号量、互斥量等的应用程序。
  • 可用于分配和释放随机字节内存的应用程序,但并不像 heap2.c 那样产生严重的内存碎片。
  • 具有不确定性,但是效率比标准 C 库中的 malloc 函数高得多。

2.5 heap_5.c

heap_5.c 方案在实现动态内存分配时与 heap4.c 方案一样,采用最佳匹配算法和合并算法,并且允许内存堆跨越多个非连续的内存区,也就是允许在不连续的内存堆中实现内存分配,比如用户在片内 RAM 中定义一个内存堆,还可以在外部 SDRAM 再定义一个或多个内存堆,这些内存都归系统管理。

heap_5.c 方案通过调用 vPortDefineHeapRegions()函数来实现系统管理的内存初始化,在内存初始化未完成前不允许使用内存分配和释放函数。如创建 FreeRTOS 对象(任务、队列、信号量等)时会隐式的调用 pvPortMalloc()函数,因此必须注意:使用 heap_5.c 内存管理方案创建任何对象前,要先调用 vPortDefineHeapRegions()函数将内存初始化。

heap_5.c 方案具有以下特点:

  • 可用于重复删除任务、队列、信号量、互斥量等的应用程序。
  • 可用于分配和释放随机字节内存的应用程序,但并不像 heap2.c 那样产生严重的内存碎片。
  • 具有不确定性,但是效率比标准 C 库中的 malloc 函数高得多。
  • 允许内存堆跨越多个非连续的内存区,也就是允许在不连续的内存堆中实现内存分配。

三、内存管理接口函数

3.1 pvPortMalloc

void *pvPortMalloc( size_t xSize )
这个函数是由 FreeRTOS 规定的内存分配函数。系统内部及用户如果要使用内存,只能通过该函数接口进行申请。

3.2 vPortFree

void vPortFree( void *pv )
这个函数是由 FreeRTOS 规定的内存释放函数。系统内部及用户如果要释放内存,只能通过该函数接口进行申请。在heap_1.c方案,内存一旦申请便无法释放!

3.3 vPortInitialiseBlocks

void vPortInitialiseBlocks( void )
这个函数是由 FreeRTOS 规定的内存管理初始化函数。

3.4 xPortGetFreeHeapSize

size_t xPortGetFreeHeapSize( void )
这个函数是由 FreeRTOS 规定的获取动态内存的剩余大小的函数。唯一需要注意的就是,其大小是在对齐之后的!

3.5 xPortGetMinimumEverFreeHeapSize

size_t xPortGetMinimumEverFreeHeapSize( void )
获取未分配的内存堆历史最小值。

四、示例

uint8_t *Test_Ptr = NULL;

/* 获取当前内存大小 */ 
g_memsize = xPortGetFreeHeapSize(); 
printf("系统当前内存大小为 %d 字节,开始申请内存\n",g_memsize); 
Test_Ptr = pvPortMalloc(1024); // 申请内存
if (NULL != Test_Ptr) 
{ 
    printf("内存申请成功!\n"); 
    printf("申请到的内存地址为%#x\n",(int)Test_Ptr);
}

vPortFree(Test_Ptr); // 释放内存 
Test_Ptr = NULL; 
/* 获取当前内剩余存大小 */ 
g_memsize = xPortGetFreeHeapSize(); 
printf("系统当前内存大小为 %d 字节,内存释放完成\n",g_memsize);

• 由 Leung 写于 2020 年 12 月 28 日

• 参考:野火FreeRTOS视频与PDF教程

你可能感兴趣的:(FreeRTOS学习笔记(9)——内存管理)