前言
在进行Free RTOS移植时,我们难免会看到heap1.c,heap2.c,heap3.c,heap4.c,heap5.c这五个文件,在跟着移植教程时进行操作时,教程只说选择heap4.c即可,好一点的会说一说这五个文件的大概不同,跟着教程确实可以成功移植Free RTOS,但是好像有一种听君一席话,如听一席话,正好最近也在学习Free RTOS,所以在这里总结一下5种heap文件的不同。
一、四种内存分配方案对比
1、heap_1
实现了最基本的内存分配pvPortMalloc(),没有实现内存释放pvPortFree()
所以heap_1更适合在只创建任务/内核对象,但不会删除任务/内核对象的系统中使用。
由于动态内存分配的不确定性和内存碎片化,导致在一些商业关键系统和安全关键系统等系统中禁止使用动态内存分配。heap_1分配内存总是确当的,不会让内存碎片化,所以heap_1可以应用在一些关键系统中。
每个任务在创建时都需要使用heap分配TCB(task control block)和栈(stack)两个内核对象。
下图是在heap_1在每次创建任务时RAM的分配情况
A:为Free RTOS在创建任务前,可使用堆(heap)的总量
B:展示了在创建一个任务后堆(heap)的使用情况
C:展示了在创建三个任务后堆(heap)的使用情况
注意heap_1只实现了内存分配,并没有实现内存释放,所以堆的 Free space 使用了就不能恢复了。除非重新上电。
heap1.c实现代码
heap1.c首先定义一块大数组,作为堆,使用pvPortMalloc()函数时,在这块大数组中分配空间。
#define configTOTAL_HEAP_SIZE ((size_t)40*1024)
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
为什么要进行内存对齐?
主要可以从 兼容性 和 性能 进行分析,如果不进行内存对齐,可能会:
程序运行的慢
你的程序会死锁(lock up)
你的操作系统会崩溃
你的程序会悄无声息的失败,并且产生错误结果
void * pvPortMalloc( size_t xWantedSize )
{
void * pvReturn = NULL;
static uint8_t * pucAlignedHeap = NULL;
/* Ensure that blocks are always aligned. */
#if ( portBYTE_ALIGNMENT != 1 )
{
if( xWantedSize & portBYTE_ALIGNMENT_MASK )
{
/* Byte alignment required. Check for overflow. */
if ( (xWantedSize + ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) )) > xWantedSize )
{
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
}
else
{
xWantedSize = 0;
}
}
}
#endif
/* malloc属于临界资源,关闭任务调度,防止其他任务使用malloc函数 */
vTaskSuspendAll();
{
if( pucAlignedHeap == NULL )
{
/* Ensure the heap starts on a correctly aligned boundary. */
pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) & ucHeap[ portBYTE_ALIGNMENT ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
}
/* Check there is enough room left for the allocation and. */
if( ( xWantedSize > 0 ) && /* valid size */
( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE ) &&
( ( xNextFreeByte + xWantedSize ) > xNextFreeByte ) ) /* Check for overflow. */
{
/* Return the next free byte then increment the index past this
* block. */
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;
}
2、heap_2
Heap_2之所以还保留,只是为了兼容以前的代码。新设计中不再推荐使用Heap_2。建议使用Heap_4来替代Heap_2,更加高效。
Heap_2也是在数组上分配内存,与Heap_1不同之处在于:
- 使用了最佳匹配算法(best fit algorithm)进行内存分配
- 允许内存释放vPortFree()
- 可以查看剩余空间xPortGetFreeHeapSize()
最佳匹配算法使用大小与请求字节数最接近的可用内存块。
- 例如heap有3块空闲内存块:5 bytes,25 bytes,100 bytes
- pvPortMalloc() 需要申请20bytes内存
- 能满足20bytes的最小的RAM空闲内存为25bytes的内存块
- 将25bytes的内存块拆分为20bytes和5bytes
- 返回指向20bytes内存块的指针
- 剩余5bytes保留,供后续pvPortMalloc使用
下图为heap_2下的内存分配示意图:
A:表示已经创建了三个任务后堆(heap)的使用情况,栈顶仍然保留着一块大内存
B:表示在删除一个任务后,栈顶的空闲内存仍然保留,已删除任务占用的空间被释放。
C:在B的基础上重新创建了一个新任务,因为TCB、栈大小跟前面被删除任务的TCB、栈大小一致,所以刚好分配
到原来的内存
heap_2是分配内存不是确定性的,所以heap_2要比标准的malloc() 和 free()执行速度要快。
3、heap_3
- heap_3使用了标准的malloc() 和 free()函数,所以堆大小由链接器的配置决定,配置项configTOTAL_HEAP_SIZE不再起作用
- C库里的malloc、free函数并非线程安全的,Heap_3中先暂停FreeRTOS的调度器,再去调用这些函数,使用这种方法实现了线程安全
4、heap_4
跟Heap_1、Heap_2一样,Heap_4也是通过大数组来分配内存,heap_4使用了首次适应算法来分配内存(first fit algorithm)与heap_2不同的是heap_4能够将相邻的空闲内存合并为一个更大的空闲内存,这有助于减少内存的碎片问题。
首次适应算法:
- 堆中包含三块空闲内存5 bytes, 200 bytes和100 bytes
- pvPortMalloc()需要 20 bytes 的RAM空间.
- 找出足够大的内存空间200 bytes
- pvPortMalloc() 将200 bytes空间拆分为20 bytes和180 bytes
- 返回指向20 bytes空间的指针
- 剩余180 bytes空间保留,供后续pvPortMalloc()使用
下图是heap4的内存分配示意图
A:展示创建了三个任务堆的使用情况
B:展示删除一个任务后堆的使用情况,与heap_2不同的是,heap_4将被删除任务释放的TCB和栈空间合成为了一个大空间
C:展示Free RTOS创建了一个队列,xQueueCreate() 调用pvPortMalloc()为队列分配了一块内存
D:用户直接使用pvPortMalloc()来分配一块内存,用户分配的块足够小,可以容纳第一个空闲块,即分配给队列的内存和分配给后续TCB的内存之间的块。
E:删除Queue,系统自动释放分配Queue的内存,在用户内存前后有两块空闲内存
F:在释放用户申请的内存后,用户占用的内存和用户前的内存、用户后的内存会合成为一个大的空闲内存。
heap_4是分配内存不是确定性的,所以heap_2要比标准的malloc() 和 free()执行速度要快。
5、heap_5
Heap_5使用的分配/释放内存的算法跟Heap_4使用的算法是一样的。与heap_4不同的是。Heap_5并不局限于在一个大数组中进行内存分配heap_5能够在多个独立空间中进行内存分配。
在嵌入式系统中,内存的地址可能并不连续,这种场景下可以使用Heap_5。
既然内存时分隔开的,那么就需要进行初始化:确定这些内存块在哪、多大:
在使用pvPortMalloc之前,必须先指定内存块的信息使用vPortDefineHeapRegions来指定这些信息
/* 指定一块内存 */
typedef struct HeapRegion
{
uint8_t * pucStartAddress; // 起始地址
size_t xSizeInBytes; // 大小
} HeapRegion_t
/* 使用数组指定多块内存 */
HeapRegion_t xHeapRegions[] =
{
{ ( uint8_t * ) 0x80000000UL, 0x10000 }, // 起始地址0x80000000,大小0x10000
{ ( uint8_t * ) 0x90000000UL, 0xa0000 }, // 起始地址0x90000000,大小0xa0000
{ NULL, 0 } // 表示数组结束
};
/*vPortDefineHeapRegions函数原型如下 用来初始化heap_5*/
/* 在创建任务、队列、信号等时,需要先调用vPortDefineHeapRegions */
void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions );
总结
以上总结了在移植Free RTOS时,遇到的五个关于Free RTOS内存管理文件。由于除heap_1外其余代码比较多,详细的信息请参考官方源码和官方收据手册。