FreeRTOS 动态内存管理

news/2024/7/20 15:47:06 标签: 内存管理, 数据结构与算法, 嵌入式

以下转载自安富莱电子: http://forum.armfly.com/forum.php

本章节为大家讲解 FreeRTOS 动态内存管理,动态内存管理是 FreeRTOS 非常重要的一项功能,前面
章节讲解的任务创建、 信号量、 消息队列、 事件标志组、 互斥信号量、 软件定时器组等需要的 RAM 空间
都是通过动态内存管理从 FreeRTOSConfig.h 文件定义的 heap 空间中申请的。
动态内存管理介绍
FreeRTOS 支持 5 种动态内存管理方案,分别通过文件 heap_1,heap_2,heap_3,heap_4 和 heap_5
实现,这 5 个文件在 FreeRTOS 软件包中的路径是:FreeRTOS\Source\portable\MemMang。 用户创
建的 FreeRTOS 工程项目仅需要 5 种方式中的一种。
下面将这 5 种动态内存管理方式分别进行讲解。

动态内存管理方式一 heap_1
heap_1 动态内存管理方式是五种动态内存管理方式中最简单的,这种方式的动态内存管理一旦申请
了相应内存后,是不允许被释放的。 尽管如此,这种方式的动态内存管理还是满足大部分嵌入式应用的,
因为这种嵌入式应用在系统启动阶段就完成了任务创建、 事件标志组、 信号量、 消息队列等资源的创建,
而且这些资源是整个嵌入式应用过程中一直要使用的,所以也就不需要删除,即释放内存。 FreeRTOS 的
动态内存大小在 FreeRTOSConfig.h 文件中进行了定义:
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) ) //单位字节
用户通过函数 xPortGetFreeHeapSize 就能获得 FreeRTOS 动态内存的剩余,进而可以根据剩余情况优化
动态内存的大小。 heap_1 方式的动态内存管理有以下特点:
项目应用不需要删除任务、 信号量、 消息队列等已经创建的资源。
具有时间确定性,即申请动态内存的时间是固定的并且不会产生内存碎片。
确切的说这是一种静态内存分配,因为申请的内存是不允许被释放掉的。
动态内存管理方式二 heap_2
与 heap_1 动态内存管理方式不同,heap_2 动态内存管理利用了最适应算法,并且支持内存释放。
但是 heap_2 不支持内存碎片整理,动态内存管理方式四 heap_4 支持内存碎片整理。 FreeRTOS 的动态
内存大小在 FreeRTOSConfig.h 文件中进行了定义:
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) ) //单位字节
用户通过函数 xPortGetFreeHeapSize 就能获得 FreeRTOS 动态内存的剩余,但是不提供动态内存是
如何被分配成各个小内存块的信息。 另外,就是用户可以根据剩余情况优化动态内存的大小。 heap_2 方
式的动态内存管理有以下特点:
不考虑内存碎片的情况下,这种方式支持重复的任务、 信号量、 事件标志组、 软件定时器等内部资源
的创建和删除。
如果用户申请和释放的动态内存大小是随机的,不建议采用这种动态内存管理方式,比如:
项目应用中需要重复的创建和删除任务,如果每次创建需要动态内存大小相同,那么 heap_2 比
较适合,但每次创建需要动态内存大小不同,那么方式 heap_2 就不合适了,因为容易产生内存
碎片,内存碎片过多的话会导致无法申请出一个大的内存块出来,这种情况使用 heap_4 比较合
适。
项目应用中需要重复的创建和删除消息队列,也会出现类似上面的情况,这种情况下使用 heap_4
比较合适。
直接的调用函数 pvPortMalloc() 和 vPortFree()也容易出现内存碎片。 如果用户按一定顺序成
对的申请和释放,基本没有内存碎片的,而不按顺序的随机申请和释放容易产生内存碎片。
如果用户随机的创建和删除任务、 消息队列、 事件标志组、 信号量等内部资源也容易出现内存碎片。
heap_2 方式实现的动态内存申请不具有时间确定性,但是比 C 库中的 malloc 函数效率要高。
大部分需要动态内存申请和释放的小型实时系统项目可以使用 heap_2。 如果需要内存碎片的回收机
制可以使用 heap_4。
动态内存管理方式三 heap_3
这种方式实现的动态内存管理是对编译器提供的 malloc 和 free 函数进行了封装,保证是线程安全的。
heap_3 方式的动态内存管理有以下特点:
需要编译器提供 malloc 和 free 函数。
不具有时间确定性,即申请动态内存的时间不是固定的。
增加 RTOS 内核的代码量。
另外要特别注意一点,这种方式的动态内存申请和释放不是用的 FreeRTOSConfig.h 文件中定义的
heap空间大小,而是用的编译器设置的heap空间大小或者说STM32启动代码中设置的heap空间大小,
比如 MDK 版本的 STM32F103 工程中 heap 大小就是在这里进行的定义:

动态内存管理方式四 heap_4
与 heap_2 动态内存管理方式不同,heap_4 动态内存管理利用了最适应算法,且支持内存碎片的回
收并将其整理为一个大的内存块。 FreeRTOS 的动态内存大小在 FreeRTOSConfig.h 文件中进行了定义:
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) ) //单位字节
heap_4 同时支持将动态内存设置在指定的 RAM 空间位置。
用户通过函数 xPortGetFreeHeapSize 就能获得 FreeRTOS 动态内存的剩余,但是不提供动态内存是
如何被分配成各个小内存块的信息。 使用函数 xPortGetMinimumEverFreeHeapSize 能够获取从系统启
动到当前时刻的动态内存最小剩余,从而用户就可以根据剩余情况优化动态内存的大小。 heap_4 方式的
动态内存管理有以下特点:
可以用于需要重复的创建和删任务、 信号量、 事件标志组、 软件定时器等内部资源的场合。
随机的调用 pvPortMalloc() 和 vPortFree(),且每次申请的大小都不同,也不会像 heap_2 那样产
生很多的内存碎片。
不具有时间确定性,即申请动态内存的时间不是确定的,但是比 C 库中的 malloc 函数要高效。
heap_4 比较实用,本教程配套的所有例子都是用的这种方式的动态内存管理,用户的代码也可以直
接调用函数 pvPortMalloc() 和 vPortFree()进行动态内存的申请和释放。
动态内存管理方式五 heap_5
有时候我们希望 FreeRTOSConfig.h 文件中定义的 heap 空间可以采用不连续的内存区,比如我们希
望可以将其定义在内部 SRAM 一部分,外部 SRAM 一部分,此时我们就可以采用 heap_5 动态内存管理
方式。另外,heap_5 动态内存管理是在 heap_4 的基础上实现的。
heap_5 动态内存管理是通过函数 vPortDefineHeapRegions 进行初始化的,也就是说用户在创建任
务 FreeRTOS 的内部资源前要优先级调用这个函数 vPortDefineHeapRegions,否则是无法通过函数
pvPortMalloc 申请到动态内存的。
函数 vPortDefineHeapRegions 定义不同段的内存空间采用了下面这种结构体:

定义的时候要注意两个问题,一个是内存段结束时要定义 NULL。另一个是内存段的地址是从低地址到高
地址排列。
用户通过函数 xPortGetFreeHeapSize 就能获得 FreeRTOS 动态内存的剩余,但是不提供动态内存是
如何被分配成各个小内存块的信息。 使用函数 xPortGetMinimumEverFreeHeapSize 能够获取从系统启
动到当前时刻的动态内存最小剩余,从而用户就可以根据剩余情况优化动态内存的大小。

五种动态内存方式总结
五种动态内存管理方式简单总结如下,实际项目中,用户根据需要选择合适的:
heap_1:五种方式里面最简单的,但是申请的内存不允许释放。
heap_2:支持动态内存的申请和释放,但是不支持内存碎片的处理,并将其合并成一个大的内存块。
heap_3:将编译器自带的 malloc 和 free 函数进行简单的封装,以支持线程安全,即支持多任务调
用。
heap_4:支持动态内存的申请和释放,支持内存碎片处理,支持将动态内存设置在个固定的地址。
heap_5:在 heap_4 的基础上支持将动态内存设置在不连续的区域上。

动态内存和静态内存比较
静态内存方式是从 FreeRTOS 的 V9.0.0 版本才开始有的,而我们本次教程使用的版本是 V8.2.3。所
以静态内存方式我们暂时不做讲解,等 FreeRTOS 教程版本升级时再做讲解。 关于静态内存方式和动态内
存方式的优缺点可以看官方的此贴说明:点击查看
(制作此教程的时候,官方的 FreeRTOS V9.0.0 正式版本还没有发布,所以采用的是当前最新的 V8.2.3)
动态内存 API 函数
动态内存的 API 函数在官方的在线版手册上面没有列出,其实使用也比较简单,类似 C 库的 malloc
和 free 函数,具体使用参看下面的实例说明。
实验练兵场:

 声明一个结构类型:

typedef struct Msg
{
    uint8_t  ucMessageID;
    uint16_t usData[2];
    uint32_t ulData[2];
}MSG_T;

消息队列发送任务:

static void vTaskWork(void *pvParameters)
{
        MSG_T *ptMsg;
    uint8_t ucCount = 0;


    while(1)
    {
        if (key1_flag==1)
        {
            key1_flag=0;
        
        }
        /* K2键按下,向xQueue1发送数据 */
        if(key2_flag==1)
        {
                    key2_flag=0;
                    printf("=================================================\r\n");
                    printf("当前动态内存大小 = %d\r\n", xPortGetFreeHeapSize());
                    ptMsg = (MSG_T  *)pvPortMalloc(sizeof(MSG_T));
                 // ptMsg = (MSG_T  *)pvPortMalloc(32);
                    printf("申请动态内存后剩余大小 = %d\r\n", xPortGetFreeHeapSize());
                
                    ptMsg->ucMessageID = ucCount++; 
                    ptMsg->ulData[0] = ucCount++;
                    ptMsg->usData[0] = ucCount++;
                
                    /* 使用消息队列实现指针变量的传递 */
                    if(xQueueSend(xQueue1,                  /* 消息队列句柄 */
                                 (void *) &ptMsg,           /* 发送结构体指针变量ptMsg的地址 */
                                 (TickType_t)10) != pdPASS )
                    {
                        /* 发送失败,即使等待了10个时钟节拍 */
                        printf("K2键按下,向xQueue2发送数据失败,即使等待了10个时钟节拍\r\n");
                        vPortFree(ptMsg);
                        printf("释放申请的动态内存后大小 = %d\r\n", xPortGetFreeHeapSize());
                    }
                    else
                    {
                        /* 发送成功 */
                        printf("K2键按下,向xQueue2发送数据成功\r\n");
                        /* 由于是低优先级任务向高优先级任务发送消息队列,如果成功的话说明高优先级任务已经执行。
                           并获得了消息队列中的数据,所以我们可以在此处释放动态内存,不会出现高优先级任务还没有
                           获得消息队列数据,我们就将动态内存释放掉了。
                        */                        
                        vPortFree(ptMsg);
                        printf("释放申请的动态内存后大小 = %d\r\n", xPortGetFreeHeapSize());                        
                    }
                //    TIM_Mode_Config();
            
            
        }
    
        vTaskDelay(200);
    }
}

接收任务:

void vTaskBeep(void *pvParameters)
{
    MSG_T *ptMsg;
    BaseType_t xResult;
    const TickType_t xMaxBlockTime = pdMS_TO_TICKS(500); /* 设置最大等待时间为500ms */
    
    while(1)
    {
            xResult = xQueueReceive(xQueue1,                   /* 消息队列句柄 */
                                                            (void *)&ptMsg,             /* 这里获取的是结构体的地址 */
                                                            (TickType_t)xMaxBlockTime);/* 设置阻塞时间 */
            
            
            if(xResult == pdPASS)
            {
                /* 成功接收,并通过串口将数据打印出来 */
                printf("接收到消息队列数据ptMsg->ucMessageID = %d\r\n", ptMsg->ucMessageID);
                printf("接收到消息队列数据ptMsg->ulData[0] = %d\r\n", ptMsg->ulData[0]);
                printf("接收到消息队列数据ptMsg->usData[0] = %d\r\n", ptMsg->usData[0]);
            }
            else
            {
                    /* 超时 */
                    BEEP_TOGGLE;
            }
    }                    
}

实验现象展示:

那么问题就来了:

typedef struct Msg
{
  uint8_t ucMessageID;
  uint16_t usData[2];
  uint32_t ulData[2];
}MSG_T;

这个结构体类型无论是在4字节对齐还是8字节对齐的编译器上,输出都是16字节。keil默认4字节对齐。

申请之前,显示:当前动态内存大小 = 23480

申请之后,显示:申请动态内存后剩余大小 = 23456

奇怪了,怎么会减少了24个字节呢?明明只申请了16字节啊。

heap_4文件也就是我们所有实验使用的堆内存文件,它使用一个链表结构来跟踪记录空闲内存块。结构体定义为:

typedef struct A_BLOCK_LINK  

{  

  struct A_BLOCK_LINK *pxNextFreeBlock;   /*指向列表中下一个空闲块*/  

  size_t xBlockSize;                      /*当前空闲块的大小,包括链表结构大小*/  

} BlockLink_t;

与第二种内存管理策略一样,空闲内存块也是以单链表的形式组织起来的,BlockLink_t类型的局部静态变量xStart表示链表头,但第四种内存管理策略的链表尾保存在内存堆空间最后位置,并使用BlockLink_t指针类型局部静态变量pxEnd指向这个区域(第二种内存管理策略使用静态变量xEnd表示链表尾),如下图所示。
第四种内存管理策略和第二种内存管理策略还有一个很大的不同是:第四种内存管理策略的空闲块链表不是以内存块大小为存储顺序,而是以内存块起始地址大小为存储顺序,地址小的在前,地址大的在后。这也是为了适应合并算法而作的改变。

整个有效空间组成唯一一个空闲块,在空闲块的起始位置放置了一个链表结构,用于存储这个空闲块的大小和下一个空闲块的地址。由于目前只有一个空闲块,所以空闲块的pxNextFreeBlock指向指针pxEnd指向的位置,而链表xStart结构的pxNextFreeBlock指向空闲块。xStart表示链表头,pxEnd指向位置表示链表尾。
        当申请x字节内存时,实际上不仅需要分配x字节内存,还要分配一个BlockLink_t类型结构体空间,用于描述这个内存块,结构体空间位于空闲内存块的最开始处。当然,申请的内存大小和BlockLink_t类型结构体大小都要向上扩大到对齐字节数的整数倍。

这个扩展怎么理解呢?我们的keil默认是4字节对齐的,那么内存地址开始处,其值一定要能被4整除,例如一个地址现在是0x01,那么我们存放一个int四字节的变量,并不能从地址0x01处开始,而必须地址扩展到0x04,这样才可以整除4.这个在C语言中已经做过分析。有了这个之后,我们看源码知道,还需要把BlockLink_t类型的结构放在我们申请的内存开始处,这就证明了,我们的消耗的堆内存,是等于字节对齐要求之后,BlockLink_t类型结构占用的字节  +   申请的字节数。

BlockLink_t类型结构的元素,第一个是个指针,在keil编译器中,一个指针4个字节,第二个是个size_t类型的元素,siez_t在我们使用的环境下,是unsigned int的别名,也是占用4个字节,这样,相当于我们实际消耗的堆内存,是申请的内存经过字节对齐之后,再加上8个字节的大小的。

现在举例说明:

 现在我把申请ptMsg = (MSG_T  *)pvPortMalloc(sizeof(MSG_T));换成:

ptMsg = (MSG_T  *)pvPortMalloc(30);

输出如下:

不是说申请内存加8字节码?这里为什么还差2字节,申请前23480,申请后23440.注意,我前面说的是内存对齐之后,再加上8字节,我申请30字节的内存,会被扩展成32字节,这样才能满足四字节对齐的要求。

我们再测试,把申请的内存换成ptMsg = (MSG_T  *)pvPortMalloc(32);这样的输出,必然和上面一样:

 


http://www.niftyadmin.cn/n/1559295.html

相关文章

深入Java设计模式之访问者模式

访问者模式介绍 最复杂的设计模式,并且使用频率不高,《设计模式》的作者评价为:大多情况下,你不需要使用访问者模式,但是一旦需要使用它时,那就真的需要使用了。 访问者模式是一种将数据操作和数据结构分…

2021天梯赛训练-7——7-10 深入虎穴 (25分)

著名的王牌间谍 007 需要执行一次任务,获取敌方的机密情报。已知情报藏在一个地下迷宫里,迷宫只有一个入口,里面有很多条通路,每条路通向一扇门。每一扇门背后或者是一个房间,或者又有很多条路,同样是每条路…

深入Java设计模式之组合模式

主要解决:它在我们树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以像处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。 何时使用: 1、您想表示对象的部分-整体层次结构&am…

PAT (Basic Level) Practice (中文)——1030 完美数列 (25分)

给定一个正整数数列,和正整数 p,设这个数列中的最大值是 M,最小值是 m,如果 M≤mp,则称这个数列是完美数列。 现在给定参数 p 和一些正整数,请你从中选择尽可能多的数构成一个完美数列。 输入格式&#x…

Codeforces 86D Powerful array (莫队)

D. Powerful arraytime limit per test5 secondsmemory limit per test256 megabytesinputstandard inputoutputstandard outputAn array of positive integers a1, a2, ..., an is given. Let us consider its arbitrary subarray al, al  1..., ar, where 1 ≤ l ≤…

基于Hbase的微博案例

需求 1、 发布微博内容 a. 在微博内容表中 添加一条数据(发布者) b. 在微博内容接收邮件箱表对所有粉丝用户添加数据(订阅者) scan weibo:receive-content-email,{VERSIONS=>5} 2、添加关注用户 a. 在微博用户关系表中 添加新的好友关注(attends) b. 从…

2021天梯赛训练-8——7-11 特殊最小成本修路 (10分)请大佬指正

n个城镇之间目前有一些道路连接,但道路都是年久失修的土道。现在政府准备将其中一些土道改造为标准公路,希望标准公路能够将所有城镇连通且总成本最小,但其中有一个城镇比较特殊,受地形等限制,最多只能有两条标准公路通…

怎样给filter加入自己定义接口

给一个filter加入接口,过程例如以下:1、建立一个声明接口的头文件“Interface.h” 。内容包含指定接口的GUID(使用GuidGen.exe)以及接口函数的声明。记得加 initguid.h 的include,不然使用时会出现"无法解析的外部符号_IID_&…