内容基本上是原子的开发手册,一边学习一边记录。
标准 C 库中的 malloc()和 free()也可以实现动态内存管理,但是如下原因限制了其使用:
● 在小型的嵌入式系统中效率不高。
● 会占用很多的代码空间。
● 它们不是线程安全的。
● 具有不确定性,每次执行的时间不同。
● 会导致内存碎片。内存碎片:经过很多次的申请和释放以后,内存块被不断的分割、最终导致大量很小的内存块
● 使链接器的配置变得复杂。
FreeRTOS 提供了 5 种内存分配方法, FreeRTOS 使用者可以其中的某一个方法,或者自己的内存分配方法。这 5 种方法是 5 个文件,分别为:heap_1.c、 heap_2.c、 heap_3.c、 heap_4.c 和 heap_5.c。
heap_1
1、适用于那些一旦创建好任务、信号量和队列就再也不会删除
的应用,实际上大多数的 FreeRTOS 应用都是这样的。
2、具有可确定性(执行所花费的时间大多数都是一样的),而且不会导致内存碎片
。
3、代码实现和内存分配过程都非常简单,内存是从一个静态数组中分配到的,也就是适合于那些不需要动态内存分配的应用。
内存申请函数
void *pvPortMalloc( size_t xWantedSize )
{
void *pvReturn = NULL;
static uint8_t *pucAlignedHeap = NULL;
/* 是否需要进行字节对齐,默认为8 */
#if( portBYTE_ALIGNMENT != 1 ){
/* 通过与运算来判断是否为8字节对齐,若为0则表示是8字节对齐的 */
if( xWantedSize & portBYTE_ALIGNMENT_MASK ){
/* xWantedSize 取最近的8的倍数 */
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK));
}
}
#endif
vTaskSuspendAll(); /* 调用 vTaskSuspendAll()挂起任务调度器,因为申请内存过程中要做保护,不能被其他任务打断 */
{
if( pucAlignedHeap == NULL ){
/* 确保内存堆的可用起始地址也是 8 字节对齐的,内存堆ucHeap的起始地址是由编译器分配的,ucHeap的起始地址不一定是 8 字节对齐的 */
pucAlignedHeap = (uint8_t *) (((portPOINTER_SIZE_TYPE) & ucHeap[portBYTE_ALIGNMENT])
& (~((portPOINTER_SIZE_TYPE )portBYTE_ALIGNMENT_MASK)));
}
/* 检查一下可用内存是否够分配,分配完成以后是否会产生越界(超出内存堆范围) */
if( ( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE ) &&
( ( xNextFreeByte + xWantedSize ) > xNextFreeByte ) ) {
/* 如果内存够分配并且不会产生越界,那么就将申请到的内存首地址赋给 pvReturn */
pvReturn = pucAlignedHeap + xNextFreeByte;
xNextFreeByte += xWantedSize; /* 内存申请完成以后更新一下变量 xNextFreeByte */
}
traceMALLOC(pvReturn, xWantedSize);
}
(void) xTaskResumeAll(); /* 恢复任务调度器 */
#if(configUSE_MALLOC_FAILED_HOOK == 1) {
if( pvReturn == NULL ) {
extern void vApplicationMallocFailedHook(void);
vApplicationMallocFailedHook();
}
}
#endif
/* 返回 pvRerurn 值,如果内存申请成功的话就是申请到的内存首地址,内存申请失败的话就返回 NULL */
return pvReturn;
}
总结一下:
首先是判断申请的内存是否需要8字节的对齐。申请时挂起任务调度器,首先确保内存堆的可用起始地址也是 8 字节对齐的,接着检查一下可用内存是否够分配,分配完成以后是否会产生越界,如果内存够分配并且不会产生越界,那么就将申请到的内存首地址赋给 pvReturn,更新一下变量 xNextFreeByte,恢复任务调度器。
内存释放函数
void vPortFree( void *pv )
{
( void ) pv;
configASSERT( pv == NULL );
}
可以看出 vPortFree()并没有具体释放内存的过程。因此如果使用 heap_1,一旦申请内存成功就不允许释放!
heap_2
1、可以使用在那些可能会重复的删除任务、队列、信号量等的应用中,要注意有内存碎片产生!
2、如果分配和释放的内存 n 大小是随机的,那么就要慎重使用了,比如下面的示例:
● 如果一个应用动态的创建和删除任务,而且任务需要分配的堆栈大小都是一样的,那么 heap_2 就非常合适。如果任务所需的堆栈大小每次都是不同,那么 heap_2 就不适合了,因为这样会导致内存碎片产生,最终导致任务分配不到合适的堆栈!不过 heap_4 就很适合这种场景了。
● 如果一个应用中所使用的队列存储区域每次都不同,那么 heap_2 就不适合了,和上面一样,此时可以使用 heap_4。
● 应用需要调用 pvPortMalloc() 和 vPortFree() 来申请和释放内存,而不是通过其他FreeRTOS 的其他 API 函数来间接的调用,这种情况下 heap_2 不适合。
3、如果应用中的任务、队列、信号量和互斥信号量具有不可预料性(如所需的内存大小不能确定,每次所需的内存都不相同,或者说大多数情况下所需的内存都是不同的)的话可能会导致内存碎片。虽然这是小概率事件,但是还是要引起我们的注意!
4、具有不可确定性,但是也远比标准 C 中的 mallo()和 free()效率高!
为了实现内存释放, heap_2 引入了内存块的概念,每分出去的一段内存就是一个内存块,剩下的空闲内存也是一个内存块,内存块大小不定。为了管理内存块又引入了一个链表结构。每个内存块前面都会有一个 BlockLink_t 类型的变量来描述此内存块。
typedef struct A_BLOCK_LINK
{
struct A_BLOCK_LINK *pxNextFreeBlock; // 指向链表中下一个空闲内存块
size_t xBlockSize; // 当前空闲内存块大小
} BlockLink_t;
为了方便管理,可用的内存块会被全部组织在一个链表内,局部静态变量 xStart, xEnd 用来记录这个链表的头和尾。
内存堆初始化函数
static void prvHeapInit( void )
{
BlockLink_t *pxFirstFreeBlock;
uint8_t *pucAlignedHeap;
// 确保内存堆的开始地址是字节对齐的
pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE )&ucHeap[ portBYTE_ALIGNMENT ] ) &
(~((portPOINTER_SIZE_TYPE) portBYTE_ALIGNMENT_MASK)));
// xStart 指向空闲内存块链表首
xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
xStart.xBlockSize = ( size_t ) 0;
// xEnd 指向空闲内存块链表尾
xEnd.xBlockSize = configADJUSTED_HEAP_SIZE;
xEnd.pxNextFreeBlock = NULL;
// 刚开始只有一个空闲内存块,空闲内存块的总大小就是可用的内存堆大小
pxFirstFreeBlock = ( void * ) pucAlignedHeap;
pxFirstFreeBlock->xBlockSize = configADJUSTED_HEAP_SIZE;
pxFirstFreeBlock->pxNextFreeBlock = &xEnd;
}
内存申请函数
void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
static BaseType_t xHeapHasBeenInitialised = pdFALSE;
void *pvReturn = NULL;
vTaskSuspendAll();
{
if( xHeapHasBeenInitialised == pdFALSE ) {
/* 第一次调用函数 pvPortMalloc() 申请内存的话就需要先初始化一次内存堆 */
prvHeapInit();
xHeapHasBeenInitialised = pdTRUE;
}
/* 所申请的内存大小进行字节对齐 */
if( xWantedSize > 0 ) {
xWantedSize += heapSTRUCT_SIZE; /* 实际申请的内存大小需要再加上结构体 BlockLink_t 的大小 */
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;
/* 从 xStart(最小内存块)开始,查找大小满足所需要内存的内存块 */
while((pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL)) {
pxPreviousBlock = pxBlock;
pxBlock = pxBlock->pxNextFreeBlock;
}
if( pxBlock != &xEnd ){
/* 找到内存块以后就将可用内存首地址保存在 pvReturn 中,这个内存首地址要跳过结构体 BlockLink_t */
pvReturn = (void *) ((( uint8_t * )pxPreviousBlock->pxNextFreeBlock) + heapSTRUCT_SIZE);
/* 内存块已经被申请了,所以需要将这个内存块从空闲内存块链表中移除 */
pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;
/* 如果申请到的实际内存减去所需的内存大小(xBlockSize-xWantedSize)大于某个阈值的时候就把多余出来的内存重新
组合成一个新的可用空闲内存块 */
if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE ) {
pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );
pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
pxBlock->xBlockSize = xWantedSize;
prvInsertBlockIntoFreeList( ( pxNewBlockLink ) ); /* 将新的空闲内存块插入到空闲内存块链表中 */
}
/* 更新全局变量 xFreeBytesRemaining,此变量用来保存内存堆剩余内存大小 */
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;
}
总结一下:
(1)是否是第一次调用内存申请函数,如果是则初始化一次内存堆
(2)确保申请的内存大小大于0,同时加上结构体的大小后,进行字节对齐
(3)从列表头开始寻找满足要求的可用内存块,找到后返回首地址,要去掉结构体的大小
(4)如果申请到的内存块大小减去所需大小的值大于一个阈值,则将申请到的内存块分成两部分,多的添加到空闲列表
内存释放函数
主要目的就是将需要释放的内存所在的内存块重新添加到空闲内存块链表中
void vPortFree(void *pv)
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;
if( pv != NULL ){
/* puc 为要释放的内存首地址,pvReturn 所指向的地址减去 heapSTRUCT_SIZE 才是要释放的内存段所在内存块的首地址 */
puc -= heapSTRUCT_SIZE;
pxLink = ( void * ) puc;
vTaskSuspendAll();
{
prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) ); /* 将内存块添加到空闲内存块列表中 */
xFreeBytesRemaining += pxLink->xBlockSize; /* 更新变量 xFreeBytesRemaining */
traceFREE( pv, pxLink->xBlockSize );
}
( void ) xTaskResumeAll();
}
}
heap_3
heap_3 的分配方法是对标准 C 中的函数 malloc() 和 free() 的简单封装,内存申请函数中会通过挂起和恢复任务调度器来为内存的申请和释放提供线程保护。
heap_4
heap_4 提供了一个最优的匹配算法,不像 heap_2, heap_4 会将内存碎片合并成一个大的可用内存块,它提供了内存块合并算法。特性如下:
1、可以用在那些需要重复创建和删除任务、队列、信号量和互斥信号量等的应用中。
2、不会像 heap_2 那样产生严重的内存碎片
,即使分配的内存大小是随机的。
3、具有不确定性,但是远比 C 标准库中的 malloc()和 free()效率高。
heap_4 也使用链表结构来管理空闲内存块,链表结构体与 heap_2 一样。 heap_4 也定义了两个局部静态变量 xStart 和 pxEnd 来表示链表头和尾,其中 pxEnd 是指向 BlockLink_t 的指针。
内存堆初始化函数
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 为内存堆字节对齐以后的可用起始地址 */
pucAlignedHeap = ( uint8_t * ) uxAddress;
/* 初始化 xStart,为可用内存块链表头 */
xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
xStart.xBlockSize = ( size_t ) 0;
/* 初始化 pxEnd,为可用内存块链表尾, 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 记录最小的那个空闲内存块大小 */
xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
/* xFreeBytesRemaining 表示内存堆剩余大小 */
xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
/* 将size_t类型变量的最高位置1,用来标记某个内存块是被使用 */
xBlockAllocatedBit = ((size_t )1) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 );
}
内存申请函数
void *pvPortMalloc(size_t xWantedSize)
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
void *pvReturn = NULL;
vTaskSuspendAll();
{
if( pxEnd == NULL ) {
/* pxEnd 为 NULL,说明内存堆还没初始化,所以需要调用函数 prvHeapInit() 初始化内存堆 */
prvHeapInit();
} else { mtCOVERAGE_TEST_MARKER(); }
/* xBlockSize 是来描述内存块大小的,其最高位用来记录内存块有没有被使用,所以申请的内存块大小最高位不能为1 */
if((xWantedSize & xBlockAllocatedBit) == 0 ) {
if(xWantedSize > 0) {
xWantedSize += xHeapStructSize; /* 实际所需申请的内存数要加上结构体 BlockLink_t 的大小 */
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(); }
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 ) {
pvReturn = (void *)(((uint8_t*)pxPreviousBlock->pxNextFreeBlock) + xHeapStructSize);
pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;
if( (pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE ) {
/* 申请到的内存块大于所需的大小,因此要把多余出来的内存重新组合成一个新的可用空闲内存块 */
pxNewBlockLink = (void *) (((uint8_t *) pxBlock) + xWantedSize );
configASSERT((((size_t)pxNewBlockLink) & portBYTE_ALIGNMENT_MASK) == 0 );
pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
pxBlock->xBlockSize = xWantedSize;
/* 将新的空闲内存块插入到空闲内存块链表中 */
prvInsertBlockIntoFreeList( pxNewBlockLink );
} else { mtCOVERAGE_TEST_MARKER(); }
/* 更新全局变量 xFreeBytesRemaining 和 xMinimumEverFreeBytesRemaining */
xFreeBytesRemaining -= pxBlock->xBlockSize;
if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining ) {
xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;
} else { mtCOVERAGE_TEST_MARKER(); }
/* xBlockSize 与 xBlockAllocatedBit 进行或运算,将 xBlockSize 的最高位置 1,表示此内存块被使用 */
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;
}
内存释放函数
void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;
if( pv != NULL ) {
puc -= xHeapStructSize; /* 获取内存块的 BlockLink_t 类型结构体 */
pxLink = ( void * ) puc; //防止编译器报错
configASSERT( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 );
configASSERT( pxLink->pxNextFreeBlock == NULL );
if( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 ) { /* 判断 xBlockSize 最高位是否等于0来判断释放的内存块是否被应用使用 */
if( pxLink->pxNextFreeBlock == NULL ) {
pxLink->xBlockSize &= ~xBlockAllocatedBit; /* xBlockSize 最高位清零,重新标记此内存块没有使用 */
vTaskSuspendAll();
{
xFreeBytesRemaining += pxLink->xBlockSize;
traceFREE( pv, pxLink->xBlockSize );
prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) ); /* 将内存块插到空闲内存链表中 */
}
( void ) xTaskResumeAll();
} else { mtCOVERAGE_TEST_MARKER(); }
} else { mtCOVERAGE_TEST_MARKER(); }
}
}
heap_5
heap_5 使用了和 heap_4 相同的合并算法,内存管理实现起来基本相同,但是 heap_5 允许内存堆跨越多个不连续的内存段。这些不连续的内存段就是由结构体 HeapRegion_t 来定义的。heap_5 允许内存堆不连续,说白了就是允许有多个内存堆。在 heap_2 和 heap_4 中只有一个内存堆,初始化的时候只也只需要处理一个内存堆。 heap_5 有多个内存堆,这些内存堆会被连接在一起,和空闲内存块链表类似。
typedef struct HeapRegion
{
uint8_t *pucStartAddress; // 内存块的起始地址
size_t xSizeInBytes; // 内存段大小
} HeapRegion_t;
如果使用 heap_5 的话,在调用 API 函数之前需要先调用函数 vPortDefineHeapRegions()
来对内存堆做初始化处理,在 vPortDefineHeapRegions()未执行完之前禁止调用任何可能会调用 pvPortMalloc() 的 API 函数!比如创建任务、信号量、队列等函数。函数 vPortDefineHeapRegions()只有一个参数,参数是一个 HeapRegion_t 类型的数组。
比如以 STM32F103 开发板为例,现在有连个内存段:内部 SRAM、外部 SRAM,起始分别为: 0X20000000、 0x68000000,大小分别为: 64KB、 1MB,那么数组就如下:
HeapRegion_t xHeapRegions[] =
{
{ ( uint8_t * ) 0X20000000UL, 0x10000 },//内部 SRAM 内存,起始地址 0X20000000,大小为 64KB
{ ( uint8_t * ) 0X68000000UL, 0x100000},//外部 SRAM 内存,起始地址 0x68000000,大小为 1MB
{ NULL, 0 } //数组结尾
};
数组中成员顺序按照地址从低到高的顺序排列,而且最后一个成员必须使用 NULL。
总结
heap_1 最简单,但是只能申请内存,不能释放。
heap_2 提供了内存释放函数,用户代码也可以直接调用函数 pvPortMalloc() 和 vPortFree() 来申请和释放内存,但是 heap_2 会导致内存碎片的产生!
heap_3 是对标准 C 库中的函数 malloc() 和 free() 的简单封装,并且提供了线程保护。
heap_4 相对与 heap_2 提供了内存合并功能,可以降低内存碎片的产生,我们移植 FreeRTOS 的时候就选择了 heap_4。
heap_5 基本上和 heap_4 一样,只是 heap_5 支持内存堆使用不连续的内存块。