c++内存管理_学习RTOS(20)内存管理

l 内存管理的基本概念

FreeRTOS 操作系统将内核与内存管理分开实现,操作系统内核仅规定了必要的内存管理函数原型,而不关心这些内存管理函数是如何实现的,所以在 FreeRTOS 中提供了多种内存分配算法(分配策略),但是上层接口( API)却是统一的。这样做可以增加系统的灵活性:用户可以选择对自己更有利的内存管理策略,在不同的应用场合使用不同的内存分配策略。

在嵌入式程序设计中内存分配应该是根据所设计系统的特点来决定选择使用动态内存分配还是静态内存分配算法,一些可靠性要求非常高的系统应选择使用静态的,而普通的业务系统可以使用动态来提高内存使用效率。静态可以保证设备的可靠性但是需要考虑内存上限,内存使用效率低,而动态则是相反。

为什么不直接使用 C 标准库中的内存管理函数呢?在电脑中我们可以用 malloc()和 free()这两个函数动态的分配内存和释放内存。但是,在嵌入式实时操作系统中,调用 malloc()和 free()却是危险的,原因有以下几点:

l 这些函数在小型嵌入式系统中并不总是可用的,小型嵌入式设备中的 RAM 不足。

l 它们的实现可能非常的大,占据了相当大的一块代码空间。

l 他们几乎都不是安全的。

l 它们并不是确定的,每次调用这些函数执行的时间可能都不一样。

l 它们有可能产生碎片。

l 这两个函数会使得链接器配置得复杂。

l 如果允许堆空间的生长方向覆盖其他变量占据的内存,它们会成为 debug 的灾难。

在一般的实时嵌入式系统中,由于实时性的要求,很少使用虚拟内存机制。所有的内存都需要用户参与分配,直接操作物理内存,所分配的内存不能超过系统的物理内存,所有的系统堆栈的管理,都由用户自己管理。

同时,在嵌入式实时操作系统中,对内存的分配时间要求更为苛刻,分配内存的时间必须是确定的。一般内存管理算法是根据需要存储的数据的长度在内存中去寻找一个与这段数据相适应的空闲内存块,然后将数据存储在里面。而寻找这样一个空闲内存块所耗费的时间是不确定的,因此对于实时系统来说,这就是不可接受的,实时系统必须要保证内存块的分配过程在可预测的确定时间内完成,否则实时任务对外部事件的响应也将变得不可确定。而在嵌入式系统中,内存是十分有限而且是十分珍贵的,用一块内存就少了一块内存,而在分配中随着内存不断被分配和释放,整个系统内存区域会产生越来越多的碎片,因为在使用过程中,申请了一些内存,其中一些释放了,导致内存空间中存在一些小的内存块,它们地址不连续,不能够作为一整块的大内存分配出去,所以一定会在某个时间,系统已经无法分配到合适的内存了,导致系统瘫痪。其实系统中实际是还有内存的,但是因为小块的内存的地址不连续,导致无法分配成功,所以我们需要一个优良的内存分配算法来避免这种情况的出现。

l 内存管理的应用场景

内存管理的主要工作是动态划分并管理用户分配好的内存区间,主要是在用户需要使用大小不等的内存块的场景中使用, 当用户需要分配内存时,可以通过操作系统的内存申请函数索取指定大小内存块,一旦使用完毕,通过动态内存释放函数归还所占用内存,使之可以重复使用。

l 内存管理方案详解

FreeRTOS 规定了内存管理的函数接口,但是不管其内部的内存管理方案是怎么实现的,所以 FreeRTOS 可以提供多个内存管理方案。

85758e0c092b3bd80ec32767340d9943.png

对于 heap_1.c、 heap_2.c 和 heap_4.c 这三种内存管理方案,内存堆实际上是一个很大的 数 组 , 定 义 为 static uint8_t ucHeap[ configTOTAL_HEAP_SIZE] , 而 宏 定 义configTOTAL_HEAP_SIZE 则表示系统管理内存大小,单位为字, 在 FreeRTOSConfig.h 中由用户设定。对于 heap_3.c 这种内存管理方案, 它封装了 C 标准库中的 malloc()和 free()函数,封装后的 malloc()和 free()函数具备保护,可以安全在嵌入式系统中执行。因此, 用户需要通过编译器或者启动文件设置堆空间。heap_5.c 方案允许用户使用多个非连续内存堆空间,每个内存堆的起始地址和大小由用户定义。 这种应用其实还是很大的,比如做图形显示、 GUI 等,可能芯片内部的 RAM是不够用户使用的,需要外部 SDRAM,那这种内存管理方案则比较合适。

v heap_1.c

heap_1.c 管理方案是 FreeRTOS 提供所有内存管理方案中最简单的一个,它只能申请内存而不能进行内存释放,并且申请内存的时间是一个常量,这样子对于要求安全的嵌入式设备来说是最好的,因为不允许内存释放,就不会产生内存碎片而导致系统崩溃,但是也有缺点,那就是内存利用率不高,某段内存只能用于内存申请的地方,即使该内存只使用一次,也无法让系统回收重新利用。实际上,大多数的嵌入式系统并不会经常动态申请与释放内存,一般都是在系统完成的时候,就一直使用下去,永不删除, 所以这个内存管理方案实现简洁、安全可靠,使用的非常广泛。

heap1.c 方案具有以下特点:

1、 用于从不删除任务、队列、信号量、互斥量等的应用程序(实际上大多数使用FreeRTOS 的应用程序都符合这个条件) 。

2、 函数的执行时间是确定的并且不会产生内存碎片。

² 内存申请函数 pvPortMalloc()

内存申请函数就是用于申请一块用户指定大小的内存空间,当系统管理的内存空间满足用户需要的大小的时候,就能申请成功,并且返回内存空间的起始地址。

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

8787fbb22706dffa8ab7366d55fa67cb.png

在内存对齐完成后, 用户想要申请一个 30 字节大小的内存,那么按照系统对齐的要求,我们会申请到 32 个字节大小的内存空间,即使我们只需要 30 字节的内存。

7f63ca2a68f9aad1b2a6da87001fea24.png

v heap_2.c

heap_2.c 方案与 heap_1.c 方案采用的内存管理算法不一样,它采用一种最佳匹配算法(best fit algorithm),比如我们申请 100 字节的内存,而可申请内存中有三块,对应大小 200 字节, 500 字节和 1000 字节大小的内存块,按照算法的最佳匹配,这时候系统会把 200 字节大小的内存块进行分割并返回申请内存的起始地址,剩余的内存则插回链表留待下次申请。Heap_2.c 方案支持释放申请的内存, 但是它不能把相邻的两个小的内存块合成一个大的内存块, 对于每次申请内存大小都比较固定的,这个方式是没有问题的,而对于每次申请并不是固定内存大小的则会造成内存碎片,

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

heap_2.c 方案与 heap_1 方案在内存堆初始化的时候操作都是一样的,在内存中开辟了一个静态数组作为堆的空间,大小由用户定义,然后进行字节对齐处理。heap_2.c 方案采用链表的数据结构记录空闲内存块,将所有的空闲内存块组成一个空闲内存块链表, FreeRTOS 采用 2 个 BlockLink_t 类型的局部静态变量 xStart、 xEnd 来标识空闲内存块链表的起始位置与结束位置。

² 内存申请函数 pvPortMalloc()

heap_2.c 内存管理方案采用最佳匹配算法管理内存,系统会先从内存块空闲链表头开始进行遍历,查找符合用户申请大小的内存块( 内存块空闲链表按内存块大小升序排列,所以最先返回的的块一定是最符合申请内存大小,所谓的最匹配算法就是这个意思来的)。当找到内存块的时候, 返回该内存块偏移 heapSTRUCT_SIZE 个字节后的地址, 因为在每块内存块前面预留的节点是用于记录内存块的信息, 用户不需要也不允许操作这部分内存。

在申请内存成功的同时系统还会判断当前这块内存是否有剩余(大于一个链表节点所需内存空间) , 这样子就表示剩下的内存块还是能存放东西的,也要将其利用起来。如果有剩余的内存空间, 系统会将内存块进行分割, 在剩余的内存块头部添加一个内存节点,并且完善该空闲内存块的信息,然后将其按内存块大小插入内存块空闲链表中, 供下次分配使用, 其中 prvInsertBlockIntoFreeList() 这个函数就是把节点按大小插入到链表中。

4299602d6d8b61f3e6056d9f27cc18d9.png

随着内存申请, 越来越多申请的内存块脱离空闲内存链表, 但链表仍是以 xStart 节点开头以 xEnd 节点结尾, 空闲内存块链表根据空闲内存块的大小进行排序。每当用户申请一次内存的时候,系统都要分配一个 BlockLink_t 类型结构体空间,用于保存申请的内存块信息,并且每个内存块在申请成功后会脱离空闲内存块链表,申请两次后的内存示意图。

be85b134f8664b600e05bafee6842f2a.png

² 内存释放函数 vPortFree()

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

0678144979654bf1be91f364447d0cca.png

32497ff668857c4cac70983f1bc009c8.png

从内存的申请与释放看来, heap_2.c 方案采用的内存管理算法虽然是高效但还是有缺陷的,由于在释放内存时不会将相邻的内存块合并,所以这可能造成内存碎片,当然并不是说这种内存管理算法不好,只不过对使用的条件比较苛刻,要求用户每次创建或释放的任务、队列等必须大小相同如果分配或释放的内存是随机的,绝对不可以用这种内存管理。

v heap_3.c

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

heap_3.c 方案具有以下特点:

1、 需要链接器设置一个堆, malloc()和 free()函数由编译器提供。

2、 具有不确定性。

3、 很可能增大 RTOS 内核的代码大小。

要注意的是在使用heap_3.c 方案时, FreeRTOSConfig.h 文件 的configTOTAL_HEAP_SIZE 宏定义不起作用。在 STM32 系列的工程中, 这个由编译器定义的堆都在启动文件里面设置, 单位为字节。

8bd996100406d975290b6c70a873a99f.png

v heap_4.c

heap_4.c 方案与 heap_2.c 方案一样都采用最佳匹配算法来实现动态的内存分配,但是不一样的是 heap_4.c 方案还包含了一种合并算法,能把相邻的空闲的内存块合并成一个更大的块,这样可以减少内存碎片。heap_4.c 方案特别适用于移植层中可以直接使用pvPortMalloc()和 vPortFree()函数来分配和释放内存的代码。

heap_4.c 内存管理方案的空闲块链表不是以内存块大小进行排序的,而是以内存块起始地址大小排序, 内存地址小的在前,地址大的在后,因为 heap_4.c 方案还有一个内存合并算法,在释放内存的时候,假如相邻的两个空闲内存块在地址上是连续的,那么就可以合并为一个内存块, 这也是为了适应合并算法而作的改变。

heap_4.c 方案具有以下特点:

1、 可用于重复删除任务、队列、信号量、互斥量等的应用程序

2、 可用于分配和释放随机字节内存的应用程序, 但并不像 heap2.c 那样产生严重的内存碎片。

3、 具有不确定性,但是效率比标准 C 库中的 malloc 函数高得多。

² 内存申请函数 pvPortMalloc()

heap_4.c 方案的内存申请函数与 heap_2.c 方案的内存申请函数大同小异,同样是从链表头 xStart 开始遍历查找合适的内存块,如果某个空闲内存块的大小能容得下用户要申请的内存,则将这块内存取出用于需要内存空间大小的部分返回给用户,剩下的内存块组成一个新的空闲块,按照空闲内存块起始地址大小顺序插入到空闲块链表中,内存地址小的在前,内存地址大的在后。在插入到空闲内存块链表的过程中,系统还会执行合并算法将地址相邻的内存块进行合并:判断这个空闲内存块是相邻的空闲内存块合并成一个大内存块,如果可以则合并,合并算法是 heap_4.c 内存管理方案和 heap_2.c 内存管理方案最大的不同之处,这样一来,会导致的内存碎片就会大大减少,内存管理方案适用性就很强,能用于随机申请和释放内存的应用中,灵活性得到大大的提高。

eb42a722bbcfd0f71262ab8bf10799e4.png

624c045c9b670ab3bf79303f53931cde.png

93d0705c5a8db4eb752035768c59245e.png

² 内存释放函数 vPortFree()

heap_4.c 内存管理方案的内存释放函数 vPortFree()也比较简单, 根据传入要释放的内存块地址,偏移之后找到链表节点,然后将这个内存块插入到空闲内存块链表中,在内存块插入过程中会执行合并算法。最后是将这个内存块标志为“空闲” (内存块节点的 xBlockSize 成员变量最高位清 0)、再更新未分配的内存堆大小即可。

v heap_5.c

heap_5.c 方案在实现动态内存分配时与 heap4.c 方案一样, 采用最佳匹配算法和合并算法,并且允许内存堆跨越多个非连续的内存区,也就是允许在不连续的内存堆中实现内存分配,比如用户在片内 RAM 中定义一个内存堆,还可以在外部 SDRAM 再定义一个或多个内存堆,这些内存都归系统管理。heap_5.c 方案通过调用 vPortDefineHeapRegions()函数来实现系统管理的内存初始化,在内存初始化未完成前不允许使用内存分配和释放函数。如创建 FreeRTOS 对象(任务、队列、信号量等)时会隐式的调用 pvPortMalloc()函数,因此必须注意:使用 heap_5.c 内存管理方案创建任何对象前,要先调用 vPortDefineHeapRegions()函数将内存初始化。

用户需要指定每个内存堆区域的起始地址和内存堆大小、将它们放在一个HeapRegion_t 结构体类型数组中, 这个数组必须用一个 NULL 指针和 0 作为结尾,起始地址必须从小到大排列。用户在自定义好内存堆数组后,需要调用 vPortDefineHeapRegions()函数初始化这些内存堆,系统会已一个空闲内存块链表的数据结构记录这些空闲内存,链表以 xStart 节点构开头,以 pxEnd 指针指向的位置结束。vPortDefineHeapRegions()函数对内存的初始化与heap_4.c 方案一样。

411cfb5b2af65168a882ae0a16619dcd.png

l 内存管理的实验

内存管理实验使用 heap_4.c 方案进行内存管理测试, 创建了两个任务,分别是 LED 任务与内存管理测试任务,内存管理测试任务通过检测按键是否按下来申请内存或释放内存,当申请内存成功就像该内存写入一些数据,如当前系统的时间等信息,并且通过串口输出相关信息;LED 任务是将 LED 翻转,表示系统处于运行状态。在不需要再使用内存时,注意要及时释放该段内存,避免内存泄露。

644b3acbd77d41065ff3aca1f08e130c.png

实验效果

bef7a0c54a4a1a8636dc10a582e8f428.png

Hankin

2020.10.05

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值