FreeRTOS内存管理
1. 为什么要自己实现内存管理
- 对于内核对象,可以使用时分配,不使用时释放
- C语音的库函数不适应与FreeRTOS:
- 实现过于复杂,占用空间大
- 并非线程安全的
- 运行不确定性:每次运算时间不确定
- 内存碎片化
- 不太编译器配置不同
- 调试难
2. 堆栈
我们经常说的“堆栈”,是两种不同的东西
2.1. 堆
heap就是一块空闲的内存,通过malloc、free来管理
2.2. 栈
stack,函数调用时局部变量保存在栈中,当前的程序环境也保存在栈中,可以充堆中分配一块内存用于栈空间
3. FreeRTOS系统5种内存管理方法
文件在 FreeRTOS/Source/portable/MemMang 下,5个文件对应5中内存管理方法:
3.1. Heap_1
3.1.1. 特点
只实现了pvPortMalloc,没有实现vPortFree,如果程序中不需要删除内核对象,不需要释放内存,那么可以实现heap_1:
- 实现最简单
- 没有碎片问题
- 一些要求非常严格的系统中,不允许使用动态内存,可实现heap_1
3.1.2. 实现原理
- 定义一个大数组
#if ( configAPPLICATION_ALLOCATED_HEAP == 1 )
/* The application writer has already defined the array used for the RTOS
* heap - probably so it can be placed in a special segment or address. */
extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#else
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#endif /* configAPPLICATION_ALLOCATED_HEAP */
- 调用pvPortMalloc调用时,从数组中分配空间
注意:
内存申请时,需要字节对齐处理
–> 在有些处理器中,访问奇地址访问内存会报错
3.2. Heap_2
Heap_2 之所以还保留,只是为了兼容以前的代码。新的设计中,不再推进实现Heap_2。
Heap_2也是在数组上分配内存,和Heap_1不一样的地方在于:
- heap_2使用最佳匹配算法来分配内存
- 支持vPortFree
3.2.1. 数据结构
typedef struct A_BLOCK_LINK
{struct A_BLOCK_LINK * pxNextFreeBlock; /*<< The next free block in the list. */size_t xBlockSize; /*<< The size of the free block. */
} BlockLink_t;
3.2.2. 内存结构
3.2.3. 最佳匹配算法:
- 假设Heap有3个空闲块:5字节、25字节、100字节
- pvPortMalloc想申请20个字节
- 先找到最小的、能满足要求的内存:25字节
- 把它分为20字节、5字节
- 返回20字节的地址
- 剩下的5字节仍是空闲状态,继续留给后续使用
与Heap_4相比,Heap_2不会合并相邻的空闲内存,所以Heap_2会导致严重的碎片化问题,但是如果申请、分配内存大小都相同的场景下,Heap_2就没有碎片化问题,所以它适合这些场景:频繁窗户删除任务、但任务的栈大小都是相同的。
虽然不再推荐使用Heap_2,但它的效率还是远高于malloc、free.
3.3. Heap_3
Heap_3 使用标准 C 库里的 malloc、 free 函数,C 库里的 malloc、 free 函数并非线程安全的, Heap_3 中先暂停 FreeRTOS 的调度器,再去调用这些函数,使用这种方法实现了线程安全。
3.4. Heap_4
跟 Heap_1、 Heap_2 一样, Heap_4 也是使用大数组来分配内存。Heap_4 使用首次适应算法(first fit)来分配内存。它还会把相邻的空闲内存合并为一个更大的空闲内存,这有助于较少内存的碎片问题。
3.4.1. 首次适应算法
- 假设堆中有3块空闲的内存:5字节、200字节、100字节
- pvPortMalloc 想申请 20 字节
- 找出第 1 个能满足 pvPortMalloc 的内存: 200 字节
- 把它划分为 20 字节、 180 字节
- 返回这 20 字节的地址
- 剩下的 180 字节仍然是空闲状态,留给后续的 pvPortMalloc 使用
Heap_4 会把相邻空闲内存合并为一个大的空闲内存,可以较少内存的碎片化问题。适用于这种场景:频繁地分配、释放不同大小的内存。
Heap_4 的使用过程举例如下:
- A:创建了 3 个任务
- B:删除了一个任务,空闲内存有 2 部分:
- 顶层的
- 被删除任务的 TCB 空间、被删除任务的 Stack 空间合并起来的
- C:分配了一个 Queue,从第 1 个空闲块中分配空间
- D:分配了一个 User 数据,从 Queue 之后的空闲块中分配
- E:释放的 Queue, User 前后都有一块空闲内存
- F:释放了 User 数据, User 前后的内存、 User 本身占据的内存, 合并为一个大的空闲内存
Heap_4 执行的时间是不确定的,但是它的效率高于标准库的 malloc、 free。
3.5. Heap_5
Heap_5 分配内存、释放内存的算法跟 Heap_4 是一样的。
相比于 Heap_4, Heap_5 并不局限于管理一个大数组:它可以管理多块、分隔开的内存。
在嵌入式系统中,内存的地址可能并不连续,这种场景下可以使用 Heap_5。
既然内存时分隔开的,那么就需要进行初始化:确定这些内存块在哪、多大:
- 在使用 pvPortMalloc 之前,必须先指定内存块的信息
- 使用 vPortDefineHeapRegions 来指定这些信息
怎么指定一块内存?使用如下结构体:
typedef struct HeapRegion
{uint8_t * pucStartAddress; // 起始地址size_t xSizeInBytes; // 大小
} HeapRegion_t;
怎么指定多块内存?使用一个 HeapRegion_t 数组,在这个数组中,低地址在前、高地址在后。
HeapRegion_t xHeapRegions[] =
{{ ( uint8_t * ) 0x80000000UL, 0x10000 }, // 起始地址 0x80000000,大小 0x10000{ ( uint8_t * ) 0x90000000UL, 0xa0000 }, // 起始地址 0x90000000,大小 0xa0000{ NULL, 0 } // 表示数组结束
};
vPortDefineHeapRegions 函数原型如下:
void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions );
把 xHeapRegions 数组传给 vPortDefineHeapRegions 函数,即可初始化 Heap_5。
4. Heap接口
4.1. pvPortMalloc/vPortFree
void * pvPortMalloc( size_t xWantedSize );
void vPortFree( void * pv );
4.2. xPortGetFreeHeapSize
size_t xPortGetFreeHeapSize( void );
只有 heap_4、 heap_5 支持此函数。
4.3. xPortGetMinimumEverFreeHeapSize
size_t xPortGetMinimumEverFreeHeapSize( void );
返回:程序运行过程中,空闲内存容量的最小值
只有 heap_4、 heap_5 支持此函数。
4.4. malloc 失败的钩子函数
void * pvPortMalloc( size_t xWantedSize )vPortDefineHeapRegions