FreeRTOS--堆内存管理

迷思 2017-12-09

因为项目需要,最近开始学习FreeRTOS,一开始有些紧张,因为两个星期之前对于FreeRTOS的熟悉度几乎为零,经过对FreeRTOS官网的例子程序的摸索,和项目中问题的解决,遇到了很多熟悉的身影,以前在Linux平台编程的经历给了我一些十分有用的经验,后悔当初没能在第一家公司待下去,浪费了大好时光。好吧,现在还是潜下心来搞搞FreeRTOS吧。

后续都是一系列FreeRTOS相关的随笔,先把FreeRTOS“圣经”--Mastering the FreeRTOS Real Time kernel -- A Hands On Tutorial Guide 20161204好好研读,接连的几个随笔都是我从这本“圣经”中翻译出来的。翻译难免有所疏漏、词不达意,大家凑合着看吧。

从FreeRTOS V9.0.0开始FreeRTOS应用程序可以完全用静态分配内存,而没有必要引入堆内存管理。

章节引言和范围

前提

FreeRTOS是以C源文件的形式提供的,因此成为一名合格的C语言编程人员是使用FreeRTOS的必要条件,因而这个章节假定读者熟悉以下概念:

  • C语言项目是如何构建的,包含不同的编译和链接过程
  • 堆和栈分别是什么
  • 标准C库的malloc()free()函数

动态内存分配以及它和FreeRTOS的关系

从FreeRTOS V9.0.0开始内核对象既可以在编译的时候静态分配,也可以在运行时动态分配。本书随后的章节将会介绍以下内核对象:tasks, queues, semaphoresevent groups。为了尽可能让FreeRTOS易于使用,这些内核对象并不是在编译时静态分配的,而是在运行时动态分配的。内核对象创建时FreeRTOS分配RAM而在内核对象删除时释放内存。这样的策略减少了设计和计划上的努力,简化了API,并且减少了RAM的占用。

动态内存分配是C语言编程的概念,而不是针对FreeRTOS或者多任务编程的概念。它和FreeRTOS是相关的,因为内核对象是动态分配的,并且通用编译器提供的动态内存分配方案对于实时应用程序并不总是适合的。

内存可以使用标准C库的malloc()free()函数来分配,但有可能不适合,或者恰当,因为下几点原因:

  • 在小型嵌入式系统中并不总是可用的
  • 它们的实现可能非常的大,占据了相当大的一块代码空间
  • 他们几乎都不是线程安全的
  • 它们并不是确定的,每次调用这些函数执行的时间可能都不一样
  • 它们有可能产生碎片
  • 它们有可能打乱链接器的配置
  • 如果允许堆空间的生长方向覆盖其他变量占据的内存,它们会成为debug的灾难

动态内存分配的可选项

从FreeRTOS V9.0.0开始内核对象既可以在编译时静态分配也可以在运行时动态分配。如今FreeRTOS把内存分配放在可移植层。这是认识到不同的嵌入式操作有不同的动态内存管理方法和时间要求,因此单个的动态内存分配算法将只适合于应用程序的一个子集。同样,从核心代码库中移除动态内存分配使得应用程序编写者提供自己的特定的实现,如果适合的话。

当FreeRTOS需要RAM的时候,并不是调用malloc(),而是调用pvPortMalloc()。当需要释放RAM的时候,并不是调用free(),而是调用vPortFree()pvPortMalloc()和标准C库的malloc()有同样的函数原型,vPortFree()和标准C库的free()有同样的函数原型。

pvPortMalloc()vPortFree()都是公共函数,因此能够被应用代码调用。

FreeRTOS对于pvPortMalloc()vPortFree()提供了5种实现,后续章节会讲到。FreeRTOS应用程序可以使用其中的一种,或者使用自己的实现。5种实现分别在heap_1.c, heap_2.c, heap_3.c, heap_4.cheap_5.c文件中,都存在于文件夹 FreeRTOS/Source/portable/MemMang 下。

范围

本章节致力于让读者深入理解:

  • FreeRTOS何时分配RAM
  • FreeRTOS 提供的5种内存分配方案
  • 选用哪一种内存分配方案

内存分配方案示例

Heap_4 (其他几种暂不去了解)

heap_1, heap_2 一样,Heap_4也是把数组切割成更小的块。和前面一样,数组是静态声明的,由宏configTOTAL_HEAP_SIZE指定大小,所以这就使得即便数组中的内存还没有被分配出去就让应用程序显得消耗了大量的RAM。

Heap_4使用了最先适应算法来分配内存。和heap_2不同,Heap_4把临近的空闲的存储空间拼凑成一个更大的内存块,这就减少了内存碎片化的风险。

最先适应算法确保了pvPortMalloc()使用第一块空闲的足够大的内存来满足要申请的字节数。考虑下面的情景:

  • 堆里有3块空闲内存块,它们的大小分别是5个字节,200个字节,100个字节
  • 调用pvPortMalloc()来申请20个字节的RAM
    满足字节数要求的第一块空闲RAM块是200个字节的RAM块,因此pvPortMalloc()把大小为200个字节的RAM块分割成两块,一块是20个字节,一块是180个字节,然会返回一个指向20个字节的指针。新的180个字节大小的RAM块将在后续的pvPortMalloc()调用中可用。

Figure 7 演示了 Heap_4 最先适应算法如何拼接内存,同样也演示了内存的分配和释放:

FreeRTOS--堆内存管理
  1. A演示了创建3个任务之后的数组的样子,一大块空的块存在于数组的顶端。
  2. B演示了删除1个任务之后的数组,一大块空的块存在于数组的顶端。被删除的那个任务占据的TCB和栈存储空间现在是空的,并且它们拼接成一个大的空的块。
  3. C演示了FreeRTOS创建了一个Queue。队列是通过xQueueCreate() API 创建的,它是调用pvPortMalloc() 来分配存储空间的。由于Heap_4采用最先适应算法,pvPortMalloc()将会使用第一块大的足够容纳队列的RAM块来分配,在Figure 7中就采用之前删除任务的那一块。然而队列并不完全消耗那个空闲的区块,所以那个RAM块会分成两个部分,未使用的部分将会由后续的pvPortMalloc()占用。
  4. D演示了应用程序直接调用pvPortMalloc()而不是间接地由FreeRTOS API调用之后的情形。用户分配的区块足够小,能够放在第一个空闲的区块中,这个区块就是队列占用的区块和后面的TCB占用的区块之间的那一块。
    删除任务释放的内存,现在被分割成3个区块,第一个区块是队列,第二个区块是用户分配的,第三个区块还是空的。
  5. E 演示了队列删除之后,存储空间也自动释放了。现在用户分配的区块两边都是空闲区块。
  6. F 演示了用户分配的存储空间释放的情形。这个区块现在和两边的空闲区块拼接成了一个更大的空闲区块。

Heap_4并不是确定性的,但是要比标准库函数实现的malloc()free()运行的更快。

设定Heap_4数组的起始地址

此章节包含更高阶的信息,仅仅为了使用Heap_4是没有必要阅读和理解此章节的。

某些时候应用程序开发者需要指定Heap_4数组的起始地址位于某个特定的内存。例如,FreeRTOS 任务的栈是从堆中分配的,就有可能有必要保证堆是分配在快速的内存中,而不是慢速的外存。

默认情况下,Heap_4数组是在heap_4.c源文件中声明的,它的起始地址是由链接器自动确定的。然而,如果在文件FreeRTOSConfig.h中把编译时配置选项configAPPLICATION_ALLOCATED_HEAP设为常量1,那么数组必须由使用FreeRTOS的应用声明。如果把数组声明为应用的一部分,那么应用编写者可以指定数组的起始地址。

如果把文件FreeRTOSConfig.h中的configAPPLICATION_ALLOCATED_HEAP设定为1,那么应用程序源文件中必须声明一个名字为ucHeapuint8_t类型的数组,它的大小有configTOTAL_HEAP_SIZE设定。

把变量放在某个内存地址的语法取决于使用了哪种编译器,下面演示了两种编译器的用法:

  1. Listing 2演示的是GCC编译器声明数组并把数组放在名字为.my_heap的段中。
  2. Listing 3演示的是IAR编译器把数组放在内存绝对地址0x20000000上。

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

Listing 2

uint8_t ucHeap [configTOTAL_HEAP_SIZE] @ 0x20000000;

Listing 3

和堆相关的实用函数

xPortGetFreeHeapSize() API

这个函数可以获取调用时堆中空闲内存的大小,以字节为单位。使用它可以优化堆的大小。例如,当内核对象都创建完毕后调用xPortGetFreeHeapSize()返回2000,那么可以把configTOTAL_HEAP_SIZE减小2000.

需要注意,当使用heap_3时是不能调用这个函数的。

xPortGetMinimumEverFreeHeapSize() API

此函数返回FreeRTOS应用程序开始运行之后曾经存在的最小的未被分配的存储空间的字节数。它的返回值指示了应用程序离将要耗尽堆空间的接近程度。例如xPortGetMinimunEverFreeHeapSize()返回200个字节,那么从应用程序开始运行之后的某个时间,在使用200个字节就会把堆空间用完。

需要注意,xPortGetMinimumEverFreeHeapSize()只在使用Heap_4或者heap_5时生效。

Malloc 失败钩子函数

应用程序可以直接调用pvPortMalloc()。当然在FreeRTOS源文件中每当内核对象创建时也会调用这个函数。此类的内核对象包括任务,队列,信号量和事件组。

和标准库函数malloc()一样,如果pvPortMalloc()因为申请RAM的大小不能满足没能返回一块RAM空间就会返回NULL。如果编程人员调用pvPortMalloc()来创建内核对象,但是返回NULL就说明内核对象没有创建成功。

例子中的所有堆分配方案都可以给pvPortMalloc()配置一个钩子函数(也称作回调函数),当pvPortMalloc()返回NULL时调用这个钩子函数。

如果文件FreeRTOSConfig.h中的configUSE_MALLOC_FAILED_HOOK设置为1,那么应用程序必须提供一个内存分配失败时的钩子函数,它的名字和原型参见如下。只要对这个应用来说是合适的,这个钩子函数可以用任何方法来实现。

void vApplicationMallocFailedHook( void );

声明

欢迎转载,请注明出处和作者,同时保留声明。
作者:LinTeX9527
出处:http://www.cnblogs.com/LinTeX9527/p/8007541.html
本博客的文章如无特殊说明,均为原创,转载请注明出处。如未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

相关推荐