动态内存分配及其与FreeRTOS的相关性

为了使FreeRTOS更易用,内核对象(如任务、队列、信号量、事件组)不在编译期静态分配,而是在运行时动态分配,FreeRTOS在内核对象创建时分配RAM,删除内核对象时释放RAM。

这种策略降低了设计难度,更简单的API,最小化内存占用。动态内存分配时C编程的概念,而非特定于FreeRTOS或多任务的概念,只是FreeRTOS的内核对象是动态分配的,一般编译器提

供的动态内存分配方案并不总是适用于实时应用。

可以使用标准C库函数mallocfree进行内存分配,但他们并不总是适用的,主要原因如下:

  • 在小型嵌入式系统上并不总是可用
  • 他们实现通常很大,占据昂贵的代码空间
  • 他们几乎不是线程安全的
  • 他们不是确定性的,执行函数所花费的时间因调用而异
  • 导致内存碎片化严重
  • 导致链接配置更复杂
  • 无法调试

动态内存分配选项

早期的FreeRTOS使用内存池分配方案,不同大小的内存池是在编译期预先分配的,尽管这是一个实时系统的通用方案,但它不能有效的使用RAM,在非常小的嵌入式系统无法使用,因此被放弃了。

FreeRTOS现在将内存分配视作可移植层的一部分而非核心代码,这是因为不同嵌入式系统对动态内存分配和时间要求不同,因此单一动态内存分配算法值适用某一种应用,所以将动态内存分配代码

从核心代码中删除,使得使用者可以在特定的应用使用合适的动态内存分配算法。

FreeRTOS分配内存使用接口pvPortMalloc而非malloc,释放内存使用vPortFree替代free。

FreeRTOS提供5种内存分配方案,接口统一使用pvPortMallocvPortFree,用户可以使用者5种方案,也可以自己实现。这5种内存分配方案实现在目录FreeRTOS/Source/portable/MemMang下,

命名分别是heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c.

内存分配方案介绍

Heap_1 

小型专用嵌入式系统在调度器运行前只创建任务和内核对象是很常见的,也就是说在应用程序执行前进行动态内存分配,而后一直保持直到应用程序生命周期结束,意味着此方案无需考虑复杂的内存

分配问题,比如确定性和碎片化,只需考虑代码大小即可。

heap_1.c实现基础版内存分配接口pvPortMalloc,未实现内存释放接口vPortFree,使用该算法的应用程序永不删除任务、内核对象。

一些商用型安全关键的系统禁止使用动态内存分配,故选用heap_1方案,以避免内存分配的不确定性。

pvPortMalloc使用heap_1方案,将从数组中分配一个小内存块,这个数组就是FreeRTOS堆内存,数组大小由宏configTOTAL_HEAP_SIZE配置,创建的每个任务都有一个任务控制块和任务栈,图

Figure-5演示了任务创建时聂村的分配过程:

  • A显示任务创建前整个内存数组是空的
  • B显示创建1个任务后的内存数组布局
  • C显示创建3个任务后的内存数组布局

Heap_2

为了向后兼容,heap_2在FreeRTOS发行版中被保留,但不建议在新设计中使用它,可以使用heap_4替代,因为heap_4提供了增强版的功能。heap_2使用最佳匹配算法进行内存分配和释放。

最佳匹配算法确保pvPortMalloc使用大小最接近请求字节数的空闲内存块。

示例考虑以下场景:

  • 堆内存包含3个空闲内存块,分别是5Byte、25Byte、100Byte
  • pvPortMalloc请求分配20Bytes内存

与请求20Byte内存大小最接近的是25Byte空闲内存块,pvPortMalloc将25Byte内存块分成20Byte和5Byte,并返回20Byte内存块指针,剩余的5Byte以供将来分配使用。

heap_2方案不会将相邻的空闲内存块合并为更大的空闲内存块,所以更容易产生内存碎片。当然每次分配的内存大小与随后释放的内存大小一致,则就不会存在内存碎片问题。

heap_2方案适合重复创建和删除任务的应用,前提是分配给创建任务的堆栈大小不改变。

图Figure-6演示了最佳匹配算法的工作过程,任务创建、删除、再次创建

  • A显示创建了3个任务,数组顶部还剩余大块空闲内存
  • B显示删除了1个任务,当前多出了2块小内存,即之前创建任务的任务控制块和任务栈
  • C显示新创建1个任务,创建任务调用2次pvPortMalloc,分别是分配任务控制块和任务栈

每个任务控制大小都一样,所以最佳匹配算法确保在先前释放的任务控制块内存位置重复分配新任务的任务控制块,新创建任务的任务栈与之前释放的任务栈大小相同,所以最佳匹配算法仍在原位置分配任务栈,

数组顶部的未分配的内存自始至终都未触碰

heap_2方案不是确定性的,但比标准库函数mallocmalloc执行快

Heap_3

heap_3方案使用标准库函数mallocfree,堆大小在链接文件中分配,宏configTOTAL_HEAP_SIZE配置的修改无影响。

heap_3方案通过挂起调度器实现函数mallocfree的线程安全。

Heap_4 

跟heap_1、heap_2一样,heap_4也是从数组中分出小块内存,堆内存大小也是宏configTOTAL_HEAP_SIZE配置的。

heap_4使用第一次匹配算法进行内存分配,相比heap_2,heap_4会合并相邻的空闲内存块,将内存碎片风险最小化。第一次匹配算法确保pvPortMalloc分配内存时,总是找到第一个可以满足请求内存大小的

空闲内存块。

示例考虑如下场景:

  • 堆内存包含3个空闲内存块,分别是5Byte、200Byte、100Byte的顺序
  • pvPortMalloc请求分配20Bytes内存

第一次匹配算法就会找到200Byte内存块分配内存,pvPortMalloc将200Byte内存块分成20Byte和180Byte两个内存块,并返回20Byte内存块指针,新的180Byte内存供后续分配使用。

heap_4方案能够合并相邻空闲内存块,最小化内存碎片化风险,并重复分配释放不同大小的内存。

图Figure-7演示了在内存分配分配释放时heap_4第一次匹配算法和合并是如何工作的:

  • A显示创建3个任务后的内存数组布局
  • B显示删除1个任务后的内存数组布局,空闲出了删除任务的任务控制块和任务栈空间,并合并为更大的空闲内存块
  • C显示创建1个FreeRTOS队列,因heap_4的第一次匹配算法机制,pvPortMalloc将队列所需内存分配至刚删除任务释放出的位置,但未占用完有剩余
  • D显示用户代码直接调用pvPortMalloc分配内存,按heap_4的第一次匹配算法机制,之前释放位置重新分配后还有剩余
  • E显示刚创建的队列被删除了,空闲内存分布在用户申请的内存两侧
  • F显示用户代码申请的内存释放了,分散的内存块又合并成大的空闲内存块

heap_4方案不是确定性的,但比标准库函数mallocfree执行快

heap_4方案内存数组起始地址设置:

有时会指定堆内存的位置,任务栈是从堆内存分配的,所以为了快速会将堆内存放在内部RAM而非外部RAM,默认情况下heap_4方案的堆内存是在链接文件中配置的,但若开启了宏

configAPPLICATION_ALLOCATED_HEAP,那内存数组就由应用程序提供,命名是uint8_t ucHeap,大小由宏configTOTAL_HEAP_SIZE配置

将对内存放置在指定位置的语法取决于具体编译器:

  • GCC编译器,将堆内存地址指定到段.my_heap

uint8_t ucHeap[ configTOTAL_HEAP_SIZE ] __attribute__ ( ( section( ".my_heap" ) ) );

  • IAR编译器,将堆内存地址指定到地址0x20000000

uint8_t ucHeap[ configTOTAL_HEAP_SIZE ] @ 0x20000000;

Heap_5 

heap_5使用的内存分配释放算法与heap_4一样,但不一样的是heap_5可以在多段独立的内存空间分配,而heap_4只能用于单个内存空间。heap_5方案使用场景是,运行FreeRTOS的系统提供的堆内存并非连续的一片内存。

在编写时,heap_5是唯一一个必须在调用pvPortMalloc()之前显式初始化的内存分配方案。Heap_5使用vPortDefineHeapRegions() API函数初始化。当使用heap_5时,必须在创建任何内核对象(任务、队列、信号量等)

之前调用vPortDefineHeapRegions()。

vPortDefineHeapRegions用于指定每个单独内存区域的起始地址和大小,这些内存区域共同构成了heap_5所使用的总内存。

void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions );

每个单独的内存区域由HeapRegion_t类型的结构描述。所有可用内存区域的描述作为HeapRegion_t结构的数组传递给vPortDefineHeapRegions()。

typedef struct HeapRegion
{
/* The start address of a block of memory that will be part of the heap.*/
uint8_t *pucStartAddress;
/* The size of the block of memory in bytes. */
size_t xSizeInBytes;
} HeapRegion_t;

示例说明heap_5的多段内存使用过程

在构建项目时,构建过程的链接阶段会为每个变量分配一个RAM地址。可由链接器使用的RAM通常由链接器配置文件(如链接器脚本)描述。在Figure-8 B中,假定链接器脚本包含RAM1上的信息,但不包含RAM2或RAM3上

的信息。因此链接器在RAM1中放置了变量,只留下RAM1地址0x0001nnnn以上的部分可以通过heap_5使用。0x0001nnnn的实际值取决于被链接的应用程序中包含的所有变量的大小。链接器让所有RAM2和RAM3都未使用,

让整个RAM2和RAM3都可以被heap_5使用。

/* Define the start address and size of the three RAM regions. */
#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 )
/* Create an array of HeapRegion_t definitions, with an index for each of the three
* RAM regions, and terminating the array with a NULL address. The HeapRegion_t
* structures must appear in start address order, with the structure that contains the
* lowest start address appearing first. */
const HeapRegion_t xHeapRegions[] =
{
{ RAM1_START_ADDRESS, RAM1_SIZE },
{ RAM2_START_ADDRESS, RAM2_SIZE },
{ RAM3_START_ADDRESS, RAM3_SIZE },
{ NULL, 0 } /* Marks the end of the array. */
};
int main( void )
{
/* Initialize heap_5. */
vPortDefineHeapRegions( xHeapRegions );
/* Add application code here. */
}

如果使用上述所示的代码,分配给地址0x0001nnnn下的heap_5的RAM将与用于保存变量的RAM重叠。为了避免这种情况,xHeapRegions[]数组中的第一个HeapRegion_t结构体可以使用0x0001nnnn的起始地址,而不是0x00010000的起始地址。

但是,这不是一个推荐的解决方案,因为:

  • 起始地址不易确定
  • 链接脚本文件会不定期修改,导致结构HeapRegion_t起始地址也要不停修改
  • 如果链接器使用的RAM和heap_5使用的RAM重叠,构建工具将不知道,因此不能警告应用程序编写器

为解决这种场景,代码实现如下:

第一块内存使用静态数组ucHeap,它是分配在链接文件指定的RAM1中,结构HeapRegion_t起始地址使用ucHeap,所以它将是heap_5管理内存的一部分,这样就避免了频繁的修改,如图Figure-8 C所示

/* Define the start address and size of the two RAM regions not used by the
linker. */
#define RAM2_START_ADDRESS ( ( uint8_t * ) 0x00020000 )
#define RAM2_SIZE ( 32 * 1024 )
#define RAM3_START_ADDRESS ( ( uint8_t * ) 0x00030000 )
#define RAM3_SIZE ( 32 * 1024 )
/* Declare an array that will be part of the heap used by heap_5. The array will be
placed in RAM1 by the linker. */
#define RAM1_HEAP_SIZE ( 30 * 1024 )
static uint8_t ucHeap[ RAM1_HEAP_SIZE ];
/* Create an array of HeapRegion_t definitions. Whereas in Listing 6 the first entry
* described all of RAM1, so heap_5 will have used all of RAM1, this time the first
* entry only describes the ucHeap array, so heap_5 will only use the part of RAM1 that
* contains the ucHeap array. The HeapRegion_t structures must still appear in start
* address order, with the structure that contains the lowest start address appearing
* first. */
const HeapRegion_t xHeapRegions[] =
{
{ ucHeap, RAM1_HEAP_SIZE },
{ RAM2_START_ADDRESS, RAM2_SIZE },
{ RAM3_START_ADDRESS, RAM3_SIZE },
{ NULL, 0 } /* Marks the end of the array. */
};

这种技术的优势是:

  • 不必写死内存起始地址
  • 结构HeapRegion_t中的起始地址是通过链接文件自动设置的,省去了频繁修改的烦恼
  • heap_5管理的内存与链接文件分配的内存不可能重叠
  • ucHeap过大则应用程序link失败

堆内存相关的实用函数

  • 获取空闲内存的大小,可用于优化堆内存大小

size_t xPortGetFreeHeapSize( void );

  • 获取FreeRTOS应用程序自开始执行以来堆中存在的最小未分配字节数,可用于堆内存溢出检查

size_t xPortGetMinimumEverFreeHeapSize( void );

  • 内存分配失败后调用的钩子函数,需要设置宏configUSE_MALLOC_FAILED_HOOK,可用于代码鲁棒性检查

void vApplicationMallocFailedHook( void );

【FreeRTOS】堆内存管理的更多相关文章

  1. FreeRTOS 动态内存管理

    以下转载自安富莱电子: http://forum.armfly.com/forum.php 本章节为大家讲解 FreeRTOS 动态内存管理,动态内存管理是 FreeRTOS 非常重要的一项功能,前面 ...

  2. Linux堆内存管理深入分析(下)

     Linux堆内存管理深入分析 (下半部) 作者@走位,阿里聚安全 0 前言回顾 在上一篇文章中(链接见文章底部),详细介绍了堆内存管理中涉及到的基本概念以及相互关系,同时也着重介绍了堆中chunk分 ...

  3. Linux堆内存管理深入分析(上)

    Linux堆内存管理深入分析(上半部) 作者:走位@阿里聚安全   0 前言 近年来,漏洞挖掘越来越火,各种漏洞挖掘.利用的分析文章层出不穷.从大方向来看,主要有基于栈溢出的漏洞利用和基于堆溢出的漏洞 ...

  4. Linux堆内存管理深入分析

    (上半部) 作者:走位@阿里聚安全 前言 近年来,漏洞挖掘越来越火,各种漏洞挖掘.利用的分析文章层出不穷.从大方向来看,主要有基于栈溢出的漏洞利用和基于堆溢出的漏洞利用两种.国内关于栈溢出的资料相对较 ...

  5. C语言堆内存管理上出现的问题,内存泄露,野指针使用,非法释放指针

    C语言堆内存管理上出现的问题,内存泄露,野指针使用,非法释放指针 (1)开辟的内存没有释放,造成内存泄露 (2)野指针被使用或释放 (3)非法释放指针 (1)开辟的内存没有释放.造成内存泄露,以下的样 ...

  6. 轻量级操作系统FreeRTOS的内存管理机制(一)

    本文由嵌入式企鹅圈原创团队成员朱衡德(Hunter_Zhu)供稿. 近几年来,FreeRTOS在嵌入式操作系统排行榜中一直位居前列,作为开源的嵌入式操作系统之一,它支持许多不同架构的处理器以及多种编译 ...

  7. Linux堆内存管理深入分析 (上半部)【转】

    转自:http://www.cnblogs.com/alisecurity/p/5486458.html Linux堆内存管理深入分析(上半部) 作者:走位@阿里聚安全 0 前言 近年来,漏洞挖掘越来 ...

  8. FreeRTOS的内存管理

    FreeRTOS提供了几个内存堆管理方案,有复杂的也有简单的.其中最简单的管理策略也能满足很多应用的要求,比如对安全要求高的应用,这些应用根本不允许动态内存分配的. FreeRTOS也允许你自己实现内 ...

  9. Linux C 堆内存管理函数malloc()、calloc()、realloc()、free()详解

    C 编程中,经常需要操作的内存可分为下面几个类别: 堆栈区(stack):由编译器自动分配与释放,存放函数的参数值,局部变量,临时变量等等,它们获取的方式都是由编译器自动执行的 堆区(heap):一般 ...

  10. 栈 & 堆 |--> 内存管理

    内存管理: 栈区 [stack]:由编译器自动分配并释放,一般存放函数的参数值,局部变量等 堆区 [heap]:由程序员分配和释放,如果程序员不释放,程序结束时,可能会由操作系统回收 全局区(静态区) ...

随机推荐

  1. 被面试官PUA了:创建索引时一定会锁表?

    索引主要是用于提高数据检索速度的一种机制,通过索引数据库可以快速定位到目标数据的位置,而不需要遍历整个数据集,它就像书籍的目录部分,有它的存在,可以大大加速查询的效率. 那么问题来了:在创建索引时一定 ...

  2. 【scikit-learn基础】--『监督学习』之 LASSO回归

    LASSO(Least Absolute Shrinkage and Selection Operator)回归模型一般都是用英文缩写表示,硬要翻译的话,可翻译为 最小绝对收缩和选择算子. 它是一种线 ...

  3. Spring Boot 2.x 到 3.2 的全面升级指南

    Spring Framework 是一种流行的开源企业级框架,用于创建在 Java Virtual Machine (JVM) 上运行的独立.生产级应用程序.而Spring Boot 是一个工具,可以 ...

  4. OpenEuler22.03安装最新版本Docker

    一.环境及问题 操作系统环境如下: 操作系统:OpenEuler 22.03 LTS 安装方式:最小化安装 在操作系统安装完毕如果直接采用dnf或者yum方式安装docker: sudo dnf in ...

  5. 记一次windows病毒联合排查全过程

    8月2日通过态势感知平台,发现大量内部DNS服务器有恶意请求,且告警描述为:试图解析僵尸网络C&C服务器xmr-eu2.nanopool.org的地址,通过截图可以看到,用户每5分钟会定期向目 ...

  6. uni-app+vue3+ts项目搭建完整流程

    项目代码同步更新至码云 uni-vue3-ts-template 开发前准备 利用 uni-app 开发,有两种方法: 通过 HBuilderX 创建(需安装 HBuilderX 编辑器) 通过命令行 ...

  7. 如何使用TCP/IP开发网络程序

    摘要:进行TCP协议网络程序的编写,关键在于ServerSocket套接字的熟练使用,TCP通信中所有的信息传输都是依托ServerSocket类的输入输出流进行的. 本文分享自华为云社区<Ja ...

  8. GaussDB(for MySQL)如何快速创建索引?华为云数据库资深架构师为您揭秘

    摘要:云服务环境下,如何解决客户基于大量数据创建索引的性能问题,成为云服务厂商的一个挑战.华为云GaussDB(for MySQL)通过引入并行创建索引技术,很好地解决了批量索引创建和临时添加索引等性 ...

  9. 聊聊数仓中TPCD-DS&TPC-H与查询性能的那些事儿

    摘要:详细讲述使用GaussDB(DWS)时,如何使用TPC-DS/TPC-H等标准数据模型,获取DWS的查询性能数据. 本文分享自华为云社区<GaussDB(DWS) <DWS之TPCD ...

  10. 技术+案例详解无监督学习Autoencoder

    摘要:本篇文章将分享无监督学习Autoencoder的原理知识,然后用MNIST手写数字案例进行对比实验及聚类分析. 本文分享自华为云社区<[Python人工智能] 十五.无监督学习Autoen ...