Freertos内核源码解读之--------内存管理
- 内存管理
- 任务栈和系统栈的区别
- FreeRTOS内存管理方法
一、内存管理
在c语言中定义了4个区:代码区、全局变量和静态变量区、动态变量区(即栈区)、动态存储区(即堆区)。
1>栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。在STM32汇编代码中设置如下:
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
2>堆区(heap)— 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。这里主要用于malloc和free函数申请的区域,如果没有用于malloc和free函数,可以设置为0。在STM32汇编代码中设置如下:
Heap_Size EQU 0x00000200
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
3>全局变量和静态变量区 —全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的 另一块区域。 - 程序结束后由系统释放。
4>常量区 —常量字符串就是放在这里的。
5>程序代码区—存放函数体的二进制代码。
C语言中各变量存放区域的关系:
1>全局静态变量:不管是否调用,它都在那里用关键字指明,这种变量是并不是真正意义的全局变,只是在这个文件的所有位置<声明位置 以后的所有位置>可用。
2>局部静态变量:和全局静态变量类似,也是不管拉不拉屎先占坑的货,特点是加了关键字,意思是在这个位置,它是唯一的,不过该变量的作用域是在定义的代码片段之内(通俗的将就是两个大括号之间,例如if循环语句之内)。这种变量是会一直占用一个内存空间的。
3> 局部动态变量:这个是最常见的,就是我们最常用的在函数内部声明的变量。这种变量是存放在栈区,退出相应函数时自动回收。
4>全局动态变量:<全局>的意思是变量本身没有编译器指 定的生命周期,也就是<作用域>,但还有代码指定的生命周期。这种变量存放在堆区,由程序员自己进行申请和回收。
通过编译STM32的工程文件,可以看到如下编译的结果如下:
查看相应的.map文件
RO:Read-Only的缩写,包括RO-data(只读数据)和RO-code(代码)。
RW:Read-Write的缩写,主要是RW-data,Rw-data由程序初始化初始值。
ZI:Zero-initialized的缩写,主要是ZI-data,由编程器初始化为0。注:在keil中,栈区被默认是ZI段的子集。
总结:
STM32的内存分配,应该分为两种情况。
1,使用了系统的malloc。
2,未使用系统的malloc。
第一种情况(使用malloc):
STM32的内存分配规律:
从0X20000000开始依次为:静态存储区+堆区+栈区
第二种情况(不使用malloc):
STM32的内存分配规律:
从0X20000000开始依次为:静态存储区+栈区
第二种情况不存在堆区。
所以,一般对于我们开发板例程,实际上,没有所谓堆区的概念,而仅仅是:静态存储区+栈区。
无论哪种情况,所有的全局变量,包括静态变量之类的,全部存储在静态存储区。
紧跟静态存储区之后的,是堆区(如没用到malloc,则没有该区),之后是栈区。
二、任务栈和系统栈的区别
1、栈存储
栈存储是一种后进先出的数据缓冲存储形式,使用PUSH指令将数据压入栈中,POP指令将数据从栈中弹出,每次进行一次PUSH和POP操作,栈空间地址会自动调整。
栈的用处:
1>往函数或者子程序传递数据;
2>用于存储局部变量;
3>在中断等异常产生时保存处理器状态或者寄存器数值;
4>当执行一个函数时,需要保存一些临时数据,函数执行完毕之后,回复原来的数据。
在Cortex-M处理器在物理上存在两个栈指针,它们分别是:
1>主栈指针(MSP):‘复位后默认使用的栈指针,用于所有的异常处理。
2>进程栈指针(PSP):只能用于线程模式的栈指针,通常用于嵌入式OS的嵌入式系统中的应用任务。
对于上面两个栈指针寄存器是由CONTROL寄存器的第二位SPSEL的数值决定,若为0,则线程模式在栈操作时使用MSP,否则线程模式使用PSP。对于从处理模式到线程模式的异常返回期间,栈指针的选择可以由EXC_RETURN(异常返回)的数值决定,这样处理器硬件会相应的更新SPSEL的数值。
对于不带有嵌入式操作系统的应用来说,线程模式和处理模式都可以只使用MSP,如图所示,在异常事件产生后,处理器在进入中断服务程序前,会首先将多个寄存器的数值压入栈中;而在ISR结束时,这些寄存器又会被恢复到寄存器中。
若在含有嵌入式操作系统的应用中,一般会将应用程序和内核所使用的栈空间进行分离。因此,PSP寄存器就将会被用到,而且在异常入口和异常退出时,会产生SP切换。如下图所示。在自动“压栈”和“出栈”阶段使用的是PSP,而在中断处理程序中使用的是MSP寄存器。这样设计的好处是简化了OS设计的复杂度,同时提高了上下文的切换速度,还有一个好处是将任务栈和内核栈分离可以避免任务栈出现错误而影响内核栈的运行。
需要说明的是:同一时间内只有一个SP寄存器是可见的。我们在应用程序代码中无需显示访问MSP和PSP。一般在嵌入式操作系统中使用汇编代码访问MSP和PSP,例如:通过MMRS指令读取PSP的数值,OS可以从应用任务API调用的栈中读出压入的数据,而且OS的上下文切换代码会在上下文切换期间更新PSP的数值。
上电之后,处理器硬件在读取向量表之后会自动初始化MSP,PSP不会被自动初始化, 需要在使用前由软件进行初始化。
经过上面的说明,对于含有操作系统的应用来说,异常处理(包括部分OS内核)使用MSP,而应用任务使用PSP。每个应用任务都有自己的栈空间,如图下所示,OS每次进行上下文切换都会更新PSP。
这种设计的优点如下:
1>如果其中的一个应用程序由于某种原因破坏了栈,由于每个应用程序的栈和内核的栈是分开的,因此不会影响到其他应用程序或者内核的栈,这样可以提高整个系统的可靠性;
2>有利于Cortex-M处理器搭载嵌入式OS;
3>这样分开的好处最明显的一个特点是,给每个任务栈只需要满足对栈的最大需求加上一级栈帧(一级栈帧指的是:对于Cortex-M3和无浮点单元的Cortex-M4来说最大9个字,对于具有浮点数的单元Cortex-M4来说最大是27个字,这里就是一些PC,LR等一些寄存器的值),对于像ISR和嵌套中断处理的这种不确定栈空间来说会被分配到主栈空间(MSP指向的栈空间);
上电后,MSP被初始化为向量表中的数值,这也是处理器复位流程的一部分。之后可以可以利用MSR指令初始化PSP。
一般来说,使用进程栈需要将OS设置为处理模式,直接编程PSP后利用异常返回流程跳转到应用任务。如下图所示,OS从线程模式启动时,利用SVC异常进入处理模式,然后创建进程栈中的栈帧,且触发使用PSP的异常返回,当加载栈帧时,应用任务就会启动。
在OS设计中,需要在不同任务间切换,这一般被称作上下文切换,其通常在PendSV异常处理中执行,该异常是由SysTick异常触发。在上下文切换中的操作如下:
a、将当前寄存器的状态保存到当前栈中;
b、保存当前的PSP数值;
c、将PSP数值设置为下一个将要执行任务的上一次SP值;
d、恢复下一个任务的上一次各寄存器数值;
e、利用异常返回切换任务。
如下图所示是一个简单的上下文切换,上下文切换是在PendSV中进行的,其优先级会被设置为最低,这样可以避免在其他中断处理过程中产生上下文切换。
总结
只有在含有操作系统的软件中分系统栈和任务栈。系统栈提供给操作系统内核和中断服务程序进行申请自动变量、函数参数传递以及保护现场。基于上述原因,在中断服务程序中可能产生中断嵌套,因此对于系统栈的大小是不能够准确估计的。当产生中断嵌套时,需要系统栈的空间会变大;没有发生中断嵌套时,需要的系统栈的空间会变小。
任务栈是提供给用户任务进行申请自动变量、函数参数传递、保护现场。每一个任务的任务栈空间可以通过粗略计算出来。这方面内容,后续我在专门写一篇博客。
注这里要着重强调一点:系统栈和任务栈具体申请在内存的堆区还是栈区,要取决于操作系统的内存管理方式。
三、FreeRTOS内存管理方法
对于任务创建、信号量、消息队列等都需要FreeRTOS操作系统中的内存管理。FreeRTOS支持5中动态内存管理方式,分别在文件heap_1、heap_2、heap_3、heap_4和heap_5,具体代码文件的路径FreeRTOS\Source\portable\MemMang。对于FreeRTOS的内存管理方式都是在一个全局的数组中进行的,因此是动态内存管理实际上是在静态存储区。下面是具体的代码:
#define configTOTAL_HEAP_SIZE ((size_t)(20*1024)) //系统所有总的堆大小
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
1、heap_1方式
/* Allocate the memory for the heap. */
#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 */
上述代码是申请动态内存管理所需要的整个内存堆ucHeap。这里要注意的是,在一些系统应用中需要外挂SDRAM或者其他形式的RAM,这是就可以将FreeRTOS的内存堆放置在外挂的RAM中,不需要使用MCU内部的RAM。因此,我们这里要进行判断,看是否在外部RAM中申请内存堆ucHeap。在外部RAM申请内存堆ucHeap的方法是需要将"FreeRTOSConfig.h"文件中添加配置并且在外部RAM中申请内存ucHeap[]。
#define configAPPLICATION_ALLOCATED_HEAP 1
configADJUSTED_HEAP_SIZE 表示整个内存堆中可用的有效内存,为保证字节对齐,需要减去一个对齐长度。
/* A few bytes might be lost to byte aligning the heap start address. */
#define configADJUSTED_HEAP_SIZE ( configTOTAL_HEAP_SIZE - portBYTE_ALIGNMENT )
xNextFreeByte 用来记录已经分配的内存的大小,它指向下一个没有被分配的空间。前面说过内存堆ucHeap[]实际上就是一个大内存。因此,我们就可以使用它来作为偏移量找到未分配内存的位置。在每一次分配成功之后,该数值会自动增加。
static size_t xNextFreeByte = ( size_t ) 0;
内存申请函数:pvPortMalloc
void *pvPortMalloc( size_t xWantedSize )
{
void *pvReturn = NULL;
static uint8_t *pucAlignedHeap = NULL;//用于指向真正可用内存的开始地址处
/* 进行字节对齐,这里使用的是8字节对齐。有时申请的字节数xWantedSize可能不是8的整数倍,
这时候就要进行额外分配机字节。例如,如果我们申请字节为11字节,其实真正分配的字节数位16字节 */
#if( portBYTE_ALIGNMENT != 1 )//判断是否开启了字节对齐管理
{
if( xWantedSize & portBYTE_ALIGNMENT_MASK )//表示申请的字节数不是8的整数倍
{
/* 计算实际要分配的字节数,由于使用的是8字节对齐。因此portBYTE_ALIGNMENT_MASK值为7 */
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
}
}
#endif
vTaskSuspendAll();//设置调度锁,关闭调度;一般和函数xTaskResumeAll()成对使用。这里只是关闭调度器
//并没有关闭中断,换句话说包括系统时钟在内的中断还是会触发。在两个函数之间不能够调用任何引起任务切换的API函数
//两个函数之间代码执行期间如果产生系统节拍中断,那么系统会用一个全局变量uxPendedTicks进行记录次数
//关于这方面内容后续文章会进行分析
{
if( pucAlignedHeap == NULL )
{
/* 保证pucAlignedHeap 能够正确对齐内存堆ucHeap的地址,保证初始地址落在字节对齐数的整数倍,很巧妙*/
pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) &ucHeap[ portBYTE_ALIGNMENT ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
}
/* 每次分配前首先是要判断内存堆ucHeap是否有足够的空间用于分配,。这里xNextFreeByte是一个ucHeap数组的下标*/
if( ( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE ) &&
( ( xNextFreeByte + xWantedSize ) > xNextFreeByte ) )/* Check for overflow. */
{
/* 返回申请内存的首地址 */
pvReturn = pucAlignedHeap + xNextFreeByte;//返回申请到的字节的首地址
xNextFreeByte += xWantedSize;//将空闲索引指向没有被申请的空间。xNextFreeByte是一个全局变量
}
traceMALLOC( pvReturn, xWantedSize );//用于追踪内存分配情况,调试用,需要用户自己实现。
}
( void ) xTaskResumeAll();//解除调度锁,系统可以进行任务调度
#if( configUSE_MALLOC_FAILED_HOOK == 1 )//内存分配失败钩子函数开关,如果定义了内存分配失败钩子函数,就会设置该宏为1
{
if( pvReturn == NULL )
{
extern void vApplicationMallocFailedHook( void );
vApplicationMallocFailedHook();//内存分配失败后调用的函数,这个函数名字系统已经该我们起好了,我们只需要实现具体函数体就可以,这个只有在内存分配失败之后调用
}
}
#endif
return pvReturn;//返回分配后的地址
}
内存堆大小与地址示意图
进行一次内存分配之后的内存分布与地址关系
内存释放函数,对于这种方式,不涉及内存释放。
void vPortFree( void *pv )
{
/* Memory cannot be freed using this scheme. See heap_2.c, heap_3.c and
heap_4.c for alternative implementations, and the memory