目录
1. 内存管理的基本概念
在计算系统中,变量、中间数据一般存放在系统存储空间中,只有在实际使用时才将它们从存储空间调入到中央处理器内部进行运算。通常存储空间可以分为两种:内部存储空间和外部存储空间。
- 内部存储空间访问速度比较快,能够按照变量地址随机地访问,也就是我们通常所说的 RAM(随机存储器),或电脑的内存;
- 外部存储空间内所保存的内容相对来说比较固定,即使掉电后数据也不会丢失,可以把它理解为电脑的硬盘。
FreeRTOS 操作系统将内核与内存管理分开实现,操作系统内核仅规定了必要的内存管理函数原型,而不关心这些内存管理函数是如何实现的,所以在 FreeRTOS 中提供了多种内存分配算法(分配策略),一共有五种分别是 heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c。
使用 C 标准库的malloc()和 free()这两个函数也可以动态的分配内存和释放内存,但是,在嵌入式实时操作系统中,调用 malloc()和 free()却是危险的,原因有以下几点:
- 这些函数在小型嵌入式系统中并不总是可用的,小型嵌入式设备中的 RAM 不足。
- 它们的实现可能非常的大,占据了相当大的一块代码空间。
- 他们几乎都不是安全的。
- 它们并不是确定的,每次调用这些函数执行的时间可能都不一样。
- 它们有可能产生碎片。
- 这两个函数会使得链接器配置得复杂。
- 如果允许堆空间的生长方向覆盖其他变量占据的内存,它们会成为 debug 的灾难。
在一般的实时嵌入式系统中,由于实时性的要求,很少使用虚拟内存机制。所有的内存都需要用户参与分配,直接操作物理内存,所分配的内存不能超过系统的物理内存,所有的系统堆栈的管理,都由用户自己管理。
2. 内存碎片
而在嵌入式系统中,内存是十分有限而且是十分珍贵的,用一块内存就少了一块内存,而在分配中随着内存不断被分配和释放,整个系统内存区域会产生越来越多的碎片,因为在使用过程中,申请了一些内存,其中一些释放了,导致内存空间中存在一些小的内存块,它们地址不连续,不能够作为一整块的大内存分配出去,所以一定会在某个时间,系统已经无法分配到合适的内存了,导致系统瘫痪。其实系统中实际是还有内存的,但是因为小块的内存的地址不连续,导致无法分配成功,所以我们需要一个优良的内存分配算法来避免这种情况的出现。
3. 内存分配方法
FreeRTOS 提供的内存管理都是从内存堆中分配内存的。从前面学习的过程中,我们也知道,创建任务、消息队列、事件等操作都使用到分配内存的函数,这是系统中默认使用内存管理函数从内存堆中分配内存给系统核心组件使用。
FreeRTOS 规定的内存管理函数接口:
void *pvPortMalloc( size_t xSize ); //内存申请函数
void vPortFree( void *pv ); //内存释放函数
void vPortInitialiseBlocks( void ); //初始化内存堆函数
size_t xPortGetFreeHeapSize( void ); //获取当前未分配的内存堆大小
size_t xPortGetMinimumEverFreeHeapSize( void ); //获取未分配的内存堆历史最小值
对于 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,那这种内存管理方案则比较合适。
3.1 heap_1.c
heap_1.c 管理方案是 FreeRTOS 提供所有内存管理方案中最简单的一个,它只能申请内存而不能进行内存释放,并且申请内存的时间是一个常量,这样子对于要求安全的嵌入式设备来说是最好的,因为不允许内存释放,就不会产生内存碎片而导致系统崩溃,但是也有缺点,那就是内存利用率不高,某段内存只能用于内存申请的地方,即使该内存只使用一次,也无法让系统回收重新利用。
heap_1.c 管理方案使用两个静态变量对系统管理的内存进行跟踪内存分配:
static size_t xNextFreeByte = ( size_t ) 0;//用来定位下一个空闲的内存堆位置
static uint8_t *pucAlignedHeap = NULL;//是一个指向对齐后的内存堆起始地址
3.1.1 内存申请函数 pvPortMalloc()
内存申请函数就是用于申请一块用户指定大小的内存空间,当系统管理的内存空间满足用户需要的大小的时候,就能申请成功,并且返回内存空间的起始地址,内存申请函数源码(注意是heap_1.c):
void *pvPortMalloc(size_t xWantedSize)
{
void *pvReturn = NULL;
static uint8_t *pucAlignedHeap = NULL;
/* 内存对齐处理(如果不是1字节对齐) */
#if (portBYTE_ALIGNMENT != 1) //(1)
{
/* 调整申请大小以满足对齐要求 */
if (xWantedSize & portBYTE_ALIGNMENT_MASK) {
xWantedSize += (portBYTE_ALIGNMENT -
(xWantedSize & portBYTE_ALIGNMENT_MASK));
}
}
#endif
/* 挂起调度器以保证线程安全 */
vTaskSuspendAll();
{
/* 首次使用时初始化对齐堆指针 */
if (pucAlignedHeap == NULL) {
/* 第一次使用,确保内存堆起始位置正确对齐,
系统需要保证 pucAlignedHeap 也是在按照指定内存要求对齐的,
通过这里可以知道,初始化 pucAlignedHeap 时并不是一定等于&ucHeap[0]的,
而是会根据字节对齐的要求,在&ucHeap[0]和&ucHeap[portBYTE_ALIGNMENT]之间 */
pucAlignedHeap = (uint8_t *)(((portPOINTER_SIZE_TYPE)&ucHeap[portBYTE_ALIGNMENT]) &
(~((portPOINTER_SIZE_TYPE)portBYTE_ALIGNMENT_MASK));
}
/* 检查可用内存是否足够,边界检测,如果已经使用的内存空间 + 新申请的内存大小 */
if (((xNextFreeByte + xWantedSize) < configADJUSTED_HEAP_SIZE) &&
((xNextFreeByte + xWantedSize) > xNextFreeByte))
{
/* 分配内存并更新指针 */
pvReturn = pucAlignedHeap + xNextFreeByte;
xNextFreeByte += xWantedSize;
}
traceMALLOC(pvReturn, xWantedSize);
}
/* 恢复调度器 */
(void)xTaskResumeAll();
#if (configUSE_MALLOC_FAILED_HOOK == 1)
{
/* 内存分配失败时调用钩子函数 */
if (pvReturn == NULL) {
extern void vApplicationMallocFailedHook(void);
vApplicationMallocFailedHook();
}
}
#endif
return pvReturn;
}
(1):如果系统要求内存对齐的字节不是按 1 字节对齐,那么就把要申请的内存大小 xWantedSize 按照要求对齐。举个例子,如果系统设置按 8 字节对齐,我们本来想要申请的内存大小 xWantedSize 是 30 个字节,与 portBYTE_ALIGNMENT_MASK 相与的结果是 2,这代表着我们申请的内存与系统设定对齐不一致,为了内存统一对齐,系统会再多给我们分配 2 个字节,也就是 32个字节。实际上可能我们不应该用到后面的 2个字节,因为我们只申请了 30个字节。
在使用内存申请函数之前,需要将管理的内存进行初始化,需要将变量 pucAlignedHeap 指向内存域第一个地址对齐处,因为系统管理的内存其实是一个大数组,而编译器为这个数组分配的起始地址是随机的,不一定符合系统的对齐要求,这时候要进行内存地址对齐操作。比如数组 ucHeap 的地址从 0x20000123 处开始,系统按照 8 字节对齐:
在内存对齐完成后,用户想要申请一个 30字节大小的内存,那么按照系统对齐的要求,我们会申请到 32 个字节大小的内存空间,即使我们只需要 30 字节的内存,申请完成的示意图:
3.2 heap_2.c
heap_2.c 方案与 heap_1.c 方案采用的内存管理算法不一样,它采用一种最佳匹配算法(best fit algorithm),比如我们申请 100 字节的内存,而可申请内存中有三块对应大小 200 字节, 500 字节和 1000 字节大小的内存块,按照算法的最佳匹配,这时候系统会把 200 字节大小的内存块进行分割并返回申请内存的起始地址,剩余的内存则插回链表留待下次申请。Heap_2.c 方案支持释放申请的内存,但是它不能把相邻的两个小的内存块合成一个大的内存块,对于每次申请内存大小都比较固定的,这个方式是没有问题的,而对于每次申请并不是固定内存大小的则会造成内存碎片。
heap_2.c 方案与 heap_1 方案在内存堆初始化的时候操作都是一样的,在内存中开辟了一个静态数组作为堆的空间,大小由用户定义,然后进行字节对齐处理。
heap_2.c 方案采用链表的数据结构记录空闲内存块,将所有的空闲内存块组成一个空闲内存块链表,FreeRTOS 采用 2 个 BlockLink_t 类型的局部静态变量 xStart、xEnd 来标识空闲内存块链表的起始位置与结束位置,空闲内存块链表结构体:
typedef struct A_BLOCK_LINK {
struct A_BLOCK_LINK *pxNextFreeBlock;//是指向下一个空闲内存块的指针。
size_t xBlockSize;//用于记录申请的内存块的大小,包括链表结构体大小。
} BlockLink_t;
3.2.1 内存申请函数 pvPortMalloc()
heap_2.c 内存管理方案采用最佳匹配算法管理内存,系统会先从内存块空闲链表头开始进行遍历,查找符合用户申请大小的内存块(内存块空闲链表按内存块大小升序排列,所以最先返回的的块一定是最符合申请内存大小,所谓的最匹配算法就是这个意思来的)。
当找到内存块的时候,返回该内存块偏移 heapSTRUCT_SIZE 个字节后的地址,因为在每块内存块前面预留的节点是用于记录内存块的信息,用户不需要也不允许操作这部分内存。
在申请内存成功的同时系统还会判断当前这块内存是否有剩余(大于一个链表节点所需内存空间),这样子就表示剩下的内存块还是能存放东西的,也要将其利用起来。如果有剩余的内存空间,系统会将内存块进行分割,在剩余的内存块头部添加一个内存节点,并且完善该空闲内存块的信息,然后将其按内存块大小插入内存块空闲链表中,供下次分配使用,其中 prvInsertBlockIntoFreeList() 这个函数就是把节点按大小插入到链表中。下面一起看看源码是怎么实现的:
void *pvPortMalloc(size_t xWantedSize)
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
static BaseType_t xHeapHasBeenInitialised = pdFALSE;
void *pvReturn = NULL;
/* 挂起调度器以保证线程安全 */
vTaskSuspendAll();
{
/* 首次调用时初始化堆 */
if (xHeapHasBeenInitialised == pdFALSE) {
prvHeapInit();//(1)初始化链表
xHeapHasBeenInitialised = pdTRUE;
}
/* 仅处理有效申请大小 */
if (xWantedSize > 0) {
/* 调整要分配的内存值,需要增加上链表结构体所占的内存空间
heapSTRUCT_SIZE 表示链表结构体节点经过内存对齐后的内存大小
因为空余内存的头部要放一个 BlockLink_t 类型的节点来管理,
因此这里需要人为的扩充下申请的内存大小 */
xWantedSize += heapSTRUCT_SIZE;
/* 内存对齐调整,需要申请的内存大小与系统要求对齐的字节数不匹配 */
if ((xWantedSize & portBYTE_ALIGNMENT_MASK) != 0) {
xWantedSize += (portBYTE_ALIGNMENT -
(xWantedSize & portBYTE_ALIGNMENT_MASK));
}
}
/* 检查申请大小是否合法 */
if ((xWantedSize > 0) && (xWantedSize < configADJUSTED_HEAP_SIZE)) {
/* 遍历空闲链表寻找合适块 */
pxPreviousBlock = &xStart;
pxBlock = xStart.pxNextFreeBlock;
while ((pxBlock->xBlockSize < xWantedSize) &&
(pxBlock->pxNextFreeBlock != NULL)) {
pxPreviousBlock = pxBlock;
pxBlock = pxBlock->pxNextFreeBlock;
}
/* 找到合适内存块时的处理 */
if (pxBlock != &xEnd) {
/* 返回用户可用地址(跳过链表头结构) */
pvReturn = (void *)(((uint8_t *)pxPreviousBlock->pxNextFreeBlock) + heapSTRUCT_SIZE);
/* 从空闲链表移除该块 */
pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;
/* 检查是否可分割剩余空间 */
if ((pxBlock->xBlockSize - xWantedSize) > heapMINIMUM_BLOCK_SIZE) {
/* 创建新的空闲块 */
pxNewBlockLink = (void *)(((uint8_t *)pxBlock) + xWantedSize);
pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
pxBlock->xBlockSize = xWantedSize;
/* 将剩余空间插入空闲链表 */
prvInsertBlockIntoFreeList(pxNewBlockLink);
}
/* 更新剩余内存统计 */
xFreeBytesRemaining -= pxBlock->xBlockSize;
}
}
traceMALLOC(pvReturn, xWantedSize);
}
/* 恢复调度器 */
(void)xTaskResumeAll();
#if (configUSE_MALLOC_FAILED_HOOK == 1)
{
/* 分配失败时调用钩子函数 */
if (pvReturn == NULL) {
extern void vApplicationMallocFailedHook(void);
vApplicationMallocFailedHook();
}
}
#endif
return pvReturn;
}
(1):如果是第一次调用内存分配函数,先调用 prvHeapInit()函数初始化内存堆,该函数源码:
/**
* @brief 初始化内存堆管理结构
* @note 必须在首次内存分配前调用,且仅调用一次
*/
static void prvHeapInit(void)
{
BlockLink_t *pxFirstFreeBlock;
uint8_t *pucAlignedHeap;
/* 计算对齐后的堆起始地址(确保地址符合系统对齐要求) */
pucAlignedHeap = (uint8_t *)(
((portPOINTER_SIZE_TYPE)&ucHeap[portBYTE_ALIGNMENT]) &
(~((portPOINTER_SIZE_TYPE)portBYTE_ALIGNMENT_MASK)
);
/*---------------- 空闲链表头初始化 ----------------*/
xStart.pxNextFreeBlock = (void *)pucAlignedHeap; // 指向第一个可用块
xStart.xBlockSize = (size_t)0; // 头节点大小为0
/*---------------- 空闲链表尾初始化 ----------------*/
xEnd.xBlockSize = configADJUSTED_HEAP_SIZE; // 尾节点标记总大小
xEnd.pxNextFreeBlock = NULL; // 尾节点无后继
/*---------------- 初始化第一个可用内存块 ----------------*/
pxFirstFreeBlock = (void *)pucAlignedHeap;
pxFirstFreeBlock->xBlockSize = configADJUSTED_HEAP_SIZE; // 整个堆作为单个空闲块
pxFirstFreeBlock->pxNextFreeBlock = &xEnd; // 指向尾节点
}
随着内存申请,越来越多申请的内存块脱离空闲内存链表,但链表仍是以 xStart 节点开头以 xEnd 节点结尾,空闲内存块链表根据空闲内存块的大小进行排序。每当用户申请一次内存的时候,系统都要分配一个 BlockLink_t 类型结构体空间,用于保存申请的内存块信息,并且每个内存块在申请成功后会脱离空闲内存块链表,申请两次后的内存示意图:
3.2.2 内存释放函数 vPortFree()
分配内存的过程简单,那么释放内存的过程更简单,只需要向内存释放函数中传入要释放的内存地址,那么系统会自动向前索引到对应链表节点,并且取出这块内存块的信息,将这个节点插入到空闲内存块链表中,将这个内存块归还给系统,下面来看看 vPortFree()的源码:
void vPortFree(void *pv)
{
uint8_t *puc = (uint8_t *)pv;
BlockLink_t *pxLink;
if (pv != NULL) {
/* 根据要释放的内存块找到对应的链表节点 */
puc -= heapSTRUCT_SIZE;
pxLink = (void *)puc;
vTaskSuspendAll();
{
/* 将要释放的内存块添加到空闲链表 */
prvInsertBlockIntoFreeList(((BlockLink_t *)pxLink));
/* 更新一下当前的未分配的内存大小 */
xFreeBytesRemaining += pxLink->xBlockSize;
traceFREE(pv, pxLink->xBlockSize);
}
(void)xTaskResumeAll();
}
}
释放一个内存块:
释放完成:
从内存的申请与释放看来,heap_2.c 方案采用的内存管理算法虽然是高效但还是有缺陷的,由于在释放内存时不会将相邻的内存块合并,所以这可能造成内存碎片,当然并不是说这种内存管理算法不好,只不过对使用的条件比较苛刻,要求用户每次创建或释放的任务、队列等必须大小相同如果分配或释放的内存是随机的,绝对不可以用这种内存管理策略;如果申请和释放的顺序不可预料,那也很危险。
3.3 heap_3.c
heap_3.c 方案只是简单的封装了标准 C 库中的 malloc()和 free()函数,并且能满足常用的编译器。重新封装后的 malloc()和 free()函数具有保护功能,采用的封装方式是操作内存前挂起调度器、完成后再恢复调度器。
heap_3.c 方案中的内存申请与释放相关函数源码过于简单,就不再讲述,源码具体见:
pvPortMalloc()源码(heap_3.c):
void *pvPortMalloc(size_t xWantedSize)
{
void *pvReturn;
vTaskSuspendAll();
{
pvReturn = malloc(xWantedSize);
traceMALLOC(pvReturn, xWantedSize);
}
(void)xTaskResumeAll();
#if (configUSE_MALLOC_FAILED_HOOK == 1)
{
if (pvReturn == NULL) {
extern void vApplicationMallocFailedHook(void);
vApplicationMallocFailedHook();
}
}
#endif
return pvReturn;
}
vPortFree()源码(heap_3.c):
void vPortFree( void *pv )
{
if ( pv )
{
vTaskSuspendAll();
{
free( pv );
traceFREE( pv, 0 );
}
( void ) xTaskResumeAll();
}
}
3.4 heap_4.c
heap_4.c 方案与 heap_2.c 方案一样都采用最佳匹配算法来实现动态的内存分配,但是不一样的是 heap_4.c 方案还包含了一种合并算法,能把相邻的空闲的内存块合并成一个更大的块,这样可以减少内存碎片。heap_4.c 方案特别适用于移植层中可以直接使用pvPortMalloc()和 vPortFree()函数来分配和释放内存的代码。
heap_4.c 内存管理方案的空闲块链表不是以内存块大小进行排序的,而是以内存块起始地址大小排序,内存地址小的在前,地址大的在后,因为 heap_4.c 方案还有一个内存合并算法,在释放内存的时候,假如相邻的两个空闲内存块在地址上是连续的,那么就可以合并为一个内存块,这也是为了适应合并算法而作的改变。
3.4.1 内存申请函数 pvPortMalloc()
heap_4.c 方案的内存申请函数与 heap_2.c 方案的内存申请函数大同小异,同样是从链表头 xStart 开始遍历查找合适的内存块,如果某个空闲内存块的大小能容得下用户要申请的内存,则将这块内存取出用户需要内存空间大小的部分返回给用户,剩下的内存块组成一个新的空闲块,按照空闲内存块起始地址大小顺序插入到空闲块链表中,内存地址小的在前,内存地址大的在后。在插入到空闲内存块链表的过程中,系统还会执行合并算法将地址相邻的内存块进行合并:判断这个空闲内存块是相邻的空闲内存块合并成一个大内存块,如果可以则合并,合并算法是 heap_4.c 内存管理方案和 heap_2.c 内存管理方案最大的不同之处,这样一来,会导致的内存碎片就会大大减少,内存管理方案适用性就很强,能一样随机申请和释放内存的应用中,灵活性得到大大的提高,下面来看看 heap_4.c 的内存申请源码:
void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
void *pvReturn = NULL;
vTaskSuspendAll();
{
/* 如果是第一次调用内存分配函数,先初始化内存堆 */
if ( pxEnd == NULL ) {
prvHeapInit(); (1)
} else {
mtCOVERAGE_TEST_MARKER();
}
/* 这里 xWantedSize 的大小有要求,需要最高位为 0。
* 因为后面 BlockLink_t 结构体中的 xBlockSize 的最高位需要使用
* 这个成员的最高位被用来标识这个块是否空闲。因此要申请的块大小不能使用这个位
*/
if ( ( xWantedSize & xBlockAllocatedBit ) == 0 ) { (2)
/* 调整要分配的内存值,需要增加上链表结构体所占的内存空间
* heapSTRUCT_SIZE 表示链表结构体节点经过内存对齐后的内存大小
* 因为空余内存的头部要放一个 BlockLink_t 类型的节点来管理,
* 因此这里需要人为的扩充下申请的内存大小
*/
if ( xWantedSize > 0 ) {
xWantedSize += xHeapStructSize;
/* 需要申请的内存大小与系统要求对齐的字节数不匹配,需要进行内存对齐 */
if ( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 ) {
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
} else {
mtCOVERAGE_TEST_MARKER();
}
} else {
mtCOVERAGE_TEST_MARKER();
}
//如果当前的空闲内存足够满足用户申请的内存大小,就进行内存申请操作
if ( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) ) {
/* 从空余内存链表的头部开始找,如果该空余内存的大小>xWantedSize,
* 就从这块内存中抠出一部分内存返回,剩余的内存生成新的 BlockLink_t 插入链表中
*/
pxPreviousBlock = &xStart;
pxBlock = xStart.pxNextFreeBlock;
//从链表头部开始查找大小符合条件的空余内存
while ( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) ) {
pxPreviousBlock = pxBlock;
pxBlock = pxBlock->pxNextFreeBlock;
}
/*
* 如果搜索到链表尾 xEnd,说明没有找到合适的空闲内存块,否则进行下一步处理
*/
if ( pxBlock != pxEnd ) {
/* 能执行到这里,说明已经找到合适的内存块了,找到内存块,就
* 返回内存块地址,注意了:这里返回的是内存块 +
* 内存块链表结构体空间的偏移地址,因为内存块头部需要有一个空闲
* 链表节点
*/
pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize );
//* 因为这个内存块被用户使用了,需要从空闲内存块链表中移除 */
pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;
/*再看看这个内存块的内存空间够不够多,能不能分成两个,
* 申请的内存块就给用户,剩下的内存就留出来,
* 放到空闲内存块链表中作为下一次内存块申请。
*/
if((pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE ) {
/* 去除分配出去的内存,在剩余内存块的起始位置放置一个链表节点*/
pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );
configASSERT( ( ( ( size_t ) pxNewBlockLink ) & portBYTE_ALIGNMENT_MASK ) == 0 );
/* 通过计算得到剩余的内存大小,并且赋值给剩余内存块链表节点中的 xBlockSize 成员变量,方便下一次的内存查找 */
pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
pxBlock->xBlockSize = xWantedSize;
/* 将被切割而产生的新空闲内存块添加到空闲链表中 */
prvInsertBlockIntoFreeList( pxNewBlockLink ); (3)
} else {
mtCOVERAGE_TEST_MARKER();
}
//更新剩余内存总大小
xFreeBytesRemaining -= pxBlock->xBlockSize;
//如果当前内存大小小于历史最小记录,更新历史最小内存记录
if ( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining ) {
xMinimumEverFreeBytesRemaining = xFreeBytesRemaining; (4)
} else {
mtCOVERAGE_TEST_MARKER();
}
/* 注意这里的 xBlockSize 的最高位被设置为 1,标记内存已经被申请使用*/
pxBlock->xBlockSize |= xBlockAllocatedBit; (5)
pxBlock->pxNextFreeBlock = NULL;
} else {
mtCOVERAGE_TEST_MARKER();
}
} else {
mtCOVERAGE_TEST_MARKER();
}
} else {
mtCOVERAGE_TEST_MARKER();
}
traceMALLOC( pvReturn, xWantedSize );
}
( void ) xTaskResumeAll();
#if( configUSE_MALLOC_FAILED_HOOK == 1 )
{
if ( pvReturn == NULL ) {
extern void vApplicationMallocFailedHook( void );
vApplicationMallocFailedHook();
} else {
mtCOVERAGE_TEST_MARKER();
}
}
#endif
return pvReturn;
}
这个合并的算法常用于释放内存的合并,申请内存的时候能合并的早已合并,因为申请内存是从一个空闲内存块前面分割,分割后产生的内存块都是一整块的,基本不会进行合并。
3.4.2 内存释放函数 vPortFree()
heap_4.c 内存管理方案的内存释放函数 vPortFree()也比较简单,根据传入要释放的内存块地址,偏移之后找到链表节点,然后将这个内存块插入到空闲内存块链表中,在内存块插入过程中会执行合并算法,这个我们已经在内存申请中讲过了(而且合并算法多用于释放内存中)。最后是将这个内存块标志为“空闲”(内存块节点的 xBlockSize 成员变量最高位清 0)、再更新未分配的内存堆大小即可,下面来看看 vPortFree()的源码:
void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;
if ( pv != NULL ) {
/* 偏移得到节点地址 */
puc -= xHeapStructSize; (1)
pxLink = ( void * ) puc;
/* 断言 */
configASSERT( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 );
configASSERT( pxLink->pxNextFreeBlock == NULL );
/* 判断一下内存块是否已经是被分配使用的,如果是就释放该内存块 */
if ( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 ) { (2)
if ( pxLink->pxNextFreeBlock == NULL ) {
/* 将内存块标识为空闲 */
pxLink->xBlockSize &= ~xBlockAllocatedBit; (3)
vTaskSuspendAll();
{
/* 更新系统当前空闲内存的大小,添加到内存块空闲链表中 */
xFreeBytesRemaining += pxLink->xBlockSize; (4)
traceFREE( pv, pxLink->xBlockSize );
prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) ); (5)
}
( void ) xTaskResumeAll();
} else {
mtCOVERAGE_TEST_MARKER();
}
} else {
mtCOVERAGE_TEST_MARKER();
}
}
}
释放一块内存无法合并:
释放一块内存可以合并:
3.5 heap_5.c
heap_5.c 方案在实现动态内存分配时与 heap4.c 方案一样,采用最佳匹配算法和合并算法,并且允许内存堆跨越多个非连续的内存区,也就是允许在不连续的内存堆中实现内存分配,比如用户在片内 RAM 中定义一个内存堆,还可以在外部 SDRAM 再定义一个或多个内存堆,这些内存都归系统管理。
heap_5.c 方案通过调用 vPortDefineHeapRegions()函数来实现系统管理的内存初始化,在内存初始化未完成前不允许使用内存分配和释放函数。如创建 FreeRTOS 对象(任务、队列、信号量等)时会隐式的调用 pvPortMalloc()函数,因此必须注意:使用 heap_5.c 内存管理方案创建任何对象前,要先调用 vPortDefineHeapRegions()函数将内存初始化。
vPortDefineHeapRegions()函数只有一个形参,该形参是一个 HeapRegion_t 类型的结构体数组。HeapRegion_t 类型结构体在 portable.h 中定义:
typedef struct HeapRegion {
/* 用于内存堆的内存块起始地址*/
uint8_t *pucStartAddress;
/* 内存块大小 */
size_t xSizeInBytes;
} HeapRegion_t;
4. 举例
内存管理实验使用 heap_4.c 方案进行内存管理测试,创建了两个任务,分别是 LED 任务与内存管理测试任务,内存管理测试任务通过检测按键是否按下来申请内存或释放内存,当申请内存成功就像该内存写入一些数据,如当前系统的时间等信息,并且通过串口输出相关信息;LED 任务是将 LED 翻转,表示系统处于运行状态。在不需要再使用内存时,注意要及时释放该段内存,避免内存泄露。
这里我使用之前创建好的空白工程:
在空白文档加点LED电平翻转的函数,方便观察现象:
void Toggle_LED_R(void)
{
BitAction LED_R = (BitAction)(1 - GPIO_ReadOutputDataBit(LED1_GPIO_PORT, LED1_GPIO_PIN));
GPIO_WriteBit(LED1_GPIO_PORT, LED1_GPIO_PIN, LED_R);
}
上面说了两个任务,那就是两个任务句柄:
static TaskHandle_t LED_Task_Handle = NULL;/* LED_Task任务句柄 */
static TaskHandle_t Test_Task_Handle = NULL;/* Test_Task任务句柄 */
任务句柄有了,就开始对任务的主体进行搭建,在开始前我们先创建一个全局变量,用于判断是否申请到内存:
uint8_t *Test_Ptr = NULL;
我们先完善按键KEY1的功能,首先,当按键KEY1按下,先判断是否申请到内存,如果没有继续申请,如果有就先去释放。对于内存的申请,我们首先调用xPortGetFreeHeapSize();获取当前内存的大小,然后通过pvPortMalloc();继续申请内存,此时在判断是否申请成功,如果成功打印数据:
if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
{
/* KEY1 被按下 */
if(NULL == Test_Ptr)
{
/* 获取当前内存大小 */
g_memsize = xPortGetFreeHeapSize();
printf("\r\n系统当前内存大小为 %d 字节,开始申请内存!\r\n",g_memsize);
Test_Ptr = pvPortMalloc(1024);
if(NULL != Test_Ptr)
{
printf("\r\n内存申请成功!\r\n");
printf("申请到的内存地址为%#x!\r\n",(int)Test_Ptr);
/* 获取当前内剩余存大小 */
g_memsize = xPortGetFreeHeapSize();
printf("\r\n系统当前内存剩余存大小为 %d 字节!\r\n",g_memsize);
//向Test_Ptr中写入当数据:当前系统时间
sprintf((char*)Test_Ptr,"当前系统TickCount = %d !\r\n",xTaskGetTickCount());
printf("写入的数据是 %s \n",(char*)Test_Ptr);
}
}
else
{
printf("\r\n请先按下KEY2释放内存再申请!\r\n");
}
}
对于释放,主要调用vPortFree();函数:
if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )
{
/* KEY2 被按下 */
if(NULL != Test_Ptr)
{
printf("\r\n释放内存!\r\n");
vPortFree(Test_Ptr); //释放内存
Test_Ptr=NULL;
/* 获取当前内剩余存大小 */
g_memsize = xPortGetFreeHeapSize();
printf("系统当前内存大小为 %d 字节,内存释放完成!\r\n",g_memsize);
}
else
{
printf("\r\n请先按下KEY1申请内存再释放!\r\n");
}
}
完整的Test_Task()任务主体:
//Test_Task任务主体
static void Test_Task(void* parameter)
{
uint32_t g_memsize;
while (1)
{
if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
{
/* KEY1 被按下 */
if(NULL == Test_Ptr)
{
/* 获取当前内存大小 */
g_memsize = xPortGetFreeHeapSize();
printf("\r\n系统当前内存大小为 %d 字节,开始申请内存!\r\n",g_memsize);
Test_Ptr = pvPortMalloc(1024);
if(NULL != Test_Ptr)
{
printf("\r\n内存申请成功!\r\n");
printf("申请到的内存地址为%#x!\r\n",(int)Test_Ptr);
/* 获取当前内剩余存大小 */
g_memsize = xPortGetFreeHeapSize();
printf("\r\n系统当前内存剩余存大小为 %d 字节!\r\n",g_memsize);
//向Test_Ptr中写入当数据:当前系统时间
sprintf((char*)Test_Ptr,"当前系统TickCount = %d !\r\n",xTaskGetTickCount());
printf("写入的数据是 %s \n",(char*)Test_Ptr);
}
}
else
{
printf("\r\n请先按下KEY2释放内存再申请!\r\n");
}
}
if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )
{
/* KEY2 被按下 */
if(NULL != Test_Ptr)
{
printf("\r\n释放内存!\r\n");
vPortFree(Test_Ptr); //释放内存
Test_Ptr=NULL;
/* 获取当前内剩余存大小 */
g_memsize = xPortGetFreeHeapSize();
printf("系统当前内存大小为 %d 字节,内存释放完成!\r\n",g_memsize);
}
else
{
printf("\r\n请先按下KEY1申请内存再释放!\r\n");
}
}
vTaskDelay(20);/* 延时20个tick */
}
}
另一个任务就是一个电平翻转,标明系统现在的运行状态:
//LED_Task任务主体
static void LED_Task(void* parameter)
{
while (1)
{
Toggle_LED_R();
vTaskDelay(1000);/* 延时1000个tick */
}
}
任务主体构建完了,就开始创建任务,分配栈空间、优先级等信息:
//任务创建函数
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
taskENTER_CRITICAL(); //进入临界区
/* 创建LED_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )LED_Task, /* 任务入口函数 */
(const char* )"LED_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )2, /* 任务的优先级 */
(TaskHandle_t* )&LED_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建LED_Task任务成功!\r\n");
/* 创建Test_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )Test_Task, /* 任务入口函数 */
(const char* )"Test_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )3, /* 任务的优先级 */
(TaskHandle_t* )&Test_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建Test_Task任务成功!\r\n");
vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
taskEXIT_CRITICAL(); //退出临界区
}
完整main函数:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "Usart.h"
#include "Key.h"
/* FreeRTOS头文件 */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
/*
* 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
* 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
* 这个句柄可以为NULL。
*/
static TaskHandle_t AppTaskCreate_Handle = NULL; /* 创建任务句柄 */
static TaskHandle_t LED_Task_Handle = NULL;/* LED_Task任务句柄 */
static TaskHandle_t Test_Task_Handle = NULL;/* Test_Task任务句柄 */
/********************************** 内核对象句柄 *********************************/
/*
* 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
* 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
* 们就可以通过这个句柄操作这些内核对象。
*
* 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
* 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
* 来完成的
*
*/
uint8_t *Test_Ptr = NULL;
/******************************* 宏定义 ************************************/
/*
* 当我们在写应用程序的时候,可能需要用到一些宏定义。
*/
//一些函数声明
static void AppTaskCreate(void);/* 用于创建任务 */
static void LED_Task(void* pvParameters);/* LED_Task任务实现 */
static void Test_Task(void* pvParameters);/* Test_Task任务实现 */
static void All_Function_Init(void);/* 用于初始化板载相关资源 */
int main(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
All_Function_Init();//硬件初始化
while (1)
{
/* 创建AppTaskCreate任务 */
xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate, /* 任务入口函数 */
(const char* )"AppTaskCreate",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )1, /* 任务的优先级 */
(TaskHandle_t* )&AppTaskCreate_Handle);/* 任务控制块指针 */
/* 启动任务调度 */
if(pdPASS == xReturn)
vTaskStartScheduler(); /* 启动任务,开启调度 */
else
return -1;
}
}
//任务创建函数
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
taskENTER_CRITICAL(); //进入临界区
/* 创建LED_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )LED_Task, /* 任务入口函数 */
(const char* )"LED_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )2, /* 任务的优先级 */
(TaskHandle_t* )&LED_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建LED_Task任务成功!\r\n");
/* 创建Test_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )Test_Task, /* 任务入口函数 */
(const char* )"Test_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )3, /* 任务的优先级 */
(TaskHandle_t* )&Test_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建Test_Task任务成功!\r\n");
vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
taskEXIT_CRITICAL(); //退出临界区
}
//LED_Task任务主体
static void LED_Task(void* parameter)
{
while (1)
{
Toggle_LED_R();
vTaskDelay(1000);/* 延时1000个tick */
}
}
//Test_Task任务主体
static void Test_Task(void* parameter)
{
uint32_t g_memsize;
while (1)
{
if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
{
/* KEY1 被按下 */
if(NULL == Test_Ptr)
{
/* 获取当前内存大小 */
g_memsize = xPortGetFreeHeapSize();
printf("\r\n系统当前内存大小为 %d 字节,开始申请内存!\r\n",g_memsize);
Test_Ptr = pvPortMalloc(1024);
if(NULL != Test_Ptr)
{
printf("\r\n内存申请成功!\r\n");
printf("申请到的内存地址为%#x!\r\n",(int)Test_Ptr);
/* 获取当前内剩余存大小 */
g_memsize = xPortGetFreeHeapSize();
printf("\r\n系统当前内存剩余存大小为 %d 字节!\r\n",g_memsize);
//向Test_Ptr中写入当数据:当前系统时间
sprintf((char*)Test_Ptr,"当前系统TickCount = %d !\r\n",xTaskGetTickCount());
printf("写入的数据是 %s \n",(char*)Test_Ptr);
}
}
else
{
printf("\r\n请先按下KEY2释放内存再申请!\r\n");
}
}
if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )
{
/* KEY2 被按下 */
if(NULL != Test_Ptr)
{
printf("\r\n释放内存!\r\n");
vPortFree(Test_Ptr); //释放内存
Test_Ptr=NULL;
/* 获取当前内剩余存大小 */
g_memsize = xPortGetFreeHeapSize();
printf("系统当前内存大小为 %d 字节,内存释放完成!\r\n",g_memsize);
}
else
{
printf("\r\n请先按下KEY1申请内存再释放!\r\n");
}
}
vTaskDelay(20);/* 延时20个tick */
}
}
//初始化声明
static void All_Function_Init(void)
{
/*
* STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
* 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
* 都统一用这个优先级分组,千万不要再分组,切忌。
*/
NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
/* LED 初始化 */
LED_GPIO_Config();
/* 串口初始化 */
USART_Config();
//按键初始化
Key_GPIO_Config();
}
运行结果:
完整工程:
5. 总结
5.1 heap_1.c
特点:① 最简单的实现,仅支持 pvPortMalloc,不支持 vPortFree(内存一旦分配就无法释放)。
② 内存分配是静态的(编译时确定堆大小)。
适用场景:仅需在初始化时分配内存,且后续无需释放的应用(如嵌入式系统中静态任务/队列的创建)。
优缺点:
✅ 代码极小,无碎片问题。
❌ 无法动态释放内存。
5.2 heap_2.c
特点:① 支持 pvPortMalloc 和 vPortFree,但使用首次适应算法(First Fit)。
② 不会合并相邻空闲块,可能导致内存碎片。
适用场景:需要动态分配/释放内存,但分配块大小固定或较少变化的场景(如重复分配相同大小的对象)。
优缺点:
✅ 支持内存释放。
❌ 长期运行后碎片化严重(不推荐长期使用)。
5.3 heap_3.c
特点:① 是对标准库 malloc/free 的封装,依赖编译器的堆管理。
② 通过 heap_3.c 调用 malloc/free,并添加了线程安全保护(临界区)。
适用场景:① 需要兼容已有代码(使用标准库 malloc/free)的项目。
② 适用于有MMU的系统(如Linux移植)。
优缺点:
✅ 兼容性强。
❌ 依赖编译器实现,可能不稳定或碎片化。
5.4 heap_4.c
特点:① 支持 pvPortMalloc 和 vPortFree,使用首次适应算法 + 相邻空闲块合并。
② 显著减少碎片问题,是FreeRTOS推荐的通用实现。
适用场景:需要频繁动态分配/释放不同大小内存的长期运行应用(如复杂嵌入式系统)。
优缺点:
✅ 碎片少,稳定性高。
❌ 代码稍复杂(但仍比 heap_5.c 简单)。
5.5 heap_5.c
特点:① 在 heap_4.c 基础上支持非连续内存区域(如将多个物理上不连续的RAM区域组合成一个逻辑堆)。
② 需通过 vPortDefineHeapRegions() 显式配置内存区域。
适用场景:内存分散的复杂系统(如多块SRAM、外部SDRAM等)。
优缺点:
✅ 灵活支持非连续内存。
❌ 配置复杂,需手动管理内存区域。
实现 | 是否支持释放 | 碎片处理 | 适用场景 | 复杂度 |
---|---|---|---|---|
heap_1 | ❌ | 无碎片 | 静态分配 | 极低 |
heap_2 | ✅ | 无合并(高碎片) | 固定块大小分配 | 低 |
heap_3 | ✅ | 依赖编译器 | 兼容标准库 | 中 |
heap_4 | ✅ | 合并块(低碎片) | 通用动态分配 | 中高 |
heap_5 | ✅ | 合并块 + 非连续 | 多区域内存(如SDRAM+SRAM) | 高 |