一、内存管理基本概�?

FreeRTOS 内存管理模块管理用于系统中内存资源,它是操作系统的核心模块之一。主要包括内存的初始化、分配以及释放�?

很多人会有疑问,什么不直接使用 C 标准库中的内存管理函数呢?在电脑中我们可以用 malloc()�? free()这两个函数动态的分配内存和释放内存。但是,在嵌入式实时操作系统中,调用 malloc()�? free()却是危险的,原因有以下几点:

  • 这些函数在小型嵌入式系统中并不总是可用的,小型嵌入式设备中�? RAM 不足�?

  • 它们的实现可能非常的大,占据了相当大的一块代码空间�?

  • 他们几乎都不是安全的�?

  • 它们并不是确定的,每次调用这些函数执行的时间可能都不一样�?

  • 它们有可能产生碎片。(就是内存会越切越小)

  • 这两个函数会使得链接器配置得复杂�?

  • 如果允许堆空间的生长方向覆盖其他变量占据的内存,它们会成�? debug 的灾难�?

FreeRTOS 对内存管理做了很多事情,FreeRTOS �? V9.0.0 版本为我们提供了 5 种内存管理算法,分别�? heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c,源文件存放于FreeRTOS\Source\portable\MemMang 路径下,在使用的时候选择其中一个添加到我们的工程中去即可�?

FreeRTOS内核规定的几个内存管理函数原型为�?

void *pvPortMalloc( size_t xSize ) :内存申请函�?
void vPortFree( void *pv ) :内存释放函�?
void vPortInitialiseBlocks( void ) :初始化内存堆函�?
size_t xPortGetFreeHeapSize( void ) :获取当前未分配的内存堆大小
size_t xPortGetMinimumEverFreeHeapSize( void ):获取未分配的内存堆历史最小�?

二、内存管理方案详�?

对于 heap_1.c、heap_2.c �? heap_4.c 这三种内存管理方案,内存堆实际上是一个很大的�? �?�? �? �? �? static uint8_t ucHeap[ configTOTAL_HEAP_SIZE] �? �? �? �? 义configTOTAL_HEAP_SIZE 则表示系统管理内存大小,单位为字,在 FreeRTOSConfig.h 中由用户设定�?

对于 heap_3.c 这种内存管理方案,它封装�? C 标准库中�? malloc()�? free()函数,封装后�? malloc()�? free()函数具备保护,可以安全在嵌入式系统中执行。因此,用户需要通过编译器或者启动文件设置堆空间。heap_5.c 方案允许用户使用多个非连续内存堆空间,每个内存堆的起始地址和大小由用户定义。这种应用其实还是很大的,比如做图形显示、GUI 等,可能芯片内部�? RAM是不够用户使用的,需要外�? SDRAM,那这种内存管理方案则比较合适�?

1、heap_1.c

heap_1.c 管理方案�? FreeRTOS 提供所有内存管理方案中最简单的一个,它只能申请内存而不能进行内存释放,并且申请内存的时间是一个常量,这样子对于要求安全的嵌入式设备来说是最好的,因为不允许内存释放,就不会产生内存碎片而导致系统崩溃,但是也有缺点,那就是内存利用率不高,某段内存只能用于内存申请的地方,即使该内存只使用一次,也无法让系统回收重新利用�?

heap1.c 方案具有以下特点�?

  1. 用于从不删除任务、队列、信号量、互斥量等的应用程序(实际上大多数使用FreeRTOS 的应用程序都符合这个条件)�?
  2. 函数的执行时间是确定的并且不会产生内存碎片�?

这个内存管理策略使用两个局部静态变量来跟踪内存分配,变量定义为�?

1
2
static size_t xNextFreeByte = ( size_t ) 0;
static uint8_t *pucAlignedHeap = NULL;

1.1内存申请:pvPortMalloc()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
void *pvPortMalloc( size_t xWantedSize )
{
void *pvReturn = NULL;
static uint8_t *pucAlignedHeap = NULL;


/* 确保申请的字节数是对齐字节数的倍数 */
#if( portBYTE_ALIGNMENT != 1 )
{
if( xWantedSize & portBYTE_ALIGNMENT_MASK )
{
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
}
}
#endif


vTaskSuspendAll();
{
if( pucAlignedHeap == NULL )
{
/* 第一次使�?,确保内存堆起始位置正确对�? */
pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) &ucHeap[ portBYTE_ALIGNMENT ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
}


/* 边界检�?,变量xNextFreeByte是局部静态变�?,初始值为0 */
if( ( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE ) &&
( ( xNextFreeByte + xWantedSize ) > xNextFreeByte ) )
{
/* 返回申请的内存起始地址并更新索�? */
pvReturn = pucAlignedHeap + xNextFreeByte;
xNextFreeByte += xWantedSize;
}
}
( void ) xTaskResumeAll();


#if( configUSE_MALLOC_FAILED_HOOK == 1 )
{
if( pvReturn == NULL )
{
extern void vApplicationMallocFailedHook( void );
vApplicationMallocFailedHook();
}
}
#endif


return pvReturn;
}

�? �? �? �? �? �? �? �? �? �? �? �? 需 �? �? �? �? �? �? �? �? �? �? �? �? �? 需 �? �? �? 量pucAlignedHeap 指向内存域第一个地址对齐处,因为系统管理的内存其实是一个大数组,而编译器为这个数组分配的起始地址是随机的,不一定符合系统的对齐要求,这时候要进行内存地址对齐操作。比如数�? ucHeap 的地址�? 0x20000123 处开始,系统按照 8 字节对齐,如下�?:

image-20230214190355481

在内存对齐完成后,用户想要申请一�? 30字节大小的内存,那么按照系统对齐的要求,我们会申请到 32 个字节大小的内存空间,即使我们只需�? 30 字节的内存,申请完成的示意图具体如下:

image-20230214190445976

2、heap_2.c

heap_2.c 方案�? heap_1.c 方案采用的内存管理算法不一样,它采用一种最佳匹配算�?(best fit algorithm),比如我们申�? 100 字节的内存,而可申请内存中有三块对应大小 200 字节�? 500 字节�? 1000 字节大小的内存块,按照算法的最佳匹配,这时候系统会�? 200 字节大小的内存块进行分割并返回申请内存的起始地址,剩余的内存则插回链表留待下次申请。Heap_2.c 方案支持释放申请的内存,但是它不能把相邻的两个小的内存块合成一个大的内存块,对于每次申请内存大小都比较固定的,这个方式是没有问题的,而对于每次申请并不是固定内存大小的则会造成内存碎片,后面要讲解�? heap_4.c 方案采用的内存管理算法能解决内存碎片的问题,可以把这些释放的相邻的小的内存块合并成一个大的内存块�?

同样�? ,内存分配时需 要的总的内存�? 空间由文 �? FreeRTOSConfig.h 中的 宏configTOTAL_HEAP_SIZE 配置,单位为字。通过调用函数 xPortGetFreeHeapSize() 我们可同样的 ,内存分配时需 要的总的内存�? 空间由文 �? FreeRTOSConfig.h 中configTOTAL_HEAP_SIZE 配置,单位为字。通过调用函数 xPortGetFreeHeapSize() 我们可以知道还剩下多少内存没有使用,但是并不包括内存碎片,这样一来我们可以实时的调整和优�? configTOTAL_HEAP_SIZE 的大小�?

heap_2.c 方案具有以下特点�?

  1. 可以用在那些反复的删除任务、队列、信号量、等内核对象且不担心内存碎片的应用程序�?
  2. 如果我们的应用程序中的队列、任务、信号量、等工作在一个不可预料的顺序,这样子也有可能会导致内存碎片�?
  3. 具有不确定性,但是效率比标�? C 库中�? malloc 函数高得多�?
  4. 不能用于那些内存分配和释放是随机大小的应用程序�?

heap_2.c 方案采用链表的数据结构记录空闲内存块,将所有的空闲内存块组成一个空闲内存块链表,FreeRTOS 采用 2 �? BlockLink_t 类型的局部静态变�? xStart、xEnd 来标识空闲内存块链表的起始位置与结束位置,空闲内存块链表结构体如下:

1
2
3
4
typedef struct A_BLOCK_LINK {
struct A_BLOCK_LINK *pxNextFreeBlock;//pxNextFreeBlock 成员变量是指向下一个空闲内存块的指针�?
size_t xBlockSize; //xBlockSize 用于记录申请的内存块的大小,包括链表结构体大小�?
} BlockLink_t;

初始化prvHeapInit()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void prvHeapInit(void)
{
BlockLink_t *pxFirstFreeBlock;
uint8_t *pucAlignedHeap;

/* 保证 pucAlignedHeap 也是按照指定内存要求对齐�? */
pucAlignedHeap = (uint8_t *)(((portPOINTER_SIZE_TYPE)&ucHeap[portBYTE_ALIGNMENT]) & (~((portPOINTER_SIZE_TYPE)portBYTE_ALIGNMENT_MASK)));

/* 空闲内存链表头部初始�? */
xStart.pxNextFreeBlock = (void *)pucAlignedHeap;
xStart.xBlockSize = (size_t)0;

/* 空闲内存链表尾部初始�? */
xEnd.xBlockSize = configADJUSTED_HEAP_SIZE;
xEnd.pxNextFreeBlock = NULL;

/* �? pxFirstFreeBlock 放入空闲链表中,因为空闲内存块链表除了要有头部与尾部�?
还需要有真正可用的内存,而第一块可用的内存就是 pxFirstFreeBlock�?
pxFirstFreeBlock 的大小是系统管理的内存大�? configADJUSTED_HEAP_SIZE */
pxFirstFreeBlock = (void *)pucAlignedHeap;
pxFirstFreeBlock->xBlockSize = configADJUSTED_HEAP_SIZE;
pxFirstFreeBlock->pxNextFreeBlock = &xEnd;
}

局部静态变量pucAlignedHeap指向对齐后的内存堆起始位置。地址对齐的原因在第一种内存管理策略中已经说明。假如内存堆数ucHeap从RAM地址0x10002003处开始,系统按照8字节对齐,则对齐后的内存堆与第一个内存管理策略一样,如图:

image-20230214192646068

空闲内存块的初始化就分析完成,将内存块以链表的形式去管理,初始化完成示意图具体如�?:

image-20230214193153123

内存申请pvPortMalloc()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
static BaseType_t xHeapHasBeenInitialised = pdFALSE;
void *pvReturn = NULL;
/* 挂起调度�? */
vTaskSuspendAll();
{
/* 如果是第一次调用内存分配函�?,这里先初始化内存�?,如图2-2所�? */
if( xHeapHasBeenInitialised == pdFALSE )
{
prvHeapInit();
xHeapHasBeenInitialised = pdTRUE;
}


/* 调整要分配的内存�?,需要增加上链表结构体空�?,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 ) )
{
/* 空闲内存块是按照块的大小排序�?,从链表头xStart开�?,小的在前大的在后,以链表尾xEnd结束 */
pxPreviousBlock = &xStart;
pxBlock = xStart.pxNextFreeBlock;
/* 搜索最合适的空闲�? */
while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
{
pxPreviousBlock = pxBlock;
pxBlock = pxBlock->pxNextFreeBlock;
}


/* 如果搜索到链表尾xEnd,说明没有找到合适的空闲内存�?,否则进行下一步处�? */
if( pxBlock != &xEnd )
{
/* 返回内存空间,注意是跳过了结构体BlockLink_t空间. */
pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + heapSTRUCT_SIZE);


/* 这个块就要返回给用户,因此它必须从空闲块中去除. */
pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;
/* 如果这个块剩余的空间足够�?,则将它分成两�?,第一个返回给用户,第二个作为新的空闲块插入到空闲块列表中去*/
if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
{
/* 去除分配出去的内�?,在剩余内存块的起始位置放置一个链表结构并初始化链表成�? */
pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );


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

随着内存申请,越来越多申请的内存块脱离空闲内存链表,但链表仍是以 xStart 节点开头以 xEnd 节点结尾,空闲内存块链表根据空闲内存块的大小进行排序。每当用户申请一次内存的时候,系统都要分配一�? BlockLink_t 类型结构体空间,用于保存申请的内存块信息,并且每个内存块在申请成功后会脱离空闲内存块链表,申请两次后的内存示意图具体见图�?

image-20230214193506420

内存释放函数 vPortFree()

这个函数比较简单,就直接看源码就好�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;


if( pv != NULL )
{
/* 根据传入的参数找到链表结�? */
puc -= heapSTRUCT_SIZE;


/* 预防某些编译器警�? */
pxLink = ( void * ) puc;


vTaskSuspendAll();
{
/* 将这个块添加到空闲块列表 */
prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );//直接将内存块按从小到大插入就好了
/* 更新未分配的内存堆大�? */
xFreeBytesRemaining += pxLink->xBlockSize;

traceFREE( pv, pxLink->xBlockSize );
}
( void ) xTaskResumeAll();
}
}

image-20230214194242383

获取未分配的内存堆大小xPortGetFreeHeapSize()

1
2
3
4
size_t xPortGetFreeHeapSize( void )
{
return xFreeBytesRemaining;//它用来动态记录未分配的内存堆大小
}

3、heap_3.c

第三种内存管理策略简单的封装了标准库中的malloc()和free()函数,采用的封装方式是操作内存前挂起调度器、完成后再恢复调度器。封装后的malloc()和free()函数具备线程保护�?第一种和第二种内存管理策略都是通过定义一个大数组作为内存堆,数组的大小由宏configTOTAL_HEAP_SIZE指定。第三种内存管理策略与前两种不同,它不再需要通过数组定义内存堆,而是需要使用编译器设置内存堆空间,一般在启动代码中设置。因此宏configTOTAL_HEAP_SIZE对这种内存管理策略是无效的�?

内存申请pvPortMalloc()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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;
}

内存释放

1
2
3
4
5
6
7
8
9
10
11
12
void vPortFree( void *pv )
{
if( pv )
{
vTaskSuspendAll();
{
free( pv );
traceFREE( pv, 0 );
}
( void ) xTaskResumeAll();
}
}

4、heap4.c

第四种内存分配方法与第二种比较相似,只不过增加了一个合并算法,将相邻的空闲内存块合并成一个大块。与第一种和第二种内存管理策略一样,内存堆仍然是一个大数组,定义为�?

1
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];

heap_4.c 方案的空闲内存块也是以单链表的形式连接起来的,BlockLink_t 类型的局部静态变�? xStart 表示链表头,�? heap_4.c 内存管理方案的链表尾部则保存在内存堆空间最后位置,并使�? BlockLink_t 指针类型局部静态变�? pxEnd 指向这个区域(�? heap_2.c 内存管理方案则使�? BlockLink_t 类型的静态变�? xEnd 表示链表尾)

heap_4.c 内存管理方案的空闲块链表不是以内存块大小进行排序的,而是以内存块起始地址大小排序,内存地址小的在前,地址大的在后,因�? heap_4.c 方案还有一个内存合并算法,在释放内存的时候,假如相邻的两个空闲内存块在地址上是连续的,那么就可以合并为一个内存块,这也是为了适应合并算法而作的改变�?

heap_4.c 方案具有以下特点�?

  1. 可用于重复删除任务、队列、信号量、互斥量等的应用程序
  2. 可用于分配和释放随机字节内存的应用程序,但并不像 heap2.c 那样产生严重的内存碎片�?
  3. 具有不确定性,但是效率比标�? C 库中�? malloc 函数高得多�?

内存初始化函数prvHeapInit()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
static void prvHeapInit(void)
{
lockLink_t *pxFirstFreeBlock;
int8_t *pucAlignedHeap;
size_t uxAddress;
size_t xTotalHeapSize = configTOTAL_HEAP_SIZE;
/*进行内存对齐操作 */
xAddress = (size_t)ucHeap;

if ((uxAddress & portBYTE_ALIGNMENT_MASK) != 0)
{
uxAddress += (portBYTE_ALIGNMENT - 1);
uxAddress &= ~((size_t)portBYTE_ALIGNMENT_MASK);
// xTotalHeapSize 表示系统管理的总内存大�?
xTotalHeapSize -= uxAddress - (size_t)ucHeap;
}

pucAlignedHeap = (uint8_t *)uxAddress;

// 初始化链表头�?
xStart.pxNextFreeBlock = (void *)pucAlignedHeap;
xStart.xBlockSize = (size_t)0;

/* 初始�? pxEnd,计�? pxEnd 的位置,它的值为内存尾部向前偏移一�?
BlockLink_t 结构体大小,偏移出来的这�? BlockLink_t 就是 pxEnd */
uxAddress = ((size_t)pucAlignedHeap) + xTotalHeapSize;
uxAddress -= xHeapStructSize;
uxAddress &= ~((size_t)portBYTE_ALIGNMENT_MASK);
pxEnd = (void *)uxAddress;
pxEnd->xBlockSize = 0;
pxEnd->pxNextFreeBlock = NULL;

/* �? heap_2.c 中的初始化类似,将当前所有内存插入空闲内存块链表中�?
不同的是链表的尾部不是静态的,而是放在了内存的最后�? */
pxFirstFreeBlock = (void *)pucAlignedHeap;
pxFirstFreeBlock->xBlockSize = uxAddress - (size_t)pxFirstFreeBlock;
pxFirstFreeBlock->pxNextFreeBlock = pxEnd;

/* 更新统计变量 */
xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;

/* 这个 xBlockAllocatedBit 比较特殊,这里被设置为最高位�? 1 其余�? 0 �?
一�? size_t 大小的值,这样任意一�? size_t 大小的值和 xBlockAllocatedBit
进行按位与操�?,如果该值最高位�? 1,那么结果为 1,否则结果为 0�?
FreeRTOS 利用这种特性标记一个内存块是否空闲�? */
xBlockAllocatedBit = ((size_t)1) << ((sizeof(size_t) * heapBITS_PER_BYTE) - 1);
}

heap_4.c 内存初始化完成示意图具体见图23-8�?

image-20230214202303646

内存块插入函�? prvInsertBlockIntoFreeList()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
static void prvInsertBlockIntoFreeList(BlockLink_t *pxBlockToInsert)
{
BlockLink_t *pxIterator;
uint8_t *puc;

/* 首先找到�? pxBlockToInsert 相邻的前一个空闲内�? */
for (pxIterator = &xStart;pxIterator->pxNextFreeBlock < pxBlockToInsert;pxIterator = pxIterator->pxNextFreeBlock)
{
//这里什么都不做,只为了找空闲内存而已
}

puc = (uint8_t *)pxIterator;

/* 如果前一个内存的尾部恰好�? pxBlockToInsert 的头部,
那代表这两个内存是连续的,可以合�?*/
if ((puc + pxIterator->xBlockSize) == (uint8_t *)pxBlockToInsert)
{
/* �? pxBlockToInsert 合并�? pxIterator �? */
pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
pxBlockToInsert = pxIterator;
}
else
{
mtCOVERAGE_TEST_MARKER();
}

/* 判断 pxBlockToInsert 是否和后面的空闲内存相邻 */
puc = (uint8_t *)pxBlockToInsert;
if ((puc + pxBlockToInsert->xBlockSize) ==(uint8_t *)pxIterator->pxNextFreeBlock)
{
/* 与之相邻的下一个内存块不是链表尾节�? */
if (pxIterator->pxNextFreeBlock != pxEnd)
{
/* 将后面的内存合入 pxBlockToInsert,并�? pxBlockToInsert 代替该内存在链表中的位置 */
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();
}
}

这个合并的算法常用于释放内存的合并,申请内存的时候能合并的早已合并,因为申请内存是从一个空闲内存块前面分割,分割后产生的内存块都是一整块的,基本不会进行合并,申请内存常见的情况具体见图23-9�?

image-20230214203159531

内存申请函数 pvPortMalloc()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
void *pvReturn = NULL;

vTaskSuspendAll();
{
/* 如果是第一次调用内存分配函数,先初始化内存�? */
if ( pxEnd == NULL ) {
prvHeapInit();
}else {
mtCOVERAGE_TEST_MARKER();
}
/* 这里 xWantedSize 的大小有要求,需要最高位�? 0。因为后�? BlockLink_t 结构体中�? xBlockSize 的最高位需要使用这个成员的最高位被用来标识这个块是否空闲。因此要申请的块大小不能使用这个�?*/
if ( ( xWantedSize & xBlockAllocatedBit ) == 0 ) {
/* 调整要分配的内存值,需要增加上链表结构体所占的内存空间heapSTRUCT_SIZE 表示链表结构体节点经过内存对齐后的内存大小因为空余内存的头部要放一�? BlockLink_t 类型的节点来管理,因此这里需要人为的扩充下申请的内存大小 */
if ( xWantedSize > 0 ) {
xWantedSize += xHeapStructSize;

/* 需要申请的内存大小与系统要求对齐的字节数不匹配,需要进行内存对�? */
if ( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 ) {
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
} else {
mtCOVERAGE_TEST_MARKER();
}
} else {
mtCOVERAGE_TEST_MARKER();
}

//如果当前的空闲内存足够满足用户申请的内存大小,就进行内存申请操作
if ( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) ) {
/* 从空余内存链表的头部开始找,如果该空余内存的大�?>xWantedSize,就从这块内存中抠出一部分内存返回,剩余的内存生成新的 BlockLink_t 插入链表�?*/
pxPreviousBlock = &xStart;
pxBlock = xStart.pxNextFreeBlock;
//从链表头部开始查找大小符合条件的空余内存
while ( ( pxBlock->xBlockSize < xWantedSize )&& ( pxBlock->pxNextFreeBlock != NULL ) ) {
pxPreviousBlock = pxBlock;
pxBlock = pxBlock->pxNextFreeBlock;
}

/* 如果搜索到链表尾 xEnd,说明没有找到合适的空闲内存块,否则进行下一步处�?*/
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 );
/* 通过计算得到剩余的内存大小,并且赋值给剩余内存块链表节点中�? xBlockSize 成员变量,方便下一次的内存查找 */
pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
pxBlock->xBlockSize = xWantedSize;

/* 将被切割而产生的新空闲内存块添加到空闲链表中 */
prvInsertBlockIntoFreeList( pxNewBlockLink );
} else {
mtCOVERAGE_TEST_MARKER();
}

//更新剩余内存总大�?
xFreeBytesRemaining -= pxBlock->xBlockSize;
//如果当前内存大小小于历史最小记录,更新历史最小内存记�?
if ( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining ) {
xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;
} else {
mtCOVERAGE_TEST_MARKER();
}

/* 注意这里�? 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
return pvReturn;
}

内存申请函数,在申请 3 次内存完成之后的示意图具体见�? 23-10�?

image-20230214203317335

内存释放函数vPortFree()

heap_4.c 内存管理方案的内存释放函�? vPortFree()也比较简单,根据传入要释放的内存块地址,偏移之后找到链表节点,然后将这个内存块插入到空闲内存块链表中,在内存块插入过程中会执行合并算法,这个我们已经在内存申请中讲过了(而且合并算法多用于释放内存中)。最后是将这个内存块标志为“空闲”(内存块节点的 xBlockSize 成员变量最高位�? 0)、再更新未分配的内存堆大小即可�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void vPortFree(void *pv)
{
uint8_t *puc = (uint8_t *)pv;
BlockLink_t *pxLink;

if (pv != NULL)
{
/* 偏移得到节点地址 */
puc -= xHeapStructSize;
pxLink = (void *)puc;

/* 断言 */
configASSERT((pxLink->xBlockSize & xBlockAllocatedBit) != 0);
configASSERT(pxLink->pxNextFreeBlock == NULL);

/* 判断一下内存块是否已经是被分配使用的,如果是就释放该内存块 */
if ((pxLink->xBlockSize & xBlockAllocatedBit) != 0)
{
if (pxLink->pxNextFreeBlock == NULL)
{
/* 将内存块标识为空�? */
pxLink->xBlockSize &= ~xBlockAllocatedBit;
vTaskSuspendAll();
{
/* 更新系统当前空闲内存的大小,添加到内存块空闲链表�? */
xFreeBytesRemaining += pxLink->xBlockSize;
traceFREE(pv, pxLink->xBlockSize);
prvInsertBlockIntoFreeList(((BlockLink_t *)pxLink));
}
(void)xTaskResumeAll();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}

按照内存释放的过程,当我们释放一个内存时,如果与它相邻的内存块都不是空闲的,那么该内存块并不会合并,只会被添加到空闲内存块链表中,其过程示意图具体见�?23-11。而如果某个时间段释放了另一个内存块,发现该内存块前面有一个空闲内存块与它在地址上是连续的,那么这两个内存块会合并成一个大的内存块,并插入空闲内存块链表中,其过程示意图具体见�? 23-12�?

image-20230214203903557

image-20230214203849508

5、heap_5.c

heap_5.c 方案在实现动态内存分配时�? heap4.c 方案一样,采用最佳匹配算法和合并算法,并且允许内存堆跨越多个非连续的内存区,也就是允许在不连续的内存堆中实现内存分配,比如用户在片内 RAM 中定义一个内存堆,还可以在外�? SDRAM 再定义一个或多个内存堆,这些内存都归系统管理�?

heap_5.c 方案通过调用 vPortDefineHeapRegions()函数来实现系统管理的内存初始化,在内存初始化未完成前不允许使用内存分配和释放函数。如创建 FreeRTOS 对象(任务、队列、信号量等)时会隐式的调�? pvPortMalloc()函数,因此必须注意:使用 heap_5.c 内存管理方案创建任何对象前,要先调用 vPortDefineHeapRegions()函数将内存初始化�?

vPortDefineHeapRegions()函数只有一个形参,该形参是一�? HeapRegion_t 类型的结构体数组。HeapRegion_t 类型结构体在 portable.h 中定义,具体见代码:

1
2
3
4
5
6
typedef struct HeapRegion {
/* 用于内存堆的内存块起始地址*/
uint8_t *pucStartAddress;
/* 内存块大�? */
size_t xSizeInBytes;
} HeapRegion_t;

�? �? 需 要指 定每 �? 内存 �? 区域 的起 �? 地址 �? 内存 堆大 �? 、将 �? 们放 在一 个HeapRegion_t 结构体类型数组中,这个数组必须用一�? NULL 指针�? 0 作为结尾,起始地址必须从小到大排列。假设我们为内存堆分配两个内存块,第一个内存块大小�? 0x10000字节,起始地址�? 0x80000000;第二个内存块大小为 0xa0000 字节,起始地址�?0x90000000,vPortDefineHeapRegions()函数使用实例具体见代�?:

1
2
3
4
5
6
7
8
9
10
11
/* 在内存中为内存堆分配两个内存块�?
第一个内存块大小�? 0x10000 字节,起始地址�? 0x80000000,
第二个内存块大小�? 0xa0000 字节,起始地址�? 0x90000000�?
起始地址�? 0x80000000 的内存块的起始地址更低,因此放到了数组的第一个位置�?*/
const HeapRegion_t xHeapRegions[] = {
{ ( uint8_t * ) 0x80000000UL, 0x10000 },
{ ( uint8_t * ) 0x90000000UL, 0xa0000 },
{ NULL, 0 } /* 数组结尾 */
};
/* 向函�? vPortDefineHeapRegions()传递形�? */
vPortDefineHeapRegions( xHeapRegions );

用户在自定义好内存堆数组后,需要调�? vPortDefineHeapRegions()函数初始化这些内存堆,系统会已一个空闲内存块链表的数据结构记录这些空闲内存,链表�? xStart 节点构开头,�? pxEnd 指针指向的位置结束。vPortDefineHeapRegions()函数对内存的初始�?与heap_4.c 方案一�?,在这里就不再重复赘述过程。以上面的内存堆数组为例,初始化完成后的内存堆示意图:

image-20230214204419906

三、参考文�?

野火电子论坛

FreeRTOS高级�?7—-FreeRTOS内存管理分析