FreeRTOS的源代码里有5种堆栈管理系统,分别命名heap1.c,heap2.c到heap5.c 均放在FreeRTOS\Source\portable\MemMang 这个文件夹里面,对于M3内核的单片机而言,其Demo默认采用的是heap2.c这个系统,这里分析一下这个管理系统的源代码。
首先,对于ARM内核,其汇编编程有一套APPCS规则(参考这个博文:APPCS汇编编程规则),其为ARM公司制定的一套编程规则。其中一点提到的是:sp指向最后一个压入的值,数据栈由高地址向低地址生长)类型,即满递减堆栈,并且对堆栈的操作是8字节对齐。因此,在STM32F10x.s这个用汇编编写的启动文件里,在代码块里有ALIGN=3这么一个属性,就是让堆栈是8字节对齐的。在heap2.c的一开头,就有一个宏定义:
/* A few bytes might be lost to byte aligning the heap start address. */
#define configADJUSTED_HEAP_SIZE ( configTOTAL_HEAP_SIZE - portBYTE_ALIGNMENT )
其左边configTOTAL_HEAP_SIZE 是定义在FreeRTOSConfig.h中的,值为17K,也就是DEMO默认给堆管理系统的最大的管理内存大小为17KB,如果你的处理器的内存够大,可以可以设置为更大的。后者portBYTE_ALIGNMENT定义在portmacro.h中,代表的就是字节对齐的数量,也就是8,为什么这里会让总大小减去对齐大小8?这是因为在下面的申请堆的内存时:
/* 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 */
我们不知道申请到的内存的地址大小是否是8的整数,也就是8字节对齐的地址。举一个简单的例子,假如我们要申请的内存为32个字节(必须为8字节对齐),而我们申请到的内存的首地址为0x0200_0003,这样末尾地址就是0x0200_0022。因为要8字节对齐,因此实际我们可使用的内存是从0x0200_0008开始到0x0200_0001F共24个字节,因为从0x0200_0020到0x0200_0022之间不足8个字节了,所以末尾这3个字节也被舍去,再加上开头舍去的5个字节,一共是8个字节被舍去,因此实际得到的内存大小是期望的总数减去对齐大小8。(PS:但如果申请的内存的首地址刚好就是对齐的,那么开头地址的8个字节也会被舍去不被使用)
下面几行定义了内存块的结构体:
/* 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; /*<< The next free block in the list. */
size_t xBlockSize; /*<< The size of the free block. */
} BlockLink_t;
从这个定义可以看出,结构体内仅含有两个成员,一个是该内存块的下一个空闲内存块的地址,另一个是这个内存块的大小。
heap2的内存管理结构如下:
每一个内存的开头都有一个BlockLint结构体,这个结构体的大小在CM3里是8字节,PNB指针是4个字节,BlockSize这个变量也是4个字节,一共8个字节,刚好是8字节对齐的。所以每一个内存块的实际可用内存大小是BlockSize减去heapSTRUCT_SIZE也就是8,因此比如要申请76个字节的内存,那么首先要8字节对齐,需要申请80字节内存,再加上管理块的8字节大小一共需要88字节的内存。
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;
/* Keeps track of the number of free bytes remaining, but says nothing about
fragmentation. */
static size_t xFreeBytesRemaining = configADJUSTED_HEAP_SIZE;
heap2定义了头尾两个空的内存块,分别为xStart和xEnd,两个内存块只有管理部分8字节。xStart的BlockSize是0,不作用处,而NextFreeBlock则指向第一个空的内存的地址,然后一直指向下去,直到最后一个内存块的NextFreeBlock指向了结尾xEnd,而xEnd的BlockSize的值在初始化时定义为整个堆内存大小configADJUSTED_HEAP_SIZE,也不作用处,在上图中,两块灰色部分内存的大小之和总是为8。最后的静态变量xFreeBytesRemaining是可用内存大小,通过这个变量可以知道当前剩余的内存。
size_t xPortGetFreeHeapSize( void )
{
return xFreeBytesRemaining;
}
第一个函数,是用宏定义完成的宏函数prvInsertBlockIntoFreeList:
#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 */ \
/* than the block we are inserting. */ \
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; \
}
这是往空闲内存块里插入一个被释放了内存的内存块操作,插入是按照内存块的大小来进行排序的,因此上图中的箭头并不总是这样从左往右的,有时会在中间过程有翻转的箭头,但不变的是第一个箭头总是由xStart指向出去,最后一个箭头总是指向到xEnd。在这个插入操作里,首先就是通过pxNextFreeBlock指针来遍历空闲内存块,来找到所要插入的内存块的大小按从小到大排列时应该在的地方,然后把所得正确位置的内存块pxIterator的pxNextFreeBlock指针指向要插入的内存块,把要插入的内存块的pxNextFreeBlock指针指向pxIterator的下一个内存块,把内存块插进去并连接好链接就像在list里所做的操作一样。
第二个函数则是对外要用的内存分配函数pvPortMalloc,唯一参数xWantedSize是要申请的内存大小(实际会占用的是8字节对齐后再加上8字节的管理部分)
void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
static BaseType_t xHeapHasBeenInitialised = pdFALSE;
void *pvReturn = NULL;
vTaskSuspendAll();
{
/* If this is the first call to malloc then the heap will require
initialisation to setup the list of free blocks. */
if( xHeapHasBeenInitialised == pdFALSE ) //如果堆栈还未被初始化
{
prvHeapInit(); //那么首先调用prvHeapInit来初始化
xHeapHasBeenInitialised = pdTRUE; //然后定这个变量来表明已初始化
}
/* The wanted size is increased so it can contain a BlockLink_t
structure in addition to the requested amount of bytes. */
if( xWantedSize > 0 )
{
xWantedSize += heapSTRUCT_SIZE; //让要申请的内存大小加上控制块部分的大小8字节
/* Ensure that blocks are always aligned to the required number of bytes. */
if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0 ) //如果这个大小不是8字节对齐的
{
/* Byte alignment required. */
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );//那么增加几个字节让大小能被8整除
}
}
if( ( xWantedSize > 0 ) && ( xWantedSize < configADJUSTED_HEAP_SIZE ) )
{
/* Blocks are stored in byte order - traverse the list from the start
(smallest) block until one of adequate size is found. */
pxPreviousBlock = &xStart;
pxBlock = xStart.pxNextFreeBlock;
while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
{
pxPreviousBlock = pxBlock; //利用前面定义的两个临时变量,通过while循环
pxBlock = pxBlock->pxNextFreeBlock; //来遍历查找在空闲内存块里,内存大小比要申请的大小要大的内存块
} //如果找不到,最后停止在pxBlock=xEnd处,因为xEnd->pxNextFreeBlock==NULL
/* If we found the end marker then a block of adequate size was not found. */
if( pxBlock != &xEnd )
{
/* Return the memory space - jumping over the BlockLink_t structure
at its start. */ //在判断查询得到的内存块不是xEnd后,赋值给要返回的pvReturn为申请到的地址(注意这个地址是在块内的管理部分之后的)
pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + heapSTRUCT_SIZE );
/* This block is being returned for use so must be taken out of the
list of free blocks. */
pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock; //在空闲内存块链表里取出这个内存块后,把链接再接起来
/* If the block is larger than required it can be split into two. */
if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE ) //如果申请到的内存块大小比需求的大小大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 ); //新内存块的地址
/* 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 ) ); //把这个新内存块加入到空闲内存块链表里
}
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;
}
在这个函数里,有一个分割内存块的操作,这个操作的作用很重要,不仅仅是节约内存的作用,更主要的是对内存块进行切割得到其余内存块。因为在heapInit里,是没有预先分割内存块的,只有一个大的内存块,大小为configADJUSTED_HEAP_SIZE,见prvHeapInit函数:
static void prvHeapInit( void )
{
BlockLink_t *pxFirstFreeBlock;
uint8_t *pucAlignedHeap;
/* Ensure the heap starts on a correctly aligned boundary. */ //为了确保内存地址是8字节对齐的,首先进行位移对齐操作,如果本身是对齐的,那么开头的8字节会被舍去
pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) &ucHeap[ portBYTE_ALIGNMENT ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
/* xStart is used to hold a pointer to the first item in the list of free
blocks. The void cast is used to prevent compiler warnings. */
xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap; //设置xStart的下个指向为对齐后的内存地址,此为首个空闲内存块的地址
xStart.xBlockSize = ( size_t ) 0;
/* xEnd is used to mark the end of the list of free blocks. */
xEnd.xBlockSize = configADJUSTED_HEAP_SIZE;
xEnd.pxNextFreeBlock = NULL; //设置xEnd的下个指向为空,以此来判断这个内存块是否就是xEnd
/* To start with there is a single free block that is sized to take up the
entire heap space. */
pxFirstFreeBlock = ( void * ) pucAlignedHeap; //第一个内存块的地址就是对齐和的内存地址
pxFirstFreeBlock->xBlockSize = configADJUSTED_HEAP_SIZE; //大小就是前面计算好的configADJUSTED_HEAP_SIZE
pxFirstFreeBlock->pxNextFreeBlock = &xEnd; //下个指向就是xEnd,也就是此时整个堆栈内存里只有这一个空闲内存块,占了所有内存
}
所以一开始,整个内存只有一个空闲内存块pxFirstFreeBlock,占了整个内存大小,如果在分配内存时进行切割,那么在下一次分配内存时将无内存块可用,所以切割内存块是必须的操作。每次分配基本都会切割一次,这样内存会被切割成一个个零散的内存块了。
最后就是同样对外使用的内存释放函数vPortFree:
void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;
if( pv != NULL ) //先判定要释放的内存块不是空的
{
/* The memory being freed will have an BlockLink_t structure immediately
before it. */
puc -= heapSTRUCT_SIZE; //上移8个字节(控制部分大小),得到内存块的首地址
/* This unexpected casting is to keep some compilers from issuing
byte alignment warnings. */
pxLink = ( void * ) puc; //赋值给临时变量pxLink得到要释放的内存块地址
vTaskSuspendAll();
{
/* Add this block to the list of free blocks. */
prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) ); //把这个内存块利用prvInsertBlockIntoFreeList宏函数加入到空闲链表中
xFreeBytesRemaining += pxLink->xBlockSize; //重新计算空闲内存大小
traceFREE( pv, pxLink->xBlockSize );
}
( void ) xTaskResumeAll();
}
}
细心的人在看到heap2的管理方式后,应该会想到这样一个问题,那就是当内存块在一次次分配又释放后被切割得很零碎时,如果此时我们突然需要申请一个内存稍大的内存块时,这众多零碎内存块里并有比所需内存要大的内存块的话,就会出现申请不到内存的问题了,但实际上我们还有很多空闲内存,只是被分割的。因此heap2这种管理方式存在弊端,所以我个人更喜欢heap4的内存管理方式。heap4的内存管理方式与heap2的不同点是,heap4里空闲内存块的排列时按内存地址大小来排列的,当释放内存后在插入空闲内存块时时候,会对所释放的内存块判断是否与前面的内存是连续的,如果是就把两个内存块进行合并,然后再判断是否与后面的内存块是否也是连续的,如果也是那么再次合并,这样就可以合并那些连续的零碎的内存块为一个大内存块,不用太过担心会出现空闲内存块很多但是太小而不满住要求的情况了。