FreeRTOS之内存管理详解

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的工程文件,可以看到如下编译的结果如下:
FreeRTOS之内存管理详解_第1张图片
查看相应的.map文件FreeRTOS之内存管理详解_第2张图片
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结束时,这些寄存器又会被恢复到寄存器中。
FreeRTOS之内存管理详解_第3张图片
若在含有嵌入式操作系统的应用中,一般会将应用程序和内核所使用的栈空间进行分离。因此,PSP寄存器就将会被用到,而且在异常入口和异常退出时,会产生SP切换。如下图所示。在自动“压栈”和“出栈”阶段使用的是PSP,而在中断处理程序中使用的是MSP寄存器。这样设计的好处是简化了OS设计的复杂度,同时提高了上下文的切换速度,还有一个好处是将任务栈和内核栈分离可以避免任务栈出现错误而影响内核栈的运行。
FreeRTOS之内存管理详解_第4张图片
需要说明的是:同一时间内只有一个SP寄存器是可见的。我们在应用程序代码中无需显示访问MSP和PSP。一般在嵌入式操作系统中使用汇编代码访问MSP和PSP,例如:通过MMRS指令读取PSP的数值,OS可以从应用任务API调用的栈中读出压入的数据,而且OS的上下文切换代码会在上下文切换期间更新PSP的数值。
上电之后,处理器硬件在读取向量表之后会自动初始化MSP,PSP不会被自动初始化, 需要在使用前由软件进行初始化。
经过上面的说明,对于含有操作系统的应用来说,异常处理(包括部分OS内核)使用MSP,而应用任务使用PSP。每个应用任务都有自己的栈空间,如图下所示,OS每次进行上下文切换都会更新PSP。
FreeRTOS之内存管理详解_第5张图片
这种设计的优点如下:
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的异常返回,当加载栈帧时,应用任务就会启动。
FreeRTOS之内存管理详解_第6张图片
在OS设计中,需要在不同任务间切换,这一般被称作上下文切换,其通常在PendSV异常处理中执行,该异常是由SysTick异常触发。在上下文切换中的操作如下:
a、将当前寄存器的状态保存到当前栈中;
b、保存当前的PSP数值;
c、将PSP数值设置为下一个将要执行任务的上一次SP值;
d、恢复下一个任务的上一次各寄存器数值;
e、利用异常返回切换任务。
如下图所示是一个简单的上下文切换,上下文切换是在PendSV中进行的,其优先级会被设置为最低,这样可以避免在其他中断处理过程中产生上下文切换。
FreeRTOS之内存管理详解_第7张图片

总结

只有在含有操作系统的软件中分系统栈和任务栈。系统栈提供给操作系统内核和中断服务程序进行申请自动变量、函数参数传递以及保护现场。基于上述原因,在中断服务程序中可能产生中断嵌套,因此对于系统栈的大小是不能够准确估计的。当产生中断嵌套时,需要系统栈的空间会变大;没有发生中断嵌套时,需要的系统栈的空间会变小。
任务栈是提供给用户任务进行申请自动变量、函数参数传递、保护现场。每一个任务的任务栈空间可以通过粗略计算出来。这方面内容,后续我在专门写一篇博客。
这里要着重强调一点:系统栈和任务栈具体申请在内存的堆区还是栈区,要取决于操作系统的内存管理方式。

三、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;//返回分配后的地址
}

内存堆大小与地址示意图
FreeRTOS之内存管理详解_第8张图片
进行一次内存分配之后的内存分布与地址关系
FreeRTOS之内存管理详解_第9张图片
内存释放函数,对于这种方式,不涉及内存释放。

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 management pages of
	http://www.FreeRTOS.org for more information. */
	( void ) pv;

	/* Force an assert as it is invalid to call this function. */
	configASSERT( pv == NULL );
}

对内存进行初始化,可以认为将整个内存设置为可用

void vPortInitialiseBlocks( void )
{
	/* 就是将全局变量xNextFreeByte设置为0,该变量始终用来表示ucHeap数组可用处的下标 */
	xNextFreeByte = ( size_t ) 0;
}

返回内存堆剩余字节函数: xPortGetFreeHeapSize函数

size_t xPortGetFreeHeapSize( void )
{
	return ( configADJUSTED_HEAP_SIZE - xNextFreeByte );
}

2、heap_2方式

第二种方法采用一种最佳匹配算法,并且可以回收已分配的内存空间。但是,该算法没有两个地址连续的空闲空间进行合并为一个空间,因此这样容易造成内存碎片。
与前面的heap_1的内存管理区域是一样的,都是对ucHeap这样一个数组进行管理。代码如下:

/* 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 */

上面代码已经分析过,不在赘述。
用于跟踪空闲内存块的链表结构如下:

/* Define the linked list structure.  This is used to link free blocks in order
of their size. */
typedef struct A_BLOCK_LINK
{
	struct A_BLOCK_LINK *pxNextFreeBlock;	/*指向下一个空闲内存的地址 */
	size_t xBlockSize;						/* 当前空闲块的大小 */
} BlockLink_t;

heapSTRUCT_SIZE 表示结构体类型BlockLink_t根据系统规定的字节对齐时,占用的字节数。举个例子:根据上面代码定义结构体BlockLink_t类型的变量是8字节的,但是如果在系统内存中使用16字节对齐,那么heapSTRUCT_SIZE将是16字节,会在后面填充8字节的空闲内存,一同分配给BlockLink_t类型的变量。

static const uint16_t heapSTRUCT_SIZE	= ( ( sizeof ( BlockLink_t ) + ( portBYTE_ALIGNMENT - 1 ) ) & ~portBYTE_ALIGNMENT_MASK );

定义最小内存块的大小。

#define heapMINIMUM_BLOCK_SIZE	( ( size_t ) ( heapSTRUCT_SIZE * 2 ) )

定义两个链表项,分别表示开头和结尾。

/* Create a couple of list links to mark the start and end of the list. */
static BlockLink_t xStart, xEnd;

xFreeBytesRemaining 表示剩余空闲字节数。

/* Keeps track of the number of free bytes remaining, but says nothing about
fragmentation. */
static size_t xFreeBytesRemaining = configADJUSTED_HEAP_SIZE;

内存申请函数pvPortMalloc():
对于该函数,系统会维护一个空闲内存链表,在该链表中是按照链表项内存大小进行排序的。当我们根据内存申请大小,遍历该链表,找的第一个大于申请内存的链表项,如果该链表项,分配给用户后,剩余的内存大于heapMINIMUM_BLOCK_SIZE,那么就将剩余的内存块填充为新的空闲链表项加入到空闲链表。

void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
static BaseType_t xHeapHasBeenInitialised = pdFALSE;//一个静态局部变量,用来标记内存是否被初始化
void *pvReturn = NULL;

	vTaskSuspendAll();
	{
		/* 检查内存堆ucHeap是否被初始化 */
		if( xHeapHasBeenInitialised == pdFALSE )
		{
			prvHeapInit();//初始化内存堆ucHeap,文章下面会将该函数
			xHeapHasBeenInitialised = pdTRUE;//标记内存堆ucHeap已经被初始化
		}

		/* 计算实际申请的内存空间的大小,函数参数传过来的是申请内存的大小,这里必须加入头部占用的空间,以及为了满足系统要求的字节对齐而额外分配的空间。*/
		//即:总空间=xWantedSize +  heapSTRUCT_SIZE + 字节对齐额外空间*/
		if( xWantedSize > 0 )
		{
			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 ) )//内存的大小是按照链表项中的xBlockSize中的由小到大排序的
			{
				pxPreviousBlock = pxBlock;
				pxBlock = pxBlock->pxNextFreeBlock;
			}

			/* 是否找到满足要求的内存块 */
			if( pxBlock != &xEnd )
			{
				/* 返回pvReturn指向实际可用的内存地址,这里出去链表项的地址域 */
				pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + heapSTRUCT_SIZE );

				/* 从空闲链表中删除该链表,因为已经被使用 */
				pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;

				/* 查找到的空闲块大于申请的空闲块,将会把空闲块进行拆分为两个,一个分给用户,另一个还是作为空闲块,并填充为空闲块链表项,加入到此系统维护的空闲链表中 */
				if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
				{
					/* pxNewBlockLink指向分配给用户之后剩余内存空间的地址 */
					pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );

					/* Calculate the sizes of two blocks split from the single
					block. */
					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;
}

内存释放函数void vPortFree( void *pv )
释放内存的过程就是通过之前分配的内存地址,找到真个内存块的地址,将该内存块加入到系统维护的内存空闲链表中,该函数只是将内存块按大小加入到 空闲内存链表中,不会将相邻的空闲链表进行合并。

void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;

	if( pv != NULL )
	{
		/* 查找到内存链表项的地址域 */
		puc -= heapSTRUCT_SIZE;

		/* This unexpected casting is to keep some compilers from issuing
		byte alignment warnings. */
		pxLink = ( void * ) puc;

		vTaskSuspendAll();
		{
			/* Add this block to the list of free blocks. */
			prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );//将释放的内存块加入到空闲链表中
			xFreeBytesRemaining += pxLink->xBlockSize;//增加空闲内存数
			traceFREE( pv, pxLink->xBlockSize );//调试用
		}
		( void ) xTaskResumeAll();
	}
}

得到空闲内存的大小函数 size_t xPortGetFreeHeapSize( void )。

size_t xPortGetFreeHeapSize( void )
{
	return xFreeBytesRemaining;
}

函数 prvInsertBlockIntoFreeList( pxBlockToInsert ),其实是一个宏定义:
将块插入空闲链表中 - 该链表是按照空闲块大小进行有序排列的。

#define prvInsertBlockIntoFreeList( pxBlockToInsert )								\
{																					\
BlockLink_t *pxIterator;															\
size_t xBlockSize;																	\
																					\
	xBlockSize = pxBlockToInsert->xBlockSize;										\
																					\
	/* Iterate through the list until a block is found that has a larger size */	\
	//进行空闲链表的遍历,将内存块放到合适的位置									\
	for( pxIterator = &xStart; pxIterator->pxNextFreeBlock->xBlockSize < xBlockSize; pxIterator = pxIterator->pxNextFreeBlock )	\
	{																				\
		/* There is nothing to do here - just iterate to the correct position. */	\
	}																				\
																					\
	/* Update the list to include the block being inserted in the correct */		\
	/* position. */																	\
	pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;					\
	pxIterator->pxNextFreeBlock = pxBlockToInsert;									\
}

内存初始化函数 static void prvHeapInit( void ):

static void prvHeapInit( void )
{
BlockLink_t *pxFirstFreeBlock;
uint8_t *pucAlignedHeap;

	/* 和heap1内存管理一样,为实现字节对齐,必须舍弃头部相应几字节 */
	pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) &ucHeap[ portBYTE_ALIGNMENT ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );

	/* 初始化链表的头部链表项 xStart*/
	xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;//指向第一个内存堆ucHeap中第一个可用的内存单元
	xStart.xBlockSize = ( size_t ) 0;//内存块空间设置为0

	/* 初始化链表的尾部链表项 xEnd*/
	xEnd.xBlockSize = configADJUSTED_HEAP_SIZE;//将空闲空间设置为整个内存堆ucHeap的整个可用空间
	xEnd.pxNextFreeBlock = NULL;//链表的最后一个链表项地址域应该指向NULL

	/* 首先在内存堆中申请第一个空闲块,即整个内存堆可用空间 */
	pxFirstFreeBlock = ( void * ) pucAlignedHeap;
	pxFirstFreeBlock->xBlockSize = configADJUSTED_HEAP_SIZE;//整个空闲块的大小
	pxFirstFreeBlock->pxNextFreeBlock = &xEnd;//指向最后一个空闲块
}

内存初始化之后示意图如下:

FreeRTOS之内存管理详解_第10张图片下图给出内存分配之后的示意图,注: 这里设置系统字节对齐方式为8字节。
FreeRTOS之内存管理详解_第11张图片

3、heap_3方式

该内存管理方式是将标准的malloc()和free()进行了简单的封装。只是在封装的时候加入了调度器挂起和恢复操作。前两种方式内存管理是在系统申请的一个大数组中,该种方式是真正在堆中进行的,因此在STM32的启动文件(即汇编阶段)就要设置堆的大小,必须有如下的代码:

 Heap_Size       EQU     0x00000200

                AREA    HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem        SPACE   Heap_Size
__heap_limit

内存申请函数void *pvPortMalloc( size_t xWantedSize ):

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;
}

内存释放函数void vPortFree( void *pv ):

void vPortFree( void *pv )
{
	if( pv )
	{
		vTaskSuspendAll();
		{
			free( pv );
			traceFREE( pv, 0 );
		}
		( void ) xTaskResumeAll();
	}
}

4、heap_4方式

第四种分配算法与第二种基本相同,只是增加了一个合并算法,将相邻的空闲内存块合并成一个更大的空闲块。其实操作也是在申请的数组ucHeap中进行。代码如下:

/* 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 */

设置分配的内存块的最小值,代码如下:

/* 确保内存块的开头结构必须进行对齐 */
static const size_t xHeapStructSize	= ( sizeof( BlockLink_t ) + ( ( size_t ) ( portBYTE_ALIGNMENT - 1 ) ) ) & ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
/* Block sizes must not get too small. */
#define heapMINIMUM_BLOCK_SIZE	( ( size_t ) ( xHeapStructSize << 1 ) )

定义链表项的结构体,用来连接各链表项,构成链表。

/* 连接空闲链表项 */
typedef struct A_BLOCK_LINK
{
	struct A_BLOCK_LINK *pxNextFreeBlock;	/*<< 指向下一个空闲链表项地址 */
	size_t xBlockSize;						/*<< 当前空闲链表项的内存大小,其中该成员的最高位被用来表示该内存块是否是空闲(即属于应用程序还是内核系统) */
} BlockLink_t;

内存申请函数,pvPortMalloc()。在该函数中,具体实现与heap2基本相同,只是,该函数中对BlockLink_t结构体成员xBlockSize的最高位赋予了新的意义,即用来表示本结构体所表示的内存块是空闲还是已经被分配给了应用程序。当=1时,表示空闲内存块;当=1时,表示已被分配给应用程序。还有,该内存分配方式中,多加了一个变量xMinimumEverFreeBytesRemaining用来记录经过多次内存释放和分配之后,空闲内存历史最小值。具体实现代码如下:

void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
void *pvReturn = NULL;

	vTaskSuspendAll();
	{
		/* 第一次调用内存申请函数,这时将会初始化内存空间 */
		if( pxEnd == NULL )
		{
			prvHeapInit();
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}

		/* 申请的内存大小合法性检查:是否过大.结构体BlockLink_t中有一个成员xBlockSize表示块的大小,
		这个成员的最高位被用来标识这个块是否空闲.因此要申请的块大小不能使用这个位. */
		if( ( xWantedSize & xBlockAllocatedBit ) == 0 )//xBlockAllocatedBit在内存初始化函数中会将其最高位设置为1
		{
			/* 计算实际要分配的内存:实际分配内存 = 应用层需要字节数+BlockLink_t结构体占用字节数+内存对齐多分配字节数 */
			if( xWantedSize > 0 )
			{
				xWantedSize += xHeapStructSize;

				/* Ensure that blocks are always aligned to the required number
				of bytes. */
				if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
				{
					/* Byte alignment required. */
					xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
					configASSERT( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) == 0 );
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}

			if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) )//判断剩余空闲内存是否满足应用层申请的数量
			{
				/* 从低地址开始遍历,找到符合申请内存大小的内存块 */
				pxPreviousBlock = &xStart;
				pxBlock = xStart.pxNextFreeBlock;
				while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
				{
					pxPreviousBlock = pxBlock;
					pxBlock = pxBlock->pxNextFreeBlock;
				}

				/* 如果没有找到符合要求的内存块 */
				if( pxBlock != pxEnd )
				{
					/* 找到符合应用程序申请的内存空间大小的内存,返回分配的地址给应用层,这里要去掉BlockLink_t结构体占用的内存 */
					pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize );

					/* 从空闲链表中取出 */
					pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;

					/* 分配应用层需要的内存之后,剩余的空闲内存还将大于烯烃规定的最小内存块的大小,就将剩下的空闲内存块重新构成一个空闲链表项加入到空闲链表中 */
					if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
					{
						/* This block is to be split into two.  Create a new
						block following the number of bytes requested. The void
						cast is used to prevent byte alignment warnings from the
						compiler. */
						pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );//确定新产生数据块的地址
						configASSERT( ( ( ( size_t ) pxNewBlockLink ) & portBYTE_ALIGNMENT_MASK ) == 0 );

						/* Calculate the sizes of two blocks split from the
						single block. */
						pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;//新产生的数据块的字节数
						pxBlock->xBlockSize = xWantedSize;//分配给应用层的数据块整体字节数赋值

						/* Insert the new block into the list of free blocks. */
						prvInsertBlockIntoFreeList( pxNewBlockLink );//将剩余的新的内存空闲块插入到空闲链表中
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}

					xFreeBytesRemaining -= pxBlock->xBlockSize;//由于用户申请了内存,因此剩余内存要减小
                     /* 保存剩余空闲空间的最小值 */
					if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining )
					{
						xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}

					/* The block is being returned - it is allocated and owned
					by the application and has no "next" block. */
					pxBlock->xBlockSize |= xBlockAllocatedBit;//将分配给应用程序的空间设置为已被分配
					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

	configASSERT( ( ( ( size_t ) pvReturn ) & ( size_t ) portBYTE_ALIGNMENT_MASK ) == 0 );
	return pvReturn;
}

内存初始化函数static void prvHeapInit( void )

static void prvHeapInit( void )
{
BlockLink_t *pxFirstFreeBlock;
uint8_t *pucAlignedHeap;
size_t uxAddress;
size_t xTotalHeapSize = configTOTAL_HEAP_SIZE;

	/* 对齐正确的内存边界 */
	uxAddress = ( size_t ) ucHeap;

	if( ( uxAddress & portBYTE_ALIGNMENT_MASK ) != 0 )//检查内存地址ucHeap是否是系统规定的字节对齐的整数倍
	{//若不是,将会进行调整,舍弃ucHeap数组的前几个字节,达到系统规定的字节对齐
		uxAddress += ( portBYTE_ALIGNMENT - 1 );
		uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
		xTotalHeapSize -= uxAddress - ( size_t ) ucHeap;//得到由于字节对齐去掉几字节之后,可以利用的总字节数
	}

	pucAlignedHeap = ( uint8_t * ) uxAddress;//将得到的可用内存空间起始地址赋值给pucAlignedHeap

	/* xStart 用来指向第一个可以使用的空闲链表项(内存块)的地址 */
	xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
	xStart.xBlockSize = ( size_t ) 0;

	/* pxEnd  标记堆空间ucHeap可用空间的末尾 */
	uxAddress = ( ( size_t ) pucAlignedHeap ) + xTotalHeapSize;
	uxAddress -= xHeapStructSize;
	uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );//为实现字节对齐,需要对ucHeap内存堆中末尾字节进行相应的删除
	pxEnd = ( void * ) uxAddress;
	pxEnd->xBlockSize = 0;
	pxEnd->pxNextFreeBlock = NULL;

	/* pxFirstFreeBlock 表示空闲内存 */
	pxFirstFreeBlock = ( void * ) pucAlignedHeap;
	pxFirstFreeBlock->xBlockSize = uxAddress - ( size_t ) pxFirstFreeBlock;//整个内存堆ucHeap空间中可以内存的大小
	pxFirstFreeBlock->pxNextFreeBlock = pxEnd;

	/* Only one block exists - and it covers the entire usable heap space. */
	xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;//剩余可用空间的大小,这里是第一次初始化,因此是整个内存的大小
	xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;

	/* Work out the position of the top bit in a size_t variable. */
	xBlockAllocatedBit = ( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 );
}

内存释放函数void vPortFree( void *pv )。具体代码实现如下:

void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;

	if( pv != NULL )
	{
		/* 查找到需要释放内存块的首地 */
		puc -= xHeapStructSize;

		/* This casting is to keep the compiler from issuing warnings. */
		pxLink = ( void * ) puc;

		/* 判断该内存块是否是被分配给了应用程序 */
		configASSERT( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 );
		configASSERT( pxLink->pxNextFreeBlock == NULL );

		if( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 )//是否分配给了应用程序
		{
			if( pxLink->pxNextFreeBlock == NULL )
			{
				/* The block is being returned to the heap - it is no longer
				allocated. */
				pxLink->xBlockSize &= ~xBlockAllocatedBit;//标记内存块为空闲内存块

				vTaskSuspendAll();
				{
					/* Add this block to the list of free blocks. */
					xFreeBytesRemaining += pxLink->xBlockSize;//总的空闲内存空间增加
					traceFREE( pv, pxLink->xBlockSize );
					prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );//插入到空闲链表中
				}
				( void ) xTaskResumeAll();
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
}

之前已经说过,heap4与heap2的区别就是前者加入了对相邻空闲块合并的算法,该算法的实现是在空闲链表插入中static void prvInsertBlockIntoFreeList( BlockLink_t *pxBlockToInsert ),具体代码如下:

static void prvInsertBlockIntoFreeList( BlockLink_t *pxBlockToInsert )
{
BlockLink_t *pxIterator;
uint8_t *puc;

	/* Iterate through the list until a block is found that has a higher address
	than the block being inserted. */
	for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock )
	{
		/* Nothing to do here, just iterate to the right position. */
	}

	/* Do the block being inserted, and the block it is being inserted after
	make a contiguous block of memory? */
	puc = ( uint8_t * ) pxIterator;
	if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert )
	{
		pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
		pxBlockToInsert = pxIterator;
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}

	/* Do the block being inserted, and the block it is being inserted before
	make a contiguous block of memory? */
	puc = ( uint8_t * ) pxBlockToInsert;
	if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock )
	{
		if( pxIterator->pxNextFreeBlock != pxEnd )
		{
			/* 在这里执行合并块 */
			pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize;
			pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock;
		}
		else
		{
			pxBlockToInsert->pxNextFreeBlock = pxEnd;
		}
	}
	else
	{
		pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
	}

	/* If the block being inserted plugged a gab, so was merged with the block
	before and the block after, then it's pxNextFreeBlock pointer will have
	already been set, and should not be set here as that would make it point
	to itself. */
	if( pxIterator != pxBlockToInsert )
	{
		pxIterator->pxNextFreeBlock = pxBlockToInsert;
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}
}

5、heap_5方式

heap5允许内存堆可以跨越多个非连续的内存区域,对于上面讲的内存堆中heap1,heap2,heap4都是在一个定义好的大数组里进行的。heap5允许在多个不连续的内存空间,这样就可以在芯片外部多个RAM芯片中进行动态内存管理,例如我们在某个系统应用中外扩的几片单独的RAM芯片,这样我们只需要指定每个芯片的内存地址和内存大小就可以,并将他们存入结构体 HeapRegion_t。并且在需要使用内存是采用函数vPortDefineHeapRegions()进行初始化。

typedef struct HeapRegion
 {
 uint8_t *pucStartAddress; << Start address of a block of memory that will be part of the heap.
size_t xSizeInBytes;	  << Size of the block of memory.
  } HeapRegion_t;

我们举一个例子,假设有两个内存。其中内存块A的起始地址为0x80000000,内存大小为0x10000;内存块B的起始地址为0x90000000,内存大小为0xa0000。那么经过初始化之后,内存堆的示意图如下:
FreeRTOS之内存管理详解_第12张图片内存堆建立好之后,后续的内存分配和释放,与前面的heap4算法时一样的不在单独说明。
关于内存管理方面的内容就介绍到这里,有写的不对的地方欢迎大家指正。

你可能感兴趣的:(FreeRTOS)