FREERTOS内存管理

内存管理是一个系统基本组成部分,FreeRTOS 中大量使用到了内存管理,比如创建任务、信号量、队列等会自动从堆中申请内存。用户应用层代码也可以 FreeRTOS 提供的内存管理函数来申请和释放内存。

内存管理简介

FreeRTOS 创建任务、队列、信号量等的时候有两种方法,一种是动态的申请所需的 RAM。一种是由用户自行定义所需的 RAM,这种方法也叫静态方法,使用静态方法的函数一般以“Static”结尾,比如任务创建函数 xTaskCreateStatic(),使用此函数创建任务的时候需要由用户定义任务堆栈,本章我们不讨论这种静态方法。

使用动态内存管理的时候 FreeRTOS 内核在创建任务、队列、信号量的时候会动态的申请RAM。标准 C 库中的 malloc()和 free()也可以实现动态内存管理,但是如下原因限制了其使用:

● 在小型的嵌入式系统中效率不高。

● 会占用很多的代码空间。

● 它们不是线程安全的。

● 具有不确定性,每次执行的时间不同。

● 会导致内存碎片。

● 使链接器的配置变得复杂。

不同的嵌入式系统对于内存分配和时间要求不同,因此一个内存分配算法可以作为系统的可选选项。FreeRTOS 将内存分配作为移植层的一部分,这样 FreeRTOS 使用者就可以使用自己的合适的内存分配方法。

当内核需要 RAM 的时候可以使用 pvPortMalloc()来替代 malloc()申请内存,不使用内存的时候可以使用 vPortFree()函数来替代 free()函数释放内存。函数 pvPortMalloc()、vPortFree()与函数 malloc()、free()的函数原型类似。

FreeRTOS 提供了 5 种内存分配方法,FreeRTOS 使用者可以其中的某一个方法,或者自己的内存分配方法。这 5 种方法是 5 个文件,分别为:heap_1.c、heap_2.c、heap_3.c、heap_4.c 和heap_5.。这5个文件在 FreeRTOS 源码中,路径:FreeRTOS->Source->portable->MemMang, 后面会详细讲解这 5 种方法有何区别。

内存碎片

在看 FreeRTOS 的内存分配方法之前我们先来看一下什么叫做内存碎片,看名字就知道是小块的、碎片化的内存。那么内存碎片是怎么来的呢?内存碎片是伴随着内存申请和释放而来的,如下图所示。

(1)、此时内存堆还没有经过任何操作,为全新的。

(2)、此时经过第一次内存分配,一共分出去了4块内存块,大小分别为80B、80B10B和 100B。

(3)、有些应用使用完内存,进行了释放,从左往右第一个 80B 和后面的 10B 这两个内存块就是释放的内存。如果此时有个应用需要 50B 的内存,那么它可以从两个地方来获取到,一个是最前面的还没被分配过的剩余内存块,另一个就是刚刚释放出来的 80B 的内存块。但是很明显,刚刚释放出来的这个 10B 的内存块就没法用了,除非此时有另外一个应用所需要的内存小于 10B

(4)、经过很多次的申请和释放以后,内存块被不断的分割、最终导致大量很小的内存块!也就是图中 80B 50B 这两个内存块之间的小内存块,这些内存块由于太小导致大多数应用无法使用,这些没法使用的内存块就沦为了内存碎片!

内存碎片是内存管理算法重点解决的一个问题,否则的话会导致实际可用的内存越来越少,最终应用程序因为分配不到合适的内存而奔溃!FreeRTOS heap_4.c 就给我们提供了一个解决内存碎片的方法,那就是将内存碎片进行合并组成一个新的可用的大内存块。

heap_1内存分配方法

动态内存分配需要一个内存堆,FreeRTOS中的内存堆为ucHeap[],大小为configTOTAL_HEAP_SIZE,这个前面讲 FreeRTOS 配置的时候就讲过了。不管是哪种内存分配方法,它们的内存堆都为 ucHeap[]而且大小都是 configTOTAL_HEAP_SIZE

内存堆在文件heap_x.c(x 1~5)中定义的,比如 heap_1.c 文件就有如下定义:

当宏 configAPPLICATION_ALLOCATED_HEAP 1 的时候需要用户自行定义内存堆,否则的话由编译器来决定,默认都是由编译器来决定的。如果自己定义的话就可以将内存堆定义到外部 SRAM 或者 SDRAM 中。

heap_1 实现起来就是当需要 RAM 的时候就从一个大数组(内存堆)中分一小块出来,大数 组(内存堆)的容量为 configTOTAL_HEAP_SIZE,上面已经说了。使用函数xPortGetFreeHeapSize()可以获取内存堆中剩余内存大小。

heap_1 特性如下:

1、适用于那些一旦创建好任务、信号量和队列就再也不会删除的应用,实际上大多数的 FreeRTOS 应用都是这样的。

2、具有可确定性(执行所花费的时间大多数都是一样的),而且不会导致内存碎片。

3、代码实现和内存分配过程都非常简单,内存是从一个静态数组中分配到的,也就是适合于那些不需要动态内存分配的应用。

heap_1 的内存释放函数pvFree(),可以看一下 pvFree()的源码,如下:

可以看出 vPortFree()并没有具体释放内存的过程。因此如果使用 heap_1,一旦申请内存成功就不允许释放!因为heap_1根本就没有提供内存释放的功能,但是 heap_1 的内存分配过程简单,如此看来 heap_1 似乎毫无任何使用价值啊。千万不能这么想,有很多小型的应用在系统一开始就创建好任务、信号量或队列等,在程序运行的整个过程这些任务和内核对象都不会删除,那么这个时候使用 heap_1 就很合适的。

heap_2内存分配方法

heap_2提供了一个更好的分配算法,不像heap_1那样,heap_2提供了内存释放函数。heap_2不会把释放的内存块合并成一个大块,这样有一个缺点,随着你不断的申请内存,内存堆就会被分为很多个大小不一的内存(),也就是会导致内存碎片!heap_4 提供了空闲内存块合并的功能。

heap_2 的特性如下:

1、可以使用在那些可能会重复的删除任务、队列、信号量等的应用中,要注意有内存碎片产生!

2、如果分配和释放的内存 n 大小是随机的,那么就要慎重使用了,比如下面的示例:

 如果一个应用动态的创建和删除任务,而且任务需要分配的堆栈大小都是一样的,那么 heap_2 就非常合适。如果任务所需的堆栈大小每次都是不同,那么 heap_2 就不适合了,因为这样会导致内存碎片产生,最终导致任务分配不到合适的堆栈!不过 heap_4 就很适合这种场景了。

● 如果一个应用中所使用的队列存储区域每次都不同,那么 heap_2 就不适合了,和上面一样,此时可以使用 heap_4

● 应用需要调用 pvPortMalloc()vPortFree()来申请和释放内存,而不是通过其他 FreeRTOS 的其他 API 函数来间接的调用,这种情况下 heap_2 不适合。

如果应用中的任务、队列、信号量和互斥信号量具有不可预料性(如所需的内存大小不能 确定,每次所需的内存都不相同,或者说大多数情况下所需的内存都是不同的)的话可能会导致内存碎片。虽然这是小概率事件,但是还是要引起我们的注意!

heap_2 具有不可确定性,但是也远比标准 C 中的 malloc()free()效率高!

heap_2 基本上可以适用于大多数的需要动态分配内存的工程中,而 heap_4 更是具有将内存碎片合并成一个大的空闲内存块(就是内存碎片回收)的功能。

heap_1 一样,heap_2 整个内存堆为 ucHeap[],大小为 configTOTAL_HEAP_SIZE。可以通过函数 xPortGetFreeHeapSize()来获取剩余的内存大小。

为了实现内存释放,heap_2 引入了内存块的概念,每分出去的一段内存就是一个内存块,剩下的空闲内存也是一个内存块,内存块大小不定。为了管理内存块又引入了一个链表结构。为了方便管理,可用的内存块会被全部组织在一个链表内,局部静态变量 xStart, xEnd 用来记录这个链表的头和尾。

heap_2允许内存释放,内存释放函数 vPortFree()还是很简单的,主要目的就是将需要释放的内存所在的内存块添加到空闲内存块链表中。

heap_3内存分配方法

这个分配方法是对标准 C 中的函数 malloc()free()的简单封装,FreeRTOS 对这两个函数做了线程保护。

heap_3 的特性如下:

需要编译器提供一个内存堆,编译器库要提供 malloc()free()函数。比如使用 STM32 的话可以通过修改启动文件中的 Heap_Size 来修改内存堆的大小,如下图所示。

具有不确定性

可能会增加代码量。

注意,在 heap_3 configTOTAL_HEAP_SIZE 是没用的!

heap_4内存分配方法

heap_4 提供了一个最优的匹配算法,不像 heap_2,heap_4 会将内存碎片合并成一个大的可用内存块,它提供了内存块合并算法。内存堆为 ucHeap[],大小同样为 configTOTAL_HEAP_SIZE。可以通过函数 xPortGetFreeHeapSize()来获取剩余的内存大小。

heap_4 特性如下:

可以用在那些需要重复创建和删除任务、队列、信号量和互斥信号量等的应用中。

不会像 heap_2 那样产生严重的内存碎片,即使分配的内存大小是随机的。

具有不确定性,但是远比 C 标准库中的 malloc()和 free()效率高。

heap_4 非常适合于那些需要直接调用函数 pvPortMalloc()和 vPortFree()来申请和释放内存 的应用,注意,我们移植 FreeRTOS 的时候就选择的 heap_4!

heap_4 也使用链表结构来管理空闲内存块,链表结构体与 heap_2一样。heap_4 也定义了两个局部静态变量 xStart 和 pxEnd 来表示链表头和尾。

内存块插入函数 prvInsertBlockIntoFreeList()用来将某个内存块插入到空闲内存块链表中。

heap_5内存分配方法

heap_5 使用了和 heap_4 相同的合并算法,内存管理实现起来基本相同,但是heap_5允许内存堆跨越多个不连续的内存段。比如 STM32 的内部 RAM 可以作为内存堆,但是STM32 内部 RAM 比较小,遇到那些需要大容量 RAM 的应用就不行了,如音视频处理。不过STM32 可以外接 SRAM 甚至大容量的 SDRAM,如果使用 heap_4 的话你就只能在内部 RAM 和外部SRAM SDRAM 之间二选一了,使用 heap_5 的话就不存在这个问题,两个都可以一起作为内存堆来用。

如果使用 heap_5 的话,在调用 API 函数之前需要先调用函数 vPortDefineHeapRegions ()来对内存堆做初始化处理,在 vPortDefineHeapRegions()未执行完之前禁止调用任何可能会调用pvPortMalloc()API 函数!比如创建任务、信号量、队列等函数。

函数vPortDefineHeapRegions()只有一个参数,参数是一个 HeapRegion_t 类型的数组HeapRegion 为一个结构体,此结构体在portable.h 中有定义,定义如下:

上面说了,heap_5 允许内存堆跨越多个不连续的内存段,这些不连续的内存段就是由结构体 HeapRegion_t 来定义的。比如以 STM32F103 开发板为例,现在有连个内存段:内部 SRAM、外部 SRAM,起始分别为:0X200000000x68000000,大小分别为:64KB1MB,那么数组就如下:

注意,数组中成员顺序按照地址从低到高的顺序排列,而且最后一个成员必须使用 NULL

heap_5 允许内存堆不连续,说白了就是允许有多个内存堆。在 heap_2 heap_4 中只有一个内存堆,初始化的时候只也只需要处理一个内存堆。 heap_5 有多个内存堆,这些内存堆会被连接在一起,和空闲内存块链表类似,这个处理过程由函数 vPortDefineHeapRegions()完成。

使用 heap_5 的时候在一开始就应该先调用函数 vPortDefineHeapRegions()完成内存堆的初始化!然后才能创建任务、信号量这些东西。

heap_5 的内存申请和释放函数和 heap_4 基本一样,这里就不详细讲解了,大家可以对照着源码来自行分析。

总结 

至此,FreeRTOS 官方提供的 5 种内存分配方法已经讲完了:

heap_1 最简单,但是只能申请内存,不能释放。

heap_2 提供了内存释放函数,用户代码也可以直接调用函数 pvPortMalloc()vPortFree()来申请和释放内存,但是 heap_2 会导致内存碎片的产生!

heap_3 是对标准 C 库中的函数 malloc()free()的简单封装,并且提供了线程保护。

heap_4 相对与 heap_2 提供了内存合并功能,可以降低内存碎片的产生,我们移植FreeRTOS 的时候就选择了 heap_4

heap_5 基本上和 heap_4一样,只是 heap_5 支持内存堆使用不连续的内存块。

内存申请和释放是要成对出现的,在一段内存没有被释放之前绝对不能再调用一次函数 pvPortMalloc()为其再次分配内存!否则会导致内存泄漏。初学者很容易犯这样的错误,忘记释放内存,内存泄露严重的话应用可能因为申请不到合适的内存而导致死机!

  • 23
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值