fre-内存管理

本文介绍了内存管理的基本概念,包括内部存储(RAM)和外部存储(硬盘)的区别,以及动态内存分配(如malloc()和free())在嵌入式系统中的挑战。重点讨论了FreeRTOS提供的几种内存管理算法heap_1.c至heap_5.c,涉及内存碎片问题、内存分配与释放、栈与堆的区别,以及应用场景和内存管理策略。
摘要由CSDN通过智能技术生成

内存管理的基本概念

储存空间:

  1. 内部储存空间:内部存储空间访问速度比较快,能够按照变量地址随机地访问,也就是我们通常所说的 RAM(随机存储器),或电脑的内存;
  2. 外部储存空间:外部存储空间内所保存的内容相对来说比较固定,即使掉电后数据也不会丢失,可以把它理解为电脑的硬盘。

内存分配方式:

  • 动态内存分配:使用动态的方法可以提高内存使用效率
  • 静态内存分配:可靠性要求非常高的系统选择使用静态,可以保证内设备的可靠性,但是需要考虑内存上限。

电脑中我们可以用 malloc()
和 free() 这两个函数动态的分配内存和释放内存。但是,在嵌入式实时操作系统中,调用 malloc()
和 free() 却是危险的,原因有以下几点:
• 这些函数在小型嵌入式系统中并不总是可用的,小型嵌入式设备中的 RAM 不足。
• 它们的实现可能非常的大,占据了相当大的一块代码空间。
• 他们几乎都不是安全的。
• 它们并不是确定的,每次调用这些函数执行的时间可能都不一样。
• 它们有可能产生碎片。
• 这两个函数会使得链接器配置得复杂。
• 如果允许堆空间的生长方向覆盖其他变量占据的内存,它们会成为 debug 的灾难。

虚拟内存:
虚拟内存的主要思想是将应用程序的内存分为虚拟内存和物理内存。虚拟内存是一个抽象概念,通常比物理内存大得多。当应用程序需要内存时,操作系统会将其从物理内存复制到虚拟内存。这允许多个程序并发运行,并且它们都认为自己有足够的内存可用。操作系统根据需要将虚拟内存的数据从磁盘加载到物理内存,以满足应用程序的内存需求。

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

要求:实时系统必须要保证内存块的分配过程在可预测的确定时间内完成,否则实时任务对外部事件的响应也将变得不可确定。

内存碎片:嵌入式系统中,内存不停释放、分配的过程,会导致有一些很小的内存块出现,因此其会导致大片的内存不连续,不能作为一整块的大内存分配出去,从而导致系统瘫痪,所以我们需要一个优良的内存分配算法来避免这种情况的出现。

FreeRTOS 为我们提供了 5 种内存管理算法,分别是 heap_1.c、heap_2.c、 heap_3.c、 heap_4.c、 heap_5.c,在使用的时候选择其中一个添加到我们的工程中去即可。FreeRTOS 的内存管理模块通过对内存的申请、释放操作,来管理用户和系统对内存的使用,使内存的利用率和使用效率达到最优,同时最大限度地解决系统可能产生的内存碎片问题。

内存管理的应用场景

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

例如我们需要定义一个 float 型数组: floatArr[];

但是,在使用数组的时候,总有一个问题困扰着我们:数组应该有多大?在很多的情况下,你并不能确定要使用多大的数组,可能为了避免发生错误你就需要把数组定义得足够大。即使你知道想利用的空间大小,但是如果因为某种特殊原因空间利用的大小有增加或者减少,你又必须重新去修改程序,扩大数组的存储范围。这种分配固定大小的内存分配方法称之为静态内存分配。这种内存分配的方法存在比较严重的缺陷,在大多数情况下会浪费大量的内存空间,在少数情况下,当你定义的数组不够大时,可能引起下标越界错误,甚至导致严重后果。

我们用动态内存分配就可以解决上面的问题。所谓动态内存分配就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不象数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。

在这里插入图片描述
内存堆与另一个内存区域,称为栈(Stack),有所不同。栈用于存储函数调用的局部变量和控制信息,它的分配和释放是自动进行的,通常遵循"后进先出"(Last-In-First-Out,LIFO)的原则。而内存堆的内存分配和释放由程序员手动管理,通常遵循"先进先出"(First-In-First-Out,FIFO)的原则,也就是你可以在任何时候请求分配一块内存,然后在不需要它时释放它。FreeRTOS 提供的内存管理都是从内存堆中分配内存的。

heap_1.c

heap_1.c 管理方案是 FreeRTOS 提供所有内存管理方案中最简单的一个,它只能申请内存而不能进行内存释放,并且申请内存的时间是一个常量,非常安全,因为他不允许内存的释放,因此也不会产生内存碎片而导致系统崩溃.缺点:内存利用率不高。

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

  1. 用于从不删除任务、队列、信号量、互斥量等的应用程序(实际上大多数使用 FreeRTOS 的
    应用程序都符合这个条件)。
  2. 函数的执行时间是确定的并且不会产生内存碎片。
    两个静态变量实现内存管理

内存管理申请函数pvPortMalloc()

内存申请函数就是用于申请一块用户指定大小的内存空间,当系统管理的内存空间满足用户需要的大小的时候,就能申请成功,并且返回内存空间的起始地址。void *pvPortMalloc( size_t xWantedSize )
对齐示意图
申请完成示意图

heap_2.c

heap_2.c 方案与 heap_1.c 方案采用的内存管理算法不一样,它采用一种最佳匹配算法 (best fitalgorithm),比如我们申请 100 字节的内存,而可申请内存中有三块对应大小 200 字节, 500 字节和 1000 字节大小的内存块,按照算法的最佳匹配,这时候系统会把 200 字节大小的内存块进行分割并返回申请内存的起始地址,剩余的内存则插回链表留待下次申请。

Heap_2.c 方案支持释放申请的内存,但是它不能把相邻的两个小的内存块合成一个大的内存块,对于每次申请内存大小都比较固定的,这个方式是没有问题的,而对于每次申请并不是固定内存大小的则会造成内存碎片,后面要讲解的 heap_4.c 方案采用的内存管理算法能解决内存碎片的问题,可以把这些释放的相邻的小的内存块合并成一个大的内存块。

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

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

heap_2.c 方案与 heap_1 方案在内存堆初始化的时候操作都是一样的,在内存中开辟了一个静态数组作为堆的空间,大小由用户定义,然后进行字节对齐处理。

内存申请函数 pvPortMalloc()

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

申请两次内存成功示意图

内存释放函数 vPortFree()

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

释放一个内存块
内存释放完成

heap_3.c

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

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

  1. 需要链接器设置一个堆, malloc() 和 free() 函数由编译器提供。
  2. 具有不确定性。
  3. 很可能增大 RTOS 内核的代码大小。

heap_4.c

该方案与heap_2.c方案一样,都是采用最佳匹配算法来实现动态的内存分配,但是不一样的是,方案4包含了一种合并算法,可以将相邻的空闲的内存块合并成一个更大的块,这样可以减少内存碎片。

heap_4.c 方案特别适用于移植层中可以直接使用 pvPortMalloc() 和 vPortFree() 函数来分配和释放内存的代码。

heap_4.c 方案的空闲内存块也是以单链表的形式连接起来的,

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

heap_4.c 方案具有以下特点:
1、可用于重复删除任务、队列、信号量、互斥量等的应用程序
2、可用于分配和释放随机字节内存的应用程序,但并不像 heap2.c 那样产生严重的内存碎片。
3、具有不确定性,但是效率比标准 C 库中的 malloc 函数高得多。

内存申请函数 pvPortMalloc()

如果某个空闲内存块的大小能容得下用户要申请的内存,则将这块内存取出用户需要内存空间大小的部分返回给用户,剩下的内存块组成一个新的空闲块,按照空闲内存块起始地址大小顺序插入到空闲块链表中,内存地址小的在前,内存地址大的在后。

在插入到空闲内存块链表的过程中,系统还会执行合并算法将地址相邻的内存块进行合并:判断这个空闲内存块是相邻的空闲内存块合并成一个大内存块,如果可以则合并。

内存释放函数 vPortFree()

heap_4.c 内存管理方案的内存释放函数 vPortFree() 也比较简单,根据传入要释放的内存块地址,偏移之后找到链表节点,然后将这个内存块插入到空闲内存块链表中,在内存块插入过程中会执行合并算法。

heap_5.c

heap_5.c 方案在实现动态内存分配时与 heap4.c 方案一样,采用最佳匹配算法和合并算法,并且允许内存堆跨越多个非连续的内存区,也就是允许在不连续的内存堆中实现内存分配。

比如用户在片内 RAM 中定义一个内存堆,还可以在外部 SDRAM 再定义一个或多个内存堆,这些内存都归系统管理。

在这里插入图片描述

heap_5.c 方案通过调用 vPortDefineHeapRegions() 函数来实现系统管理的内存初始化,在内存初始化未完成前不允许使用内存分配和释放函数。

vPortDefineHeapRegions() 函数只有一个形参,该形参是一个 HeapRegion_t 类型的结构体数组。HeapRegion_t 类型结构体在 portable.h 中定义

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值