1、问题
项目组在使用RTOS开发项目时,时常会出现栈空间分配失败,导致线程创建失败的问题。FreeRTOS每个任务都有自己独立的栈空间,任务创建时设置的大小直接对应一片内存空间,使用全局的定长内存池,这对内存管理方式比较简单,但对应内存的动态管理提出了更高的要求。
为了提高内存申请/释放的效率,需要弄清楚rtos系统的内存分配机制,虽然项目可以通过调整线程栈大小和总体的内存分配来解决栈空间分配失败的问题,但依旧存在以下几个问题,搞不清楚这些问题,我们系统的中的问题就无法根本性解决。
- RTOS的栈内存的分配机制是怎样的?
- RTOS栈的内存碎片回收是怎样做到的?
- RTOS有没有有效的栈内存统计方案?
带着这些问题,决定对FreeRTOS内存管理的源代码进行一番研究,希望能找到这些问题的答案,并解决我们系统中遇到的问题。
2、内存分区
在解读RTOS内存源码之前,需要了解系统中的内存分区情况,在C语言中定义了4个区:代码段、数据段、栈、堆。
1)代码段:存放函数体的二进制代码;
2)数据段:主要包括Data段(包括常量段、静态变量段与已初始化全局变量)与Bss段(包括未初始化变量段);
3)栈:RTOS的栈由uheap全局变量申请,编译器自动分配,用于存在任务中函数的参数值、局部变量的值等;
4)堆:一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。这里主要用于malloc和free函数申请的区域。
代码段与数据段由链接器进行分配。而对于开发板的例程中,其实没有堆栈的概念,仅是静态存储区+动态存储区。RTOS系统中以uheap全局变量在为系统任务申请的堆区,而这里我们将其理解为栈,与用malloc申请的堆进行区分。
图1 内存分区示意图
3、RTOS内存管理
在FreeRTOS的内存管理方法 heap_x.c中定义了一个uint8_t 全局静态数组 ,任务/队列所需要的内存均从这个数组中申请获取,即从静态存储区申请内存。
// FreeRTOSConfig.h中未定义该宏,为 0
#if ( configAPPLICATION_ALLOCATED_HEAP == 1 )
extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
// 用户自定义
#else
//编译器决定,默认采用这种方法
PRIVILEGED_DATA static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#endif
图2 uheap全局变量源码
任务控制块在全局变量内申请,因此内核的不同函数均可对任务进行管理。申请内存时使用 pvPortMalloc() 来申请,释放内存时,使用vPortFree()来释放内存。
FreeRTOS主要提供了5种内存管理方案,其中heap_1.c只支持分配,不支持释放内存;heap_3.c使用标准函库中的malloc与free进行内存分配。这两种分配方案在动态申请线程的项目中得不到应用,因此不进行阐述。
heap_2.c、heap_4.c、heap_5.c整体的内存分配方案基本类型,只在内存碎片回收、分配算法以及是否使用连续内存上存在差异。它们引入了内存块与链表结构,内存块前面有一个BlockLink_t类型的变量来描述内存块,占用8个字节,即如果申请16个字节,实际申请了24 个字节。这些内存块由一个链表管理,这个链表称为空闲内存块列表。
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 空闲内存块描述结构体
1)pvPortMalloc()
任务控制块内存申请的流程如图4所示,只要分为4个步骤:
1-空闲栈的初始化,将xStart栈指针指向栈顶,xEnd栈指针指向栈底,并将空闲栈进行字节对齐;
2-对任务申请内存size进行合法性校验,并进行字节对齐;
3-给任务分配栈空间;
4-将已分配的空闲块剩余空间重新回收到空闲内存块列表中;
图4 pvPortMalloc处理流程图
heap_2.c采用的是最佳适应算法方案,为了保证当“大任务”到来时能有连续的大片空间,可以尽可能多地留下大片的空闲区,即优先使用更小的空闲区。它允许释放以前分配的块,但不会将相邻的空闲块合并成一个大块,因此会产生大量的内存碎片,如图5所示。
图5 heap_2.c内存管理示意图
heap_4.c采用的是首次适应算法,每次都从低地址开始查找,找到第一个能满足大小的空闲分区为止。它会将相邻的空闲内存块合并成一个更大的块(包含一个合并算法),如图6所示。
图6 heap_4.c内存管理示意图
heap_5.c方案使用与 heap_4.c相同,使用首次适应算法和内存合并算法。但heap_5在heap_4的基础上实现了管理多个非连续内存区域的能力。heap_5默认并没有定义内存堆,需要用户手动指定内存区域的信息来进行初始化。
typedef struct HeapRegion
{
Uint8_t pucStartAddress; //内存区域的起始地址
size_t xSizeInBytes; //内存区域的大小
} HeapRegion_t;
图7 内存区域定义结构体
通过HeapRegion_t用户可手动指定内存区域,并以此来管理多块内存。如下图8所示。
Const HeapRegion_t xHeapRegions[]=
{
{(uint8_t *)0x60000000,0x50000}, //内存区域1
{(uint8_t *)0xD0000000,0xA0000}, //内存区域2
{NULL,0} //数值终止标准
};
图8 手动指定内存区域示意图
2)pvPortFree()
任务控制块内存释放的流程如图9所示,首先判断要释放的内存是否为空;若是不为空,则获取这个内存空间真正的的空闲块(剔除头部的 BlockLink_t结构),然后挂起所有任务,把释放的内存重新插入到空闲块链表中,最后调用调试信息宏,恢复挂起的任务就结束了。
图9 pvPortFree处理流程图
4、查看FreeRTOS栈的使用情况
FreeRTOS提供了检测系统栈大小的函数接口:
size_t xPortGetFreeHeapSize( void );
//获取调用时栈中空闲内存的大小,以字节为单位
size_t xPortGetMinimumEverFreeHeapSize( void );
//此函数返回FreeRTOS应用程序开始运行之后,曾经存在的最小的未被分配的存储空间的字节数。
图10 检测系统栈大小
FreeRTOS还提供了检测任务栈大小的函数uxTaskGetStackHighWaterMark(),可以打印出来该任务自启动起来最小剩余栈空间大小。然后我们就可以计算出最大使用的大小,一般可以再乘以1.5左右作为最终分配的值。需要注意的是该函数不像前面两个返回的是bytes,而返回的以字为单位,真实的bytes需要乘以4。
UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask );
/*********************************
使用说明:
要使用此函数的话宏INCLUDE_uxTaskGetStackHighWaterMark 必须为 1,
参数:
xTask: 要查询的任务的任务句柄,当这个参数为 NULL 的话说明查询自身任务(即调用函数 uxTaskGetStackHighWaterMark()的任务)的“高水位线”
返回值:
任务堆栈的“高水位线”值,也就是堆栈的历史剩余最小值。
**********************************/
图11 检测任务栈大小
除了上述三个由系统提供的检测方法外,系统在pvPortMalloc函数中还提供了可自定义的调试信息宏traceMALLOC,如图12所示。
#ifndef traceMALLOC
#define traceMALLOC( pvAddress, uiSize )
/**************
参数:pvAddress:当前pvPortMalloc中申请栈内存的地址
uiSize:当前pvPortMalloc中申请内存的大小
***************/
#endif
图12 调试信息宏traceMALLOC
可以通过traceMALLOC统计栈空间的实际内存使用情况,将获取到的内存数据导出,通过Python等工具与数据分析方法,来可视化的查看heap,以此来更可靠的进行内存评估。