heap_1 —— 最简单,,具有确定性,从静态数组中分配内存,不允许释放内存,不会导致内存碎片化,一锤子买卖,不算真正的动态内存分配;
heap_2—— 非确定性,允许释放内存,但不会合并相邻的空闲块,也就是说没有内存碎片优化措施;
heap_3 —— 简单包装了标准 malloc() 和 free(),以保证线程安全,借壳上市,需要连接器设置堆空间分布,且需要编译器库提供malloc和free函数的实现,可能回增加RTOS内核大小;
heap_4 —— 非确定性,使用第一适应算法(first fit,FF),支持合并相邻的空闲块以避免碎片化,允许将堆放置在内存中的特定地址,官方称比大多数标准 C 库 malloc 的实现要快;
heap_5—— 如同 heap_4,但能够跨越多个不相邻内存区域的堆,且使用动态内存分配前,必须调用 vPortDefineHeapRegions() 进行初始化。
vPortFree:判断指针合法性的时候多了两个条件,一个是检查回收的块大小最高位是否为1,为1才是合法的,毕竟是分配出去了嘛。第二个是Next指针是否为空,为空了说明那是pxEnd,那就不能回收了
在实时操作系统(RTOS)的内存管理中, Heap4高效的原因:
1. **内存碎片管理**:
- Heap4 使用了一种先适配算法,当内存块被释放时,它会尝试将相邻的空闲内存块合并成一个更大的块,从而减少内存碎片。这种策略有助于在内存使用过程中保持较大的连续空闲区域,使得在需要分配大内存块时更容易找到足够的空间。heap3不会对碎片合并优化。
2. **线程安全**:
- Heap4 通过暂时挂起 FreeRTOS 调度程序来确保 malloc() 和 free() 是线程安全的,这避免了在多任务环境中由于内存分配和释放操作导致的竞态条件。
- Heap3 虽然也通过挂起调度程序来保证线程安全,但它依赖于标准库的 malloc() 和 free(),这些函数可能没有针对实时系统进行优化。
3. **确定性**:
- Heap4 提供了更好的内存分配确定性,因为它的算法设计考虑了实时系统对确定性的要求。在需要快速响应的系统中,这种确定性是非常重要的。
4. **内存分配策略**:
- Heap4 的内存分配策略是先适配,这意味着它会寻找足够大的内存块来满足请求,这有助于减少内存的浪费。
- Heap3 可能没有特定的内存分配策略。
5. **内存利用率**:
- 由于 Heap4 的内存合并策略,它可以更有效地利用内存,减少因碎片导致的内存浪费。
6. **适用场景**:
- Heap4 适用于需要频繁动态分配和释放内存的复杂场景,而 Heap3 可能更适合内存分配模式较为固定的应用。
综上所述,Heap4 通过减少内存碎片、提供线程安全和确定性,以及优化内存分配策略,通常在实时操作系统中比 Heap3 更高效。
每当创建任务、队列、互斥量、软件定时器、信号量或事件组时,RTOS内核会为它们分配RAM。标准函数库中的malloc()和free()函数有些时候能够用于完成这个任务,但是:
在嵌入式系统中,它们并不总是可以使用的;
它们会占用更多宝贵的代码空间;
它们没有线程保护;
它们不具有确定性(每次调用执行的时间可能会不同);
当RTOS内核需要RAM时,调用**pvPortMalloc()函数来代替malloc()函数。当RAM要被释放时,调用vPortFree()**函数来代替free()函数。
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
xWantedSize 是要申请的字节数
portBYTE_ALIGNMENT是字节对齐数
portBYTE_ALIGNMENT_MASK 是字节对齐掩码
一、heap_4.c
第四种内存分配方法与第二种比较相似,只不过增加了一个合并算法,将相邻的空闲内存块合并成一个大块。与第一种和第二种内存管理策略一样,内存堆仍然是一个大数组,定义为:
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE];
1
1.1 内存申请:pvPortMalloc()
使用一个链表结构来跟踪记录空闲内存块。结构体定义为:
typedef struct A_BLOCK_LINK
{
struct A_BLOCK_LINK *pxNextFreeBlock; /*指向列表中下一个空闲块*/
size_t xBlockSize; /*当前空闲块的大小,包括链表结构大小*/
} BlockLink_t;
第四种内存管理策略和第二种内存管理策略还有一个很大的不同是:第四种内存管理策略的空闲块链表不是以内存块大小为存储顺序,而是以内存块起始地址大小为存储顺序,地址小的在前,地址大的在后。这也是为了适应合并算法而作的改变。
函数中会用到几个局部静态变量在这里简单说明一下:
xFreeBytesRemaining:表示当前未分配的内存堆大小
xMinimumEverFreeBytesRemaining:表示未分配内存堆空间历史最小值。这个值跟xFreeBytesRemaining有很大区别,只有记录未分配内存堆的最小值,才能知道最坏情况下内存堆的使用情况。
xBlockAllocatedBit:这个变量在第一次调用内存申请函数时被初始化,将它能表示的数值的最高位置1。比如对于32位系统,这个变量被初始化为0x80000000(最高位为1)。内存管理策略使用这个变量来标识一个内存块是否空闲。如果内存块被分配出去,则内存块链表结构成员xBlockSize按位或上这个变量(即xBlockSize最高位置1),在释放一个内存块时,会把xBlockSize的最高位清零。
内存申请过程:
首先计算实际要分配的内存大小,判断申请内存合法性,如果合法则从链表头xStart开始查找,如果某个空闲块的xBlockSize字段大小能容得下要申请的内存,则将这块内存取出合适的部分返回给申请者,剩下的内存块组成一个新的空闲块,按照空闲块起始地址大小顺序插入到空闲块链表中,地址小的在前,地址大的在后。
在插入到空闲块链表的过程中,还会执行合并算法:判断这个块是不是可以和上一个空闲块合并成一个大块,如果可以则合并;然后再判断能不能和下一个空闲块合并成一个大块,如果可以则合并!合并算法是第四种内存管理策略和第二种内存管理策略最大的不同!经过几次内存申请和释放后。
源代码解析:
/* 定义一个结构体 */
typedef struct A_BLOCK_LINK
{
#ifdef MTK_SUPPORT_HEAP_DEBUG
uint32_t magic_header;
#endif
struct A_BLOCK_LINK *pxNextFreeBlock; /*<< The next free block in the list. */
size_t xBlockSize; /*<< The size of the free block. */
#ifdef MTK_SUPPORT_HEAP_DEBUG
uint32_t xLinkRegAddr;
#endif /* MTK_SUPPORT_HEAP_DEBUG */
} BlockLink_t;
/* 创建两个结构体变量,分别指向单链表的头和尾部 */
static BlockLink_t xStart, *pxEnd = NULL;
void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
void *pvReturn = NULL;
vTaskSuspendAll(); //将所有的任务挂起
{
if( pxEnd == NULL )
{
prvHeapInit(); //进入堆初始化,根据系统的需要,分配合法合理的堆空间
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 检查申请分配的内存是否具有合法性和检查申请的内存是否过大 */
if( ( xWantedSize & xBlockAllocatedBit ) == 0 )
{
/* 计算实际要分配的内存大小,包含链接结构体BlockLink_t在内,并且要向上字节对齐 */
if( xWantedSize > 0 )
{
xWantedSize += xHeapStructSize;
/* 确保块始终与所需的字节数对齐。 */
if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
{
/* 字节对齐 */
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
configASSERT( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) == 0 );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 首先遍历链表,找到第1块能比申请空间大小大的空闲块,修改空闲块的信息,记录用户可用的内存首地址。 */
if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) )
{
/* 从xStart起始(最低地址)块遍历列表,直到找到一个足够大的空闲块。 */
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 );
#ifdef MTK_SUPPORT_HEAP_DEBUG
pxPreviousBlock->pxNextFreeBlock->xLinkRegAddr = xLinkRegAddr;
configASSERT(pxPreviousBlock->pxNextFreeBlock->magic_header == MAGIC_HEAP_OVERHEADER);
#endif /* MTK_SUPPORT_HEAP_DEBUG */
/* 将已经分配出去的内存块从空闲块链表中删除 */
pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;
/* 如果剩下的内存足够大,则组成一个新的空闲块 */
/* 如果分配出去的空闲块比申请的空间大很多,则将该空闲块进行分割,把剩余的部分重新添加到链表中。*/
if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
{
/* 在剩余内存块的起始位置放置一个链表结构并初始化链表成员*/
pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );
configASSERT( ( ( ( uint32_t ) pxNewBlockLink ) & portBYTE_ALIGNMENT_MASK ) == 0 );
pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
pxBlock->xBlockSize = xWantedSize;
#ifdef MTK_SUPPORT_HEAP_DEBUG
pxNewBlockLink->magic_header = MAGIC_HEAP_OVERHEADER;
#endif
/* 将剩余的空闲块插入到空闲块列表中,按照空闲块的地址大小顺序,地址小的在前,地址大的在后 */
prvInsertBlockIntoFreeList( pxNewBlockLink );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
xFreeBytesRemaining -= pxBlock->xBlockSize;
/* 保存未分配内存堆空间历史最小值 */
if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining )
{
xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 将已经分配的内存块标识为"已分配" */
pxBlock->xBlockSize |= xBlockAllocatedBit;
pxBlock->pxNextFreeBlock = NULL;
xNumberOfSuccessfulAllocations++;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* traceMALLOC( pvReturn, xWantedSize)是一个宏,用于输出内存分配的调试信息,这个宏定义在FreeRTOS.h中,默认为空,如果需要将这些调试信息输出到串口...*/
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( ( ( ( uint32_t ) pvReturn ) & portBYTE_ALIGNMENT_MASK ) == 0 );
return pvReturn;
}
下面是链表的初始化
heap_2.c中链表的尾部数据并未保存在链表内,是以变量的形式存在的。heap_4.c中的链表尾部数据结构保存在链表空间尾部。
//关于堆栈的初始化
//第一步:起始地址做字节对齐,保存pucAlignedHeap 可用空间大小为xTotalHeapSize
//第二步:计算首尾 ,这里需要注意的是链表的尾部指针是保存到该地址尾部的
//第三部:完成链表的初始化,记录内存块信息
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 )
{
uxAddress += ( portBYTE_ALIGNMENT - 1 );
uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
xTotalHeapSize -= uxAddress - ( size_t ) ucHeap; //减去对齐舍弃的字节
}
pucAlignedHeap = ( uint8_t * ) uxAddress; //对齐后可以用的起始地址
//xStart链表的头
xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
xStart.xBlockSize = ( size_t ) 0;
//pxEnd链表的尾
uxAddress = ( ( size_t ) pucAlignedHeap ) + xTotalHeapSize;
uxAddress -= xHeapStructSize;
uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
pxEnd = ( void * ) uxAddress;
pxEnd->xBlockSize = 0;
pxEnd->pxNextFreeBlock = NULL;
//开始时候将内存堆整个看作为一个空闲内存块
pxFirstFreeBlock = ( void * ) pucAlignedHeap;
pxFirstFreeBlock->xBlockSize = uxAddress - ( size_t ) pxFirstFreeBlock;
pxFirstFreeBlock->pxNextFreeBlock = pxEnd;
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 );//用来标记内存块是否被使用
}
空闲块链表的插入:会判断前后的空闲块能否合并,解决内存碎片化的问题。
static void prvInsertBlockIntoFreeList( BlockLink_t *pxBlockToInsert )
{
BlockLink_t *pxIterator;
uint8_t *puc;
//遍历空闲内存块链表,找出内存块插入点。内存块是按照地址从低到高连接在一起的
for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock )
{
/* Nothing to do here, just iterate to the right position. */
}
//插入内存块,检查和前面的内存是否可以合并,如果内存可以合并则合并
puc = ( uint8_t * ) pxIterator;
if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert )
{
pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
pxBlockToInsert = pxIterator;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
//检查是否可以与后面的内存合并
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( pxIterator != pxBlockToInsert )
{
pxIterator->pxNextFreeBlock = pxBlockToInsert;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
在pvPortMalloc()中,若分配出去的空闲块比申请的内存大太多,则需要将内存进行分割,并把分割出的部分重新添加至链表中。
在heap_4.c中的重点为:
将分割出来的空闲块重新添加到链表中的过程,即使用prvInsertBlockIntoFreeList()将其空闲块添加至原本的链表中,则不会产生内存碎片。
static void prvInsertBlockIntoFreeList( BlockLink_t *pxBlockToInsert )
{
BlockLink_t *pxIterator;
uint8_t *puc;
/* 遍历找到分割出来的内存块的下一个内存块,将pxIterator指向它 */
for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock )
{
/* Nothing to do here, just iterate to the right position. */
}
puc = ( uint8_t * ) pxIterator;
/* 可以合并的标准为pxIterator的首地址加上pxIterator的块大小之后等于pxBlockToInsert的首地址。相等就说明两个块是相邻的。 */
if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert )
{
pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
pxBlockToInsert = pxIterator;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
puc = ( uint8_t * ) pxBlockToInsert;
/* FreeRTOS再试着将pxBlockToInsert和pxIterator指向的下一个空闲块进行合并。可合并的标准和刚刚说的一样,只是这次用pxBlockToInsert的首地址加上pxBlockToInsert的块大小与pxIterator指向的下一个块地址比较。*/
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;//若是没有合并,则需要修改链表的next指针
}
}
else
{
pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
}
if( pxIterator != pxBlockToInsert )
{
pxIterator->pxNextFreeBlock = pxBlockToInsert;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
1.2 内存释放vPortFree()
判断指针合法性的时候多了两个条件,一个是检查回收的块大小最高位是否为1,为1才是合法的,毕竟是分配出去了嘛。第二个是Next指针是否为空,为空了说明那是pxEnd,那就不能回收了。在这两个判断之前也有这两个条件的断言configASSERT(),定义在FreeRTOS.h里,同样也是定义为空,可能是留给用户另外用的吧。
源代码解析:
/* 放置在每个分配的内存块开头的结构的大小必须正确对齐字节。*/
static const size_t xHeapStructSize = ( ( sizeof( BlockLink_t ) + ( portBYTE_ALIGNMENT - 1 ) ) & ~portBYTE_ALIGNMENT_MASK );
void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;
if( pv != NULL )
{
/* 被释放的内存在它之前会有一个BlockLink_t结构,向前偏移,重新找回BlockLink_t */
/* 根据参数地址找出内存块链表结构 */
puc -= xHeapStructSize;
/* 这种类型转换是为了防止编译器发出警告 */
pxLink = ( void * ) puc;
/* 检查块是否被实际分配。 */
configASSERT( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 );
configASSERT( pxLink->pxNextFreeBlock == NULL );
#ifdef MTK_SUPPORT_HEAP_DEBUG
configASSERT(pxLink->magic_header == MAGIC_HEAP_OVERHEADER);
#endif
/* 。*/
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;
#ifdef MTK_SUPPORT_HEAP_DEBUG
configASSERT(((BlockLink_t *)((uint8_t *)puc + pxLink->xBlockSize))->magic_header == MAGIC_HEAP_OVERHEADER);
#endif
vTaskSuspendAll();//通过挂起调度器来创建临界区,挂起调度器有些时候也被称为锁定调度器
{
/* Add this block to the list of free blocks. */
xFreeBytesRemaining += pxLink->xBlockSize;
traceFREE( pv, pxLink->xBlockSize );
prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
xNumberOfSuccessfulFrees++;
}
( void ) xTaskResumeAll();//挂起,直到调度器被唤醒后才会得到执行
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}