1. FreeRTOS 内存管理简介

在这里插入图片描述

  静态方法创建任务、队列、信号量等对象的 API 函数一般是以“Static”结尾的,例如静态创建任务的 API 函数xTaskCreateStatic()。使用静态方式创建各种对象时,需要用户提供各种内存空间,例如任务的栈空间、任务控制块所用内存空间等等,并且使用静态方式占用的内存空间一般固定下来了,即使任务、队列等被删除后,这些被占用的内存空间也没有其他用途。

  在使用动态方式管理内存的时候,FreeRTOS 就能够在创建任务、队列、信号量等对象的时候,自动地从 FreeRTOS 管理的内存堆中申请所创建对象所需的内存,在对象被删除后,又可以将这块内存释放会 FreeRTOS 管理的内存堆,这样看来,动态方式管理内存相比与静态方式,显得灵活许多。


FreeRTOS 一共提供了 5 种动态内存管理算法,这 5 种动态内存管理算法本别对应了 5 个C 源文件,分别为:heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c

文件 原理 特点
heap_1.c 仅支持单向分配,无释放功能。内存一经分配即永久占用,适用于确定性要求高且无需动态删除对象的系统(如安全关键型嵌入式系统) 实现简单、无碎片风险,但灵活性极低
heap_2.c 允许申请和释放内存,但不能合并相邻的空闲内存块。适用于重复创建和删除相同大小任务的场景 长期运行后碎片化严重,已逐渐被heap_4.c取代
heap_3.c 封装标准库的malloc和free,通过挂起调度器(vTaskSuspendAll())保证线程安全。堆大小由链接器决定,不受configTOTAL_HEAP_SIZE限制 分配速度较慢且不确定性高,适用于需要兼容现有代码的场景
heap_4.c 允许申请和释放内存,并且能够合并相邻的空闲内存块,减少内存碎片的产生。支持动态分配不同大小的内存块,是通用型应用的首选方案 分配效率高于标准库,且能通过xPortGetMinimumEverFreeHeapSize()监控内存使用情况
heap_5.c 能够管理多个非连续内存区域的 heap_4(如同时使用内部RAM和外部SRAM)。需通过vPortDefineHeapRegions()显式初始化多个内存区域 复杂硬件环境下的内存扩展需求

2. 函数接口

在这里插入图片描述

3. 内存管理算法

3.1 heap_1 算法

heap_1 内存管理算法的内存释放函数并没有实现,因此使用heap_1 内存管理算法申请的内存,是无法释放的。
heap_1 内存管理算法的申请内存函数的实现非常简单,就是从内存堆的低地址开始往高地址分配内存,内存堆利用率是非常高的,除了内存堆起始地址的位置可能会因地址对齐产生一小块无用内存外,内存堆中其余的内存空间都可以用来分配,并且也不会产生内存碎片。

heap_1 内存管理算法具有如下特性:

  1. 适用于一旦创建好任务、队列、信号量等就不会删除的应用,实际上大多数的 FreeRTOS 应用都是这样的。
  2. 具有确定性,体现在每次执行的时间都是一样的,而且不会产生内存碎片。
  3. 实现的方式非常简单,分配的内存都是从一个静态分配的数组中分配的,因此也意味着并不适用于那些真正需要动态申请和释放内存的应用。

3.1.1 pvPortMalloc()

在这里插入图片描述

  heap_1 内存管理算法管理的内存堆是一个数组,在使用内存申请函数之前,需要将管理的内存进行初始化,需要将变量pucAlignedHeap指向内存域第一个地址对齐处,因为系统管理的内存其实是一个大数组,而编译器为这个数组分配的起始地址是随机的,不一定符合系统的对齐要求,这时候要进行内存地址对齐操作。比如数组ucHeap 的地址从0x20000123 处开始,系统按照 8 字节对齐
在这里插入图片描述

(1)如果系统要求内存对齐的字节不是按 1 字节对齐,那么就把要申请的内存大小 xWantedSize 按照要求对齐。举个例子,如果系统设置按8 字节对齐,我们本来想要申请的内存大小 xWantedSize 是 30 个字节,与 portBYTE_ALIGNMENT_MASK 相与的结果是 2,这代表着我们申请的内存与系统设定对齐不一致,为了内存统一对齐,系统会再多给我们分配 2 个字节,也就是32 个字节。实际上可能我们不应该用到后面的2个字节,因为我们只申请了30 个字节
(2)系统调用了vTaskSuspendAll() 函数挂起调度器,保证申请内存任务安全,避免分配时被切任务导致出错,因为内存申请是不可重入的(使用了静态变量)
(3)如果内存申请函数是第一次使用,那必须保证堆内存起始地址 pucAlignedHeap 也是按照指定内存对齐要求进行对齐,通过这里可以知道,初始化 pucAlignedHeap 时并不是一定等于&ucHeap[0]的,而是会根据字节对齐的要求,在 &ucHeap[0]和&ucHeap[portBYTE_ALIGNMENT] 之间
(4)在申请内存的时候进行边界检测,如果已经使用的内存空间加上新申请的内存大小小于系统能够提供的内存大小,表示目前有足够的可用内存空间,那么系统就从管理的内存中取一块分配给用户,configADJUSTED_HEAP_SIZE 是一个宏定义,表示系统真正管理的内存大小
(5)获取申请的内存空间起始地址并且保存在返回值中,并且更新索引,记录目前申请了多少内存,在下一次调用的时候进行偏移
(6)恢复调度器
(7)如果内存分配不成功,这里最可能是内存堆空间不够用了。如果用户使能了内存申请失败钩子函数这个宏定义,那么在内存申请失败的时候会调用 vApplicationMallocFailedHook()钩子函数,这个钩子函数由用户实现,通常可以输出内存申请失败的相关提示
(8)返回申请成功的内存起始地址或者NULL

3.2 heap_2 算法

在这里插入图片描述

相比于 heap_1 内存管理算法,heap_2 内存管理算法使用了最适应算法,以支持释放先前申请的内存,但是 heap_2 内存管理算法并不能将相邻的空闲内存块合并成一个大的空闲内存块,因此 heap_2 内存管理算法不可避免地会产生内存碎片。内存碎片是由于多次申请和释放内存,但释放的内存无法与相邻的空闲内存合并而产生的。
heap_2 内存管理算法具有如下特性:

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

heap_2.c 方案采用的内存管理算法虽然是高效但还是有缺陷的,由于在释放内存时不会将相邻的内存块合并,所以这可能造成内存碎片。
举个例子,假设用户先申请 128 字节内存,然后释放,此时系统释放的 128 字节内存可以重复被利用;如果用户再接着申请 64k 的字节内存,那么一个本来128 字节的大块就会被分为两个64 字节的小块,如果这种情况经常发生,就会导致每个空闲块都可能很小,最终在申请一个大块时就会因为没有合适的空闲内存块而申请失败,这并不是因为总的空闲内存不足,而是无法申请到连续可以的大块内存

3.2.1 pvPortMalloc()

在这里插入图片描述

heap_2 内存管理算法申请内存的过程,大致如下:

  1. 因为空闲块链表中的空闲内存块是按照内存块的大小从小到大排序的,因此从头开始遍历空闲块链表,找到第一个大小适合的空闲内存块。
  2. 找到大小适合的空闲内存块后,由于找到的空闲内存块可能比需要申请的内存大,因此需要将整个内存块分为两个小的内存块,其中一个内存块的大小就是需要申请内存的大小,另一个小内存块作为空闲内存块重新插入空闲块链表。
  3. heap_2 内存管理算法的释放函数很简单,就是将带释放的内存块插入到空闲块链表中。

在这里插入图片描述

(1)系统调用了 vTaskSuspendAll() 函数挂起调度器,保证申请内存任务安全,避免分配时被切任务导致出错,因为内存申请是不可重入的(使用了静态变量)
(2)如果是第一次调用内存分配函数,先调用 prvHeapInit()函数初始化内存堆

  1. 按照内存管理的要求,所有归 FreeRTOS 管理的内存堆都需要按指定的内存对齐字节数对齐,这里当然也不例外,保证 pucAlignedHeap 也是按照指定内存要求对齐的
  2. 空闲内存链表头部初始化,空闲内存块头部是一个索引,用于查找能用的内存块,所以 xStart 的pxNextFreeBlock成员变量指向对齐后的内存起始地址 pucAlignedHeap。并且空闲内存块链表的头部是没有可用的内存空间的,所以 xStart 的xBlockSize成员变量的值为 0
  3. 同理,初始化空闲内存链表尾部节点,尾部只是一个标记,当遍历空闲链表到这里的时候,表示已经没有可用的内存块了,所以 xEnd 的 pxNextFreeBlock 成员变量为 NULL,并且空闲内存块链表头部与尾部都是不可用的,至于 xEnd 的 xBlockSize 成员变量的值是什么并不重要,但是为了方便排序,FreeRTOS 给其赋值为 configADJUSTED_HEAP_SIZE,这个就是管理内存最大的值了,所以,无论当前内存块的内存是多大的,在初始化完成之后,空闲内存块链表会按内存块大小进行升序排列
  4. 将 pxFirstFreeBlock 放入空闲链表中,因为空闲内存块链表除了要有头部与尾部,还需要有真正可用的内存,而第一块可用的内存就是 pxFirstFreeBlock,内存块的起始地址就是对齐后的起始地址 pucAlignedHeap, 内存块的大小是系统管理的内存大小configADJUSTED_HEAP_SIZE,并且在内存块链表中的下一个指向就是尾部节点 xEnd

(3)在申请内存的时候,需要调整要分配的内存值,必须增加上链表结构体所占的内存空间,heapSTRUCT_SIZE 表示链表结构体节点经过内存对齐后的内存大小,因为每一块被申请出去的内存块的头部都要放一个BlockLink_t 类型的节点来管理,因此这里需要人为的扩充下申请的内存大小
(4)需要申请的内存大小与系统要求对齐的字节数不匹配,需要进行内存对齐
(5)如果当前的空闲内存足够满足用户申请的内存大小,就进行内存申请操作,系统会从空闲内存块链表的头部开始找,如果该空闲内存块的大小大于用户想要申请的内存大小 xWantedSize,那么就从这块内存中分离出一部分用户需要的内存大小,剩余的内存则生成新的内存块插入空闲内存块链表中
(6)从空闲内存块链表头部开始查找大小符合条件的空闲内存,直到满足用户要求或者遍历完链表才退出循环
(7)如果搜索到链表尾 xEnd,说明没有找到合适的空闲内存块,否则进行下一步处理
(8)能执行到这里,说明已经找到合适的内存块了,找到内存块,就返回内存块地址。注意了:这里返回的是内存块起始地址加上内存块链表结构体空间的偏移地址,因为内存块头部需要有一个节点用于保存内存相关信息
(9)因为这个内存块被用户使用了,需要从空闲内存块链表中移除
(10)分配到内存后,系统还要再看看这个内存块的内存空间够不够多,能不能分成两个,申请的内存块就给用户,剩下的内存就留出来,放到空闲内存块链表中作为下一次内存块申请,这样子就能节约内存
(11)去除分配出去的内存,在剩余内存块的起始位置放置一个链表节点,用来记录该空闲内存块的信息
(12)通过计算得到剩余的内存大小,并且赋值给剩余内存块链表节点中的 xBlockSize 成员变量,方便下一次的内存查找
(13)同时也对当前申请的内存进行保存信息处理,节点中的成员变量 xBlockSize 的值为当前申请的内存大小
(14)将被切割而产生的新空闲内存块添加到空闲链表中
(15)恢复调度器运行
(16)如果内存分配不成功,这里最可能是内存堆空间不够用了。如果用户使能了内存申请失败钩子函数这个宏定义,那么在内存申请失败的时候会调用 vApplicationMallocFailedHook()钩子函数,这个钩子函数由用户实现,通常可以输出内存申请失败的相关提示
(17)返回申请成功的内存起始地址或者NULL


3.2.2 vPortFree()

  分配内存的过程简单,那么释放内存的过程更简单,只需要向内存释放函数中传入要释放的内存地址,那么系统会自动向前索引到对应链表节点,并且取出这块内存块的信息,将这个节点插入到空闲内存块链表中,将这个内存块归还给系统

在这里插入图片描述


在这里插入图片描述

(1)根据要释放的内存块进行地址偏移找到对应的链表节点
(2)挂起调度器,内存的操作都需要挂起调度器
(3)将要释放的内存块添加到空闲链表,prvInsertBlockIntoFreeList 是一个宏定义,就是对链表的简单操作,将释放的内存块按内存大小插入空闲内存块链表中。然后系统更新一下表示未分配内存大小的变量 xFreeBytesRemaining
(4)恢复调度器

3.3 heap_3 算法

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

  1. 需要链接器提供一个堆,还需要编译器的库提供用于申请内存的函数 malloc()和用于释放内存的函数 free()
  2. 具有不确定性
  3. 很可能增大RTOS 内核的代码大小

heap_3 内存管理算法本质使用的是调用标准 C 库提供的内存管理函数,标准 C 库的内存管理需要链接器设置好一个堆,这个堆将作为内存管理的内存堆使用,在启动文件中可以配置这个堆的大小,如下所示:
在这里插入图片描述

3.3.1 pvPortMalloc()

在这里插入图片描述

3.3.2 vPortFree()

在这里插入图片描述

3.4 heap_4 算法

heap_4 内存管理算法使用了首次适应算法,与 heap_2 内存管理算法一样,heap_4 内存管理算法也支持内存的申请与释放,并且 heap_4 内存管理算法还能够将空闲且相邻的内存进行合并,从而减少内存碎片的现象。


heap_4 内存管理算法具有如下特性:

  1. 适用于在程序中多次创建和删除任务、队列、信号量等的应用。
  2. 与 heap_2 内存管理算法相比,即使多次分配和释放随机大小的内存,产生内存碎片的几率也要小得多。
  3. 具有不确定性,但是执行的效率比标准 C 库的内存管理高得多。

3.4.1 pvPortMalloc()

在这里插入图片描述

  • heap_4 内存管理算法的内存堆与 heap_1、heap_2 内存管理算法的内存堆一样,都是一个数组
  • heap_4 内存管理算法中定义的内存堆与 heap_1、heap_2 内存管理算法一样,可以在FreeRTOSConfig.h 文件中配置 configTOTAL_HEAP_SIZE 配置项,以配置内存堆的字节大小,同样地,也可以用过 configAPPLICATION_ALLOCATED_HEAP 配置项将内存堆定义在指定的内存地址中。
  • 用户可以通过函数 xPortGetFreeHeapSize()获取内存堆中未分配的内存总量,根据系统运行时内存堆中剩余的内存空间大小,就可以针对性地对 configTOTAL_HEAP_SIZE 配置项进行优化配置。
  • 与 heap_2 内存管理算法不同的是,heap_4内存管理算法中空闲块链表中的内存块并不是按照内存块大小的顺序从小到大排序,而是按照空闲块链表中内存块的起始地址大小从小到大排序,这也是为了后续往空闲块链表中插入内存块时,能够将相邻的内存块合并。
  • heap_4 内存管理算法整体与 heap_2 内存管理算法很相似,但是 heap_4 内存管理算法相较于 heap_2 内存管理算法能够将物理内存空间上相邻的两个空闲内存块合并成一个大的空闲内存块,而这正是在将空闲内存块插入空闲块链表的时候实现的。
  • 与 heap_2 内存管理算法将空闲块链表中的空闲内存块按照内存块的内存大小从小到大排序的方式不同,heap_4 内存管理算法是将空闲内存块链表中的空闲内存块按照内存块在物理内存上的起始地址从低到高进行排序的,也正是因此,才能够更加方便地找出物理内存地址相邻的空闲内存块,并将其进行合并。
  • 将空闲内存块插入空闲块链表之前,会先从头开始遍历空闲块链表,按照内存块在物理内存上起始地址从低到高的排序规则,找到空闲块要插入的位置。接着判断待插入空闲内存块的起始地址或结束地址是否分别与该位置前面内存块的结束地址或该位置后面内存块的起始地址相同,如果相同侧表示待插入的空闲内存块在物理地址上与该位置前面的内存块或该位置后面的内存块相邻,那么就将响铃的两个空闲内存块合并成一个大的内存块,再将这个大的内存块插入到空闲块链表中,这个操作的示意图如下所示(以待插入空闲内存块与找到位置的上一个内存块相邻为例)

在这里插入图片描述

  • xFreeBytesRemaining:表示当前系统中未分配的内存堆大小
  • xMinimumEverFreeBytesRemaining:表示未分配内存堆空间历史最小的内存值,只有记录未分配内存堆的最小值,才能知道最坏情况下内存堆的使用情况。
  • xBlockAllocatedBit:这个变量在内存堆初始化的时候被初始化,初始化将它能表示的数值的最高位置 1。比如对于 32 位系统,这个变量被初始化为 0x80000000(最高位为 1)。heap_4.c 内存管理方案使用xBlockAllocatedBit 来标识一个内存块是否已经被分配使用了(是否为空闲内存块),如果内存块已经被分配出去,则该内存块上的链表节点的成员变量 xBlockSize 会按位或上这个变量(即 xBlockSize 最高位置 1),而在释放一个内存块时,则会把 xBlockSize 的最高位清零,表示内存块是空闲的。

(1)内存堆初始化

  1. 按照内存管理的要求,所有归 FreeRTOS 管理的内存堆都需要按指定的内存对齐字节数对齐,
  2. 按照内存管理的要求,所有归 FreeRTOS 管理的内存堆都需要按指定的内存对齐字节数对齐,
  3. 空闲内存链表头部初始化,作用与heap_2.c 方案一样, xStart 的 pxNextFreeBlock 成员变量指向对齐后的内存起始地址 pucAlignedHeap,xStart 的 xBlockSize 成员变量的值为0
  4. 同理,初始化空闲内存链表尾部节点,计算pxEnd 的位置,它的值为内存尾部向前偏移一个 BlockLink_t 结构体大小,偏移出来的这个BlockLink_t 就是 pxEnd。尾部只是一个标记,当遍历空闲链表到这里的时候,表示已经没有可用的内存块了,所以pxEnd 的pxNextFreeBlock 成员变量为NULL,与heap_2.c 方案不同的是链表的尾部节点不是静态的,而是放在了内存的最后
  5. 将 pxFirstFreeBlock 放入空闲链表中,因为空闲内存块链表除了要有头部与尾部,还需要有真正可用的内存,而第一块可用的内存就是 pxFirstFreeBlock,内存块的起始地址就是对齐后的起始地址pucAlignedHeap, 内存块的大小是系统管理的内存大小 configADJUSTED_HEAP_SIZE,并且在内存块链表中的下一个指向就是尾部节点 pxEnd
  6. 更新统计变量
  7. 这个xBlockAllocatedBit 比较特殊,这里被设置为最高位为1 其余为0 的一个size_t 大小的值,这样任意一个size_t 大小的值和xBlockAllocatedBit 进行按位与操作,如果该值最高位为1,那么结果为1,否则结果为0,FreeRTOS 利用这种特性标记一个内存块是否空闲的

(2)这里 xWantedSize 的大小有要求,需要最高位为 0。因为后面 BlockLink_t 结构体中的xBlockSize 的最高位需要使用这个成员的最高位被用来标识这个块是否空闲,因此要求申请的块大小不能使用这个位
(3)将被切割而产生的新空闲内存块添加到空闲链表中,这里与 heap_2.c 方案不一样,这里插入空闲内存块链表的时候会通过合并算法将可以合并成大内存块的相邻内存块进行合并

  1. 首先找到和pxBlockToInsert 相邻的前一个空闲内存,找到之后就会退出for 循环
  2. 循环结束后,如果前一个内存块的尾部地址恰好是 pxBlockToInsert 的头部地址,那代表这两个内存块是连续的,可以合并,那么就把 pxBlockToInsert 合并到该内存块中
  3. 将 pxBlockToInsert 合并入pxIterator 中。pxIterator 的大小就是本身大小再加上pxBlockToInsert 的大小
  4. 同理,再判断pxBlockToInsert 是否和后面的空闲内存相邻,如果 pxBlockToInsert 的尾部地址是下一个内存块的头部地址,那么也是说明这连个内存块是连续的,可以合并。
  5. 还要判断pxBlockToInsert 的下一个内存块是不是尾部节点pxEnd,为什么呢?因为尾部节点就是放在系统管理的内存块最后的地址上,而 xStart不是,所以这里要判断一下。如果不是 pxEnd,并且还连续的,那么就将后面的内存合入 pxBlockToInsert,并用 pxBlockToInsert 代替该内存在链表中的位置,pxBlockToInsert 的大小就是本身大小再加上下一个内存块的大小
  6. 如果pxBlockToInsert 的下一个内存块是pxEnd,那就不能合并,将内存块节点的成员变量pxNextFreeBlock 指向pxEnd
  7. 如果pxBlockToInsert 与后面的内存块不相邻,那么只能插入链表了
  8. 判断下前面是否已经合并了,如果合并了,就不用再更新链表了,否则就更新一下与前一个内存块的链表连接关系

(4)如果当前内存大小小于历史最小记录,更新历史最小内存记录
(5)注意这里的xBlockSize 的最高位被设置为1,标记内存已经被申请使用,xBlockAllocatedBit 在内存初始化的时候就被初始化了

3.4.2 vPortFree()

根据传入要释放的内存块地址,偏移之后找到链表节点,然后将这个内存块插入到空闲内存块链表中,在内存块插入过程中会执行合并算法)。最后是将这个内存块标志为“空闲”(内存块节点的 xBlockSize 成员变量最高位清 0)、再更新未分配的内存堆大小即可
按照内存释放的过程,当我们释放一个内存时,如果与它相邻的内存块都不是空闲的,那么该内存块并不会合并,只会被添加到空闲内存块链表中,而如果某个时间段释放了另一个内存块,发现该内存块前面有一个空闲内存块与它在地址上是连续的,那么这两个内存块会合并成一个大的内存块,并插入空闲内存块链表中。

在这里插入图片描述

在这里插入图片描述

(1)根据要释放的内存块进行地址偏移找到对应的链表节点
(2)判断一下内存块是否已经是被分配使用的,如果是就释放该内存块。已经分配使用的内存块在其对应节点的成员变量xBlockSize 最高位为1
(3)将内存块标识为空闲,将节点的成员变量xBlockSize 最高位清0
(4)更新系统当前空闲内存的大小
(5)调用prvInsertBlockIntoFreeList()函数将释放的内存块添加到空闲内存块链表中,在这过程中,如果内存块可以合并就会进行内存块合并,否则就单纯插入空闲内存块链表(按内存地址排序)

3.5 heap_5 算法

  • heap_5 内存管理算法是在 heap_4 内存管理算法的基础上实现的,因为 heap_5 内存管理算法使用与 heap_4 内存管理算法相同的内存分配、释放和合并算法,但是 heap_5 内存管理算法在 heap_4 内存管理算法的基础上实现了管理多个非连续内存区域的能力。
  • heap_5 内 管理算法默认并没有定义内存堆,通过函数vPortDefindHeapRegions()来实现系统管理的内存初始化,在内存初始化未完成前不允许使用内存分配和释放函数。初始化后的内存堆将被作为空闲内存块链接到空闲块链表中,再接下来的内存申请与释放就和 heap_4 内存管理算法一致了。
  • 要注意的是,因为 heap_5 内存管理算法并不会自动创建好内存堆,因此需要用户手动为 heap_5 初始化好作为内存堆的内存区域后,才能够动态创建任务、队列、信号量等对象。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐