1.内存分区
在C语言
中定义了4个区:代码区、全局变量和静态变量区、动态变量区(即栈区)、动态存储区(即堆区)。
- 栈区
(stack)
: 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。在STM32
汇编代码中设置如下:
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
- 堆区
(heap)
: 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。这里主要用于malloc
和free
函数申请的区域,如果没有用于malloc
和free
函数,可以设置为0。在STM32汇编代码中设置如下:
也就是说,如果代码中没有使用
malloc
和free
函数,则没有堆区。
Heap_Size EQU 0x00000200
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
- 全局变量和静态变量区(对应数据区):全局变量和静态变量
(static修饰的变量)
存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 程序结束后由系统释放。 - 常量区:常量字符串就是放在这里的。
- 程序代码区:存放函数体的二进制代码
通过编译STM32的工程文件,可以看到如下编译的结果如下:
-
RO
:Read-Only
的缩写,包括RO-data
(只读数据)和RO-code
(代码)。 -
RW
:Read-Write
的缩写,主要是RW-data
,Rw-data
由程序初始化初始值。 -
ZI
:Zero-initialized
的缩写,主要是ZI-data
,由编程器初始化为0。注:在keil中,栈区被默认是ZI段的子集。
STM32的内存分配从0X20000000开始,分为两种情况:
使用 malloc | 未使用 malloc |
---|---|
静态存储区+堆区+栈区 | 静态存储区+栈区 |
一般对于我们开发板例程,实际上,没有所谓堆区的概念,而仅仅是:静态存储区+栈区。
无论哪种情况,所有的全局变量,包括静态变量之类的,全部存储在静态存储区。
紧跟静态存储区之后的,是堆区(如没用到malloc,则没有该区),之后是栈区。
2.内存管理概述
2.1 必要性
在创建任务时,需要申请内存来存放任务控制块和任务堆栈,同样地,创建队列时也需要内存来存放队列结构体和队列存储区。在开发应用中,肯定要创建和删除一些任务或队列等,那么它们所需内存的申请和释放必须有一定的方法来管理。
在介绍内存管理方法之前,先来看看FreeRTOS提供
的可用内存,在FreeRTOS
的内存管理方法 heap_x.c
中定义了一个uint8_t
全局静态数组 ,任务/队列所需要的内存均从这个数组中申请获取,即从静态存储区申请内存。
#if ( configAPPLICATION_ALLOCATED_HEAP == 1 ) // FreeRTOSConfig.h中未定义该宏,为 0
extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; // 用户自定义
#else
PRIVILEGED_DATA static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; //编译器决定,默认采用这种方法
#endif
configTOTAL_HEAP_SIZE
宏在FreeRTOSConfig.h
头文件中定义
#define configTOTAL_HEAP_SIZE ((size_t)(15 * 1024))
- 任务控制块在全局变量内申请,因此内核的不同函数均可对任务进行管理
- 申请内存时使用
pvPortMalloc()
来申请,释放内存时,使用vPortFree()
来释放内存
2.2 内存碎片
内存碎片是内存管理不可忽略的一个问题,内存碎片产生的原因在于不同内存大小的多次申请和释放。如下图,释放的小内存10B
,在申请大内存20B
时用不上,或者申请 8B
,剩余2B
,其他申请无法使用,多次申请和释放后产生很多内存碎片。
2.3 字节对齐
在FreeRTOS中分配内存时,都必须进行字节对齐,主要目的是提升效率。
比如说,一个
uint32_t
的变量,如果没有字节对齐,32
位MCU
可能需要读取两次才能获取这个数据。
在内存分配时会存在两个地方进行字节对齐,分别是内存堆字节对齐和申请内存字节对齐。
(1)内存堆字节对齐
以 heap_4.c
为例,在第一次调用内存申请函数时会初始化内存堆prvHeapInit()
uxAddress = ( size_t ) ucHeap;
if( ( uxAddress & portBYTE_ALIGNMENT_MASK ) != 0 ) // 进行堆内存字节对齐
{
uxAddress += ( portBYTE_ALIGNMENT - 1 );
uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
xTotalHeapSize -= uxAddress - ( size_t ) ucHeap; // 字节对齐后,少了几个字节,重新计算可用大小
}
pucAlignedHeap = ( uint8_t * ) uxAddress; // 字节对齐后的可用起始地址
(2)申请内存字节对齐
申请内存的大小最好是 8
的倍数,不然也会自动触发字节对齐,比如创建任务申请堆栈为 125
个字节,字节对齐后申请的字节为 128
个。
3.内存管理方法
FreeRTOS
提供了五种内存分配方法,经常使用的是 heap_4
,heap_4
是在 heap_2
上扩展得来的,因此着重介绍下这两种内存分配方法。
heap_1:只能申请,不能释放
heap_3:将标准的malloc()和free()进行了简单的封装
3.1 heap_2 方法
heap_2
方法中引入了内存块和链表结构,内存块前面有一个BlockLink_t
类型的变量来描述内存块,占用8
个字节,即如果申请16
个字节,实际申请了24
个字节。这些内存块由一个链表管理,这个链表称为空闲内存块列表。
内存申请过程如下,值得注意的是内存块是按照内存从小到大的顺序排序在空闲内存块链表中。
内存释放过程比较简单,主要是将需要释放的内存所在的内存块添加到空闲内存块链表中。
3.2 heap_4 方法
heap_2
方法在内存释放时只是将内存块插入到链表中,并未实现相邻内存块的合并,而这一点在 heap_4
方式中实现了,即通过合并算法将相邻的内存块合并成一个大内存块。