0. 内存管理概述
内存管理涉及基本的代码程序概念见代码重定位 - ARM裸机_arm代码重定位-CSDN博客和基本的文件与运行时代码地址布局程序内存分布和堆、栈概念_并无bin文件里包含栈-CSDN博客。
裸机程序只有一个独立的主进程工作,没有复杂的内存管理,其地址布局按照程序的编译以后的结果,根据需要指定栈地址为固定的一个数组既可以正常运行。RTOS系统一般会出现多进程工作的情况,为了较好的使用好内存信息,会有malloc结合多个进程完成内存的高效使用,但这种系统一般没有虚拟内存。Linux系统内存管理不仅有复杂的算法同时也包含了虚拟内存的内容。Linux内存管理详细见专题。
RTOS内存管理常用动态内存管理和静态内存管理两种方式,两种方式各有优缺点:
- 动态方法:自动地从管理的内存堆中申请创建对象所需大小的内存,并且在对象删除后可自动地将这块内存释放回内存堆。优点是按需分配,设备中灵活使用;缺点是内存池中可能出现碎片。
- 静态方法创建:由用户手动地提供各种内存空间,并且使用静态方式占用的内存空间一般固定下来了,即使任务、队列等被删除后,这些被占用的内存空间一般没有其他用途 ,我们一般是不会再去使用了。优点是分配和释放效率高,内存池中无碎片;缺点是只能申请预设大小的内存块,不能按需申请存在内存浪费的情况。
对于动态内存管理方法,标准 C 库也提供了函数 malloc() 和函数 free() 来实现动态地申请和释放内存 。为啥不用标准的 C 库自带的内存管理算法?因为标准 C 库的动态内存管理方法有如下几个缺点:
- 占用大量的代码空间 不适合用在资源紧缺的嵌入式系统中。而 FreeRTOS 号称是轻量级的,它的一个内存管理算法就几百行代码,所以相较之下 FreeRTOS 的代码量更小。
- 没有线程安全的相关机制。很明显 C 库又不是专门为你 os 而设计的,像 os,它任务与任务之间是会抢占的,那这个呢就涉及到线程安全了,而 FreeRTOS 提供的内存管理算法,那很显然它就是为它自己而设计的,它里面就考虑到这个线程安全了。在我们要申请的时候,它就会挂起任务调度器,那任务切换不了,就打断不了了。
- 运行有不确定性,每次调用这些函数时花费的时间可能都不相同。
- 内存碎片化,多次申请释放之后,可用的内存空间就越来越小。
1. FreeRTOS内存管理
FreeRTOS 就是因为 C 库它这个内存管理算法有这么多缺点,因此,它提供了多种动态内存管理的算法,可针对(适配)不同的嵌入式系统!FreeRTOS提供了5种动态内存管理算法,分别为: heap_1、heap_2、heap_3、heap_4、heap_5。其实都是一个 .c 文件,heap_1.c、heap_2.c、… … 这样下去,提供了 5 个 .c 文件。那这 .c 文件就是它们内存管理算法的实现。
算法 | 实现 | 优点 | 缺点 | 使用场景 |
heap_1 | 通过一个ucHeap[],大小为configTOTAL_HEAP_SIZE的数组定义一个堆内存,当然也可以通过指针的方式指定这块连续内存块大小表示一个堆。 1. 分配内存直接通过当前使用指针位置开始获取指定大小的内存,返回给使用程序。 | 分配简单,时间确定 | 只允许申请内存,不允许释放内存,内存利用率不高 | 内存可以提前固定分配,实际嵌入式中这种很常见 ①适用于那些一旦创建好任务、信号量和队列就再也不会删除的应用,实际上大多数的FreeRTOS应用都是这样的。 |
heap_2 | 通过链表串联空闲内存块,在基础上适配按照大小排序的最佳匹配算法实现内存管理 | 允许申请和释放内存 | 不能合并相邻的空闲内存块会产生碎片、时间不定 | 内存申请释放顺序不变且大小不变场景 ①可以使用在那些可能会重复的删除任务、队列、信号量等的应用中,要注意有内存碎片产生! |
heap_3 | 标准C中的函数malloc()和free()的简单封装,FreeRTOS对这两个函数做了线程保护。 | 直接调用C库函数malloc()和 free() ,在 C 库基础上增加了线程安全,简单 | 速度慢、时间不定 | 空间较大,时间要求不敏感 ①需要编译器提供一个内存堆,编译器库要提供malloc()和free()函数。比如STM32的话可以通过修改启动文件中的Heap_Size来修改内存堆的大小。 |
heap_4 | 提供了最优匹配算法和内存合并算法。与heap_2.c相比较,为了适配较好的合并,排序算法基于地址指针完成。 | 相邻空闲内存可合并,减少内存碎片的产生 | 时间不定 | 灵活内存需要的方案 ①可是用在那些需要重复创建和删除任务、队列、信号量和互斥信号量等的应用中。 |
heap_5 | heap_5.c使用了和heap_4.c相同的合并算法,内存管理实现起来基本相同,但是heap_5.c允许内存堆跨越多个不连续的内存段。比如STM32的内部RAM可以作为内存堆,但是STM32内部RAM比较小,遇到那些需要大容量RAM的应用就不行了,如音视频处理。 | 能够管理多个非连续内存区域的 heap_4 | 时间不定 | 图形显示芯片类,内部RAM空间不够外扩内存的时候使用 |
总结:FreeRTOS官方提供的5种内存分配方法已经介绍完,heap_1.c最简单,但是只能申请内存,不能释放。heap_2.c提供了内存释放函数,用户代码也可以直接调用函数pvPortMalloc()和vPortFree()来申请和释放内存,但是heap_2.c会导致内存碎片的产生!heap_3.c是对标准C库中的函数malloc()和free()的简单封装,并且提供了线程保护。heap_4.c相对与heap_2.c提供了内存合并功能,可以降低内存碎片的产生,我们移植FreeRTOS的时候就选择了heap_4.c。heap_5.c基本上和heap_4.c一样,只是heap_5.c支持内存堆使用不连续的内存块。
参考:
- FreeRTOS的内存管理方法(超详细)_rtos内存管理-CSDN博客
- FreeRTOS 内存管理_freertos内存管理-CSDN博客
- FreeRTOS学习-内存管理_freertos内存管理-CSDN博客
2. RT-Thread内存管理
1.1 静态内存管理
内存池(Memory Pool)是一种用于分配大量大小相同的小内存对象技术,应用程序可以直接在现有的池子中获取资源,分配和释放速度快。内存池创建时先向系统申请一块大内存,然后分成大小相等的多个小内存块,小内存块通过链表连接起来。
2.1 动态内存管理
其中,动态内存堆管理又根据具体设备的RAM大小划,分为三种情况:
- 小内存管理算法主要针对系统资源比较少,一般用于小于 2MB 内存空间的系统(小内存 管理算法);
- slab 内存管理算法则主要是在系统资源比较丰富时,提供了一种近似多内存池管理算法的快速算法(slab 管理算法);
- RT-Thread 还有一种针对多内存堆的管理算法,即 memheap 管理算法。memheap 方法适用于系统存在多个内存堆的情况,它可以将多个内存 “粘贴” 在一起,形成一个大的内存堆(memheap 管理算法);
注意:因为内存堆管理器要满足多线程情况下的安全分配,会考虑多线程间的互斥问题,所以不要在中断服务例程中分配或释放动态内存块,因为它可能会引起当前上下文被挂起等待;
2.1 小内存管理法
- 当需要分配内存块时,将从这个大的内存块上分割出相匹配的内存块,然后把分割出来的空闲内存块还回给堆管理系统中。每个内存块都包含一个管理用的数据头,通过这个头把使用块与空闲块用双向链表的方式链接起来,如图所示:
- 每个内存块都包含一个数据头:magic 和 used
- magic:用来标记这个内存块是一个内存管理的内存数据块,也是一个内存保护字,如果这个区域被改写,那么这个内存块就被非法改写了;
- used :用来表示当前内存块是否被分配;
struct heap_mem
{
/* magic and used flag */
rt_uint16_t magic;
rt_uint16_t used;
rt_size_t next, prev;
#ifdef RT_USING_MEMTRACE
rt_uint8_t thread[4]; /* thread name */
#endif
};
-
内存分配过程:
- 设定:空闲链表指针Ifree初始指向32字节的内存块,当用户线程需要分配一个64字节的内存块时,Ifree指针指向的内存块不能满足要求,内存管理器就会继续寻找下一个内存块,当找到时(128字节),就会进行内存分配,如果内存块比较大,分配器就会把内存块进行拆分,余下的内存(52字节)继续留在Ifree链表中,如下图所示;
- 注意:在内次分配内存块前,都会留出12字节用于magic、used信息及节点使用,返回给应用的地址实际上是这块内存块 12 字节以后的地址,前面的 12 字节数据头是用户永远不应该碰的部分(注:12 字节数据头长度会与系统对齐差异而有所不同)。
- 释放:释放内存时,分配器会查看前后相邻的内存是否是空闲,如果空闲,就回合并成一个大的空闲内存块;
4.2 slab 管理算法
- RT-Thread 的 slab 分配器的实现是建立在 slab分配器上的,针对嵌入式仙童优化的内存分配算法。(slab是linux系统中的一种内存分配机制)
- RT-Thread 的 slab 分配器实现主要是去掉了其中的对象构造及析构过程,只保留了纯粹的缓冲型的内存池算法。slab 分配器会根据对象的大小分成多个区(zone),也可以看成每类对象有一个内存池,如下图所示:
- 一个 zone 的大小在 32K 到 128K 字节之间,分配器会在堆初始化时根据堆的大小自动调整。系统中的 zone 最多包括 72 种对象,一次最大能够分配 16K 的内存空间,如果超出了 16K 那么直接从页分配器中分配。每个 zone 上分配的内存块大小是固定的,能够分配相同大小内存块的 zone 会链接在一个链表中,而 72 种对象的 zone 链表则放在一个数组(zone_array[])中统一管理。
- 内存分配:
- 假设分配一个 32 字节的内存,slab 内存分配器会先按照 32 字节的值,从 zone array 链表表头数组中找到相应的 zone 链表。如果这个链表是空的,则向页分配器分配一个新的 zone,然后从 zone 中返回第一个空闲内存块。如果链表非空,则这个 zone 链表中的第一个 zone 节点必然有空闲块存在(否则它就不应该放在这个链表中),那么就取相应的空闲块。如果分配完成后,zone 中所有空闲内存块都使用完毕,那么分配器需要把这个 zone 节点从链表中删除。
- 内存释放:分配器需要找到内存块所在的 zone 节点,然后把内存块链接到 zone 的空闲内存块链表中。如果此时 zone 的空闲链表指示出 zone 的所有内存块都已经释放,即 zone 是完全空闲的,那么当 zone 链表中全空闲 zone 达到一定数目后,系统就会把这个全空闲的 zone 释放到页面分配器中去。
4.3 memheap 管理算法
当使用的芯片有两个块SRAM(如STM32F427)或者外挂SRAM时可以使用此种内存管理方法。如果有多个不连续的 memheap 可以多次调用该函数将其初始化并加入 memheap_item 链表。
- memheap 管理算法适用于系统含有多个地址可不连续的内存堆。使用 memheap 内存管理可以简化系统存在多个内存堆时的使用:当系统中存在多个内存堆的时候,用户只需要在系统初始化时将多个所需的 memheap 初始化,并开启 memheap 功能就可以很方便地把多个 memheap(地址可不连续)粘合起来用于系统的 heap 分配;
- 在开启 memheap 之后原来的 heap 功能将被关闭,两者只可以通过打开或关闭 RT_USING_MEMHEAP_AS_HEAP 来选择其一;
- memheap 工作机制如下图所示,首先将多块内存加入 memheap_item 链表进行粘合。当分配内存块时,会先从默认内存堆去分配内存,当分配不到时会查找 memheap_item 链表,尝试从其他的内存堆上分配内存块。应用程序不用关心当前分配的内存块位于哪个内存堆上,就像是在操作一个内存堆。
参考:
- rt-thread-------内存管理(内存堆)_rtthread内存管理-CSDN博客
- https://www.cnblogs.com/icefree/p/10822971.html#_label1_1