FreeRTOS 内存管理源码解析

[toc]


FreeRTOS 提供了 5 种不同的内存管理策略以应对不同的应用场景,本章将对这 5 种不同的内存管理策略的实现进行分析。

我参考的源码是:FreeRTOS-Kernel-10.4.3-LTS-Patch-3\portable\MemMang,该路径下记录了 heap_1.cheap_2.cheap_3.cheap_4.cheap_5.c,讲解会从这 5 个文件的源码入手。

一、heap_1

1、源码讲解

首先,FreeRTOS 将堆定义为一个大数组,并使用变量 xNextFreeByte 记录已用内存大小。

1
2
3
4
5
6
7
/* 字节对齐堆起始地址可能会丢失几个字节 */
#define configADJUSTED_HEAP_SIZE ( configTOTAL_HEAP_SIZE - portBYTE_ALIGNMENT )

static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];

/* Index into the ucHeap array. */
static size_t xNextFreeByte = ( size_t ) 0;

核心代码 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
void * pvPortMalloc( size_t xWantedSize )
{
void * pvReturn = NULL;
static uint8_t * pucAlignedHeap = NULL;

/* 如果对齐字节数不为1,则对请求的字节数做调整
* 这里portBYTE_ALIGNMENT等于8 */
#if ( portBYTE_ALIGNMENT != 1 )
{
/* 检查 xWantedSize 是否满足字节对齐的要求
* 如果不满足才会进入这句判断语句 */
if( xWantedSize & portBYTE_ALIGNMENT_MASK )
{
/* 计算调整后的内存大小,并检查可能的溢出
* 这句判断计算需要调整的字节数,即需要添加的字节,以确保对齐 */
if ( (xWantedSize + ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) )) > xWantedSize )
{
// 没有溢出
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
}
else
{
// 如果在调整大小时发生了溢出,这意味着请求的大小不合法。
xWantedSize = 0;
}
}
}
#endif

/* 挂起任务 */
vTaskSuspendAll();
{
if( pucAlignedHeap == NULL )
{
/* 作用:确保堆从正确对齐的边界开始
* 宏定义:portBYTE_ALIGNMENT == 8,portBYTE_ALIGNMENT_MASK == 0x0007,portPOINTER_SIZE_TYPE 为 uint32_t
* 可以整理一下方便看:
* ( ( portPOINTER_SIZE_TYPE ) &ucHeap[ portBYTE_ALIGNMENT ] ) = x
* ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) = y
* (uint8_t *)( x & y );
* 所以这里的操作为:
* 第一句的作用:获取地址指向 ucHeap 数组中偏移 portBYTE_ALIGNMENT 的位置,
* 并将指针转换为 portPOINTER_SIZE_TYPE 可以确保我们以适当的大小处理指针,
* 避免潜在的数据丢失或不正当的操作。
* 第二局的作用:将掩码转换为 portPOINTER_SIZE_TYPE 并按位取反,相当于变成了:0xFFF8(1111 1111 1111 1000)
* 所以这一句的作用为清除 ucHeap 的低 3 位,使得分配的起始位置是满足 portBYTE_ALIGNMENT 对齐要求(8位)的 */
pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) & ucHeap[ portBYTE_ALIGNMENT ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
}

/* 检查是否有足够的空间分配 */
if( ( xWantedSize > 0 ) && /* valid size */
( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE ) &&
( ( xNextFreeByte + xWantedSize ) > xNextFreeByte ) ) /* 防止数值溢出 */
{
/* 返回首地址 */
pvReturn = pucAlignedHeap + xNextFreeByte;
/* 记录已分配空间大小 */
xNextFreeByte += xWantedSize;
}

traceMALLOC( pvReturn, xWantedSize );
}
( void ) xTaskResumeAll(); /* 恢复任务 */

return pvReturn;
}

需要注意的是,并未使用 configTOTAL_HEAP_SIZE 代表堆大小,而是用 configADJUSTED_HEAP_SIZE 表示堆大小,configADJUSTED_HEAP_SIZE 定义如下

1
#define configADJUSTED_HEAP_SIZE  ( configTOTAL_HEAP_SIZE - portBYTE_ALIGNMENT )

这里简单粗暴的丢弃掉了对齐字节数个字节,以此来表示堆的起始地址对齐的操作中损失的字节数(最多不会损失掉对齐字节数个字节)

heap_1.c 剩下的还有如下 3 个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void vPortFree( void * pv )
{
/* heap_1 没有实现如何释放内存!!! */
( void ) pv;

/* 强制 assert,因为调用此函数无效 */
configASSERT( pv == NULL );
}

void vPortInitialiseBlocks( void )
{
/* 仅在未清除静态内存时需要 */
xNextFreeByte = ( size_t ) 0;
}

size_t xPortGetFreeHeapSize( void )
{
return( configADJUSTED_HEAP_SIZE - xNextFreeByte );
}

2、总结

heap_1.c 所实现的内存管理方法十分简单,其可以使用 pvPortMalloc() 函数来申请内存,一旦申请成功了,便无法被释放。其实现大致可以用一句话概括,==在堆空间剩余时,按需分割出内存,并记录已用的内存大小==。heap_1.c 使用的内存管理算法虽然简单,但对于许多嵌入式应用场景是适用且有效的。

二、heap_2

1、源码讲解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 字节对齐堆起始地址可能会丢失几个字节 */
#define configADJUSTED_HEAP_SIZE ( configTOTAL_HEAP_SIZE - portBYTE_ALIGNMENT )

static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];

/* 定义链表结构,这用于按大小的顺序链接空闲块. */
typedef struct A_BLOCK_LINK
{
struct A_BLOCK_LINK * pxNextFreeBlock; /* 指向列表中的下一个空闲块. */
size_t xBlockSize; /* 空闲块的大小(包括BlockLink_t 头部大小) */
} BlockLink_t;

/* 确定结构 BlockLink_t 的大小,同时考虑到内存的字节对齐要求 */
static const uint16_t heapSTRUCT_SIZE = ( ( sizeof( BlockLink_t ) + ( portBYTE_ALIGNMENT - 1 ) ) & ~portBYTE_ALIGNMENT_MASK );
#define heapMINIMUM_BLOCK_SIZE ( ( size_t ) ( heapSTRUCT_SIZE * 2 ) ) /* 空闲块的最小大小 */

/* 创建两个列表链接以标记列表的开头和结尾. */
static BlockLink_t xStart, xEnd;

/* 跟踪剩余的空闲字节数,但不说明碎片 */
static size_t xFreeBytesRemaining = configADJUSTED_HEAP_SIZE;

然后 FreeRTOS 还定义了一个将内存块插入链表的宏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* 将数据块插入到可用数据块列表中
* 该列表按数据块大小排序。列表开头的块是小块和列表末尾的块是大块。
*/
#define prvInsertBlockIntoFreeList( pxBlockToInsert ) \
{ \
BlockLink_t * pxIterator; \
size_t xBlockSize; \
\
xBlockSize = pxBlockToInsert->xBlockSize; \
\
/* 遍历列表,直到找到一个比我们插入的块更大的块 */ \
for( pxIterator = &xStart; pxIterator->pxNextFreeBlock->xBlockSize < xBlockSize; pxIterator = pxIterator->pxNextFreeBlock ) \
{ \
/* 等待迭代到正确的位置 */ \
} \
\
/* 更新列表以包括插入到正确位置的块。 */ \
pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock; \
pxIterator->pxNextFreeBlock = pxBlockToInsert; \
}

简单来说就是先查找适当的位置,然后将内存块插入到链表中,非常简单,不多说。

BlockLink_t 只描述了内存块的大小和内存块的链接关系,具体分配出的内存表示方式如下图所示:

1.1 堆的初始化

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;

/* 确保堆从正确对齐的边界开始
* 和刚才 heap_1 中讲的一模一样,不理解再回去看 */
pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) & ucHeap[ portBYTE_ALIGNMENT ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );

/* xStart 用于保存指向 free 块列表中第一项的指针。
* void 强制转换用于防止编译器警告. */
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;
}

大体来看,这个函数干了三件事,分别初始化链表起始时的三个内存块:

  1. 一块是链表的头部,被定义为 xStart,内存块大小为 0;
  2. 另一块是链表的尾部,被定义为 xEnd,内存块大小为整个堆大小 configADJUSTED_HEAP_SIZEconfigADJUSTED_HEA_SIZE 是堆对齐操作后的大小);
  3. 最后一个内存块是实际用于内存分配的普通块,其被定义在堆对齐后的起始地址上,块大小为整个堆大小 configADJUSTED_HEAP_SIZE

普通内存块相比,xStartxEnd 具有一些特殊性。

  • 不参与实际的内存分配操作。
  • xStartxEnd 都不存储在堆上。

经过该函数初始化后,整个堆的状态可以用下图表示:

1.2 内存分配

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
void * pvPortMalloc( size_t xWantedSize )
{
BlockLink_t * pxBlock, * pxPreviousBlock, * pxNewBlockLink;
static BaseType_t xHeapHasBeenInitialised = pdFALSE;
void * pvReturn = NULL;

vTaskSuspendAll(); /* 挂起任务 */
{
/* 如果这是第一次调用 malloc,则堆将需要初始化以设置空闲块列. */
if( xHeapHasBeenInitialised == pdFALSE )
{
prvHeapInit(); /* 刚才第一小节讲到的函数 */
xHeapHasBeenInitialised = pdTRUE; /* 记录下状态,表示堆空间初始化过 */
}

/* 必须增加所需的大小,以便除了请求的字节数之外,它还可以包含 BlockLink_t 结构 */
if( ( xWantedSize > 0 ) &&
( ( xWantedSize + heapSTRUCT_SIZE ) > xWantedSize ) ) /* 溢出检查 */
{
/* 重新计算所需块大小 */
xWantedSize += heapSTRUCT_SIZE;

/* 字节对齐操作,调整实际申请字节数,并检查溢出 */
if( ( xWantedSize + ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) ) )
> xWantedSize )
{
// 没有溢出
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
configASSERT( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) == 0 );
}
else
{
// 发生溢出,申请的内存不合法
xWantedSize = 0;
}
}
else
{
// 发生溢出,申请的内存不合法
xWantedSize = 0;
}

/* 申请内存合法,且有足够的空闲内存 */
if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) )
{
/* 块按字节顺序存储,从开始(最小)块遍历列表,直到找到足够大小的块。 */
pxPreviousBlock = &xStart;
pxBlock = xStart.pxNextFreeBlock;

/* 遍历空闲块列表,直到找到一个合适大小的块 */
while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
{
pxPreviousBlock = pxBlock;
pxBlock = pxBlock->pxNextFreeBlock;
}

/* 如果找到匹配的内存块,进行内存分配操作 */
if( pxBlock != &xEnd )
{
/* 返回首地址值(跳过heapSTRUCT_SIZE ) */
pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + heapSTRUCT_SIZE );

/* 此块已被分配使用,因此必须从空闲块列表中删除. */
pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;

/* 如果区块大于要求,则可以将其一分为二 */
if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
{
/* 初始化相应的块头部BlockLink_t 结构体 */
pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );

/* 更新相应的块地址大小 */
pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
pxBlock->xBlockSize = xWantedSize;

/* 将分割出的新块插入链表中 */
prvInsertBlockIntoFreeList( ( pxNewBlockLink ) );
}

/* 记录堆剩余字节数 */
xFreeBytesRemaining -= pxBlock->xBlockSize;
}
}

traceMALLOC( pvReturn, xWantedSize );
}
( void ) xTaskResumeAll(); /* 恢复任务 */

return pvReturn;
}

代码看似很长,其实逻辑比较简单,大致分为如下几个步骤:

  1. 调整实际需要申请的内存(内存对齐)
  2. 检测申请字节数是否合法,若合法,则寻找合适的内存块进行分配。
  3. 将分配出去的内存块从链表中移除,若剩余内存大于最小内存块大小,则将剩余的内存块重新添加回链表中。
  4. 记录剩余字节数,返回分配内存空间的地址。

堆在初始状态下,进行一次成功的内存分配后,其状态如下图所示:

1.3 内存释放

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

释放内存,就是把这个块重新插入到空闲数据块链表中。

释放内存后,堆的空间状态如下图所示:

2、总结

heap_1.c 不同,heap_2.c 允许使用 vPortFree() 函数来释放申请的内存,其算法原理是将空闲堆分为若干个大小不等的内存块,并将其按大小排序,使用单向链表连接起来。

申请内存时,便从这些链表中寻找最小可满足申请需求的内存块进行分配。分配过程分为两步,首先将原先的内存块的链表项从链表中删除,其次是对当前内存块进行分割,将多余申请数的那部分内存变为新的链表项重新插入到链表中。释放过程则更为简单,只需要将释放的内存块重新插入到链表中即可。

从源码分析中,可以看出:随着申请释放的次数增加,heap_2.c 将使得内存块被越分越小(内存碎片),这会导致以下两个问题:

  1. 当需要再次请求一个大的内存块时,即使 xFreeBytesRemaining 大于请求的内存块,其也无法进行分配了。
  2. 大量的内存被 BlockLink_t 头部占用,导致堆的利用率降低

那有什么改进办法呢?学习操作系统的时候,提到==将相邻的内存块合并可以缓解碎片化的问题==,在我以前写的 Linux 内存管理(三)之分页机制 中也曾提到过这个。而 FreeRTOSheap_4.c 中实现了内存块合并。

三、heap_3

这个就没什么好说的了,纯粹是封装了 C 标准库中的 mallocfree 函数:

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();

return pvReturn;
}

void vPortFree( void * pv )
{
if( pv )
{
vTaskSuspendAll();
{
free( pv );
traceFREE( pv, 0 );
}
( void ) xTaskResumeAll();
}
}

下面重点看 heap_4 的原理和实现。

四、heap_4

1、源码

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
/* 区块大小不能太小 */
#define heapMINIMUM_BLOCK_SIZE ( ( size_t ) ( xHeapStructSize << 1 ) )

/* 假设 8 位字节 */
#define heapBITS_PER_BYTE ( ( size_t ) 8 )

PRIVILEGED_DATA static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];

/* 定义链表结构。 这用于按内存地址的顺序链接空闲块 */
typedef struct A_BLOCK_LINK
{
struct A_BLOCK_LINK * pxNextFreeBlock; /* 指向列表中的下一个空闲块 */
size_t xBlockSize; /* 空闲块的大小 */
} BlockLink_t;

/* 位于每个已分配内存块开头的结构体的大小,必须正确地进行字节对齐. */
static const size_t xHeapStructSize = ( sizeof( BlockLink_t ) + ( ( size_t ) ( portBYTE_ALIGNMENT - 1 ) ) ) & ~( ( size_t ) portBYTE_ALIGNMENT_MASK );

/* 创建两个列表链接以标记列表的开头和结尾 */
PRIVILEGED_DATA static BlockLink_t xStart, * pxEnd = NULL;

/* 跟踪要分配和释放内存的调用数以及剩余的空闲字节数,但不说明碎片 */
PRIVILEGED_DATA static size_t xFreeBytesRemaining = 0U;
PRIVILEGED_DATA static size_t xMinimumEverFreeBytesRemaining = 0U;
PRIVILEGED_DATA static size_t xNumberOfSuccessfulAllocations = 0;
PRIVILEGED_DATA static size_t xNumberOfSuccessfulFrees = 0;

/* 设置为 size_t 类型的顶部。
* 如果 xBlockAllocatedBit 的值为 0,那么这个内存块还没有被分配;
* 如果 xBlockAllocatedBit 的值为 1,那么这个内存块已经被分配 */
PRIVILEGED_DATA static size_t xBlockAllocatedBit = 0;

1.1 插入链表

内存块合并算法主要是在 prvInsertBlockIntoFreeList() 函数中实现。与 heap_2.c 中按内存块大小顺序插入不同,heap_4.c 是按地址大小的顺序插入,这样便于合并相邻的内存块。插入过程分为以下两步:

  1. 查找链表中链接的下一个内存块地址大于待插入内存块地址的第一个内存块(也就是与待插入内存块相邻的且地址较低的那一个内存块)的地址。
  2. 检测待插入内存块是否能与相邻的内存块合并。
    • 若能与低地址的相邻内存块合并,直接在低地址相邻的内存块大小上加上待插入内存块大小;
    • 若能与高地址的相邻内存块合并或可以同时将高低地址邻近内存块相连,则需要同时调整链表指针与内存块大小。

如上图,要插入 BlockToInsert

  1. 其会先遍历链表寻找 LowAddressAdjacentBlock,然后判断:
    • BlockToInsert 仅能和 LowAddressAdja-centBlock 合并,则将 LowAddressAdjacentBlock 的块大小更改为 LowAddressAdjacentBlockBlockToInsert 大小的和;
    • BlockToInsert 仅能和 HighAddressAdjacentBlock 合并,则用 BlockToInsert 替换 HighAddressAdjacentBlock 在链表中的位置,并修改块大小为两者之和;
    • BlockToInsert 能将 LowAddressAdjacentBlockHighAddressAdjacentBlock 连接成一整块,则从链表中删除 HighAddressAdjacentBlock,并将 LowAddressAdjacentBlock 的块大小变为三者之和;
    • BlockToInsert 是一个孤立的内存块则将其正常的插入到 LowAddressAdjacentBlockHighAddressAdjacentBlock 之间。
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
static void prvInsertBlockIntoFreeList( BlockLink_t * pxBlockToInsert ) /* PRIVILEGED_FUNCTION */
{
BlockLink_t * pxIterator;
uint8_t * puc;

/* 遍历列表,直到找到地址高于要插入的块的块 */
for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock )
{

}

/* 检验待插入内存块是否紧接在低地址邻近内存块后 */
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
{
/* pxEnd 特殊处理 */
pxBlockToInsert->pxNextFreeBlock = pxEnd;
}
}
else
{
/* 如果待插入内存块与高地址邻近内存块不能合并,调整待插入内存块的下一链接为高地址邻近内存块 */
pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
}

/* 如果pxIterator 与pxBlockToInsert 值不等,意味着低地址邻近内存块的内存块与待插入内存块并未合并,
* 因此需要将待插入内存块挂接在pxIterator 后面 */
if( pxIterator != pxBlockToInsert )
{
pxIterator->pxNextFreeBlock = pxBlockToInsert;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}

这样每次插入时,便可自动的合并掉相邻的内存块,以生成更大的内存块。但这并不意味着内存的碎片化问题被解决了。可以看以下的一个示例,当其中的 Used2 被释放,是其仍然会产生内存碎片,除非 Used1Used3 被释放,其才可能被拼接成较大的内存块。

1.2 堆的初始化

heap_4.c 的堆初始化与 heap_2.c 的初始化大同小异,不同点有以下两点:

  1. 其使用 BlockLink_t 结构体成员 xBlockSize 的最高位来标记一个内存块是否被使用,1 表示在使用,0 表示空闲。
  2. 原本的 xEnd 被定义在了堆上,且是堆的尾部,用 pxEnd 指向其地址。

heap_4.c 初始化堆后,堆状态为:

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
static void prvHeapInit( void ) /* PRIVILEGED_FUNCTION */
{
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 用于保存指向 free 块列表中第一项的指针
* void 强制转换用于防止编译器警告. */
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;

/* 初始化第一个内存块,块大小为整个堆空间减去 pxEnd 占用的空间. */
pxFirstFreeBlock = ( void * ) pucAlignedHeap;
pxFirstFreeBlock->xBlockSize = uxAddress - ( size_t ) pxFirstFreeBlock;
pxFirstFreeBlock->pxNextFreeBlock = pxEnd;

/* 只有一个块存在 - 它覆盖了整个可用的堆空间 */
xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;

/* 计算出 size_t 变量中最高位的位置 */
xBlockAllocatedBit = ( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 );
}

1.3 内存的申请

heap_4.c 的内存的申请与释放过程与 heap_2.c 相比除了增加了对 xBlockSize 最高位的处理外,没有太大的不同。

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
void * pvPortMalloc( size_t xWantedSize )
{
BlockLink_t * pxBlock, * pxPreviousBlock, * pxNewBlockLink;
void * pvReturn = NULL;

vTaskSuspendAll();
{
/* 如果这是第一次调用 malloc,则堆将需要初始化以设置空闲块列表 */
if( pxEnd == NULL )
{
prvHeapInit();
}
else
{
mtCOVERAGE_TEST_MARKER();
}

/* 检查最高位是否设置为 1,以确定是否已经分配了内存块 */
if( ( xWantedSize & xBlockAllocatedBit ) == 0 )
{
/* 必须增加所需的大小,以便除了请求的字节数之外,它还可以包含 BlockLink_t 结构 */
if( ( xWantedSize > 0 ) &&
( ( xWantedSize + xHeapStructSize ) > xWantedSize ) ) /* 检查溢出 */
{
xWantedSize += xHeapStructSize;

/* 确保块是对齐的 */
if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
{
/* 没有溢出 */
if( ( xWantedSize + ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) ) )
> xWantedSize )
{
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
configASSERT( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) == 0 );
}
else
{
/* 发生溢出,申请的内存太大 */
xWantedSize = 0;
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
xWantedSize = 0;
}

/* 申请的内存合法,且有空闲内存 */
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 -= 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 );
}
( void ) xTaskResumeAll();

configASSERT( ( ( ( size_t ) pvReturn ) & ( size_t ) portBYTE_ALIGNMENT_MASK ) == 0 );
return pvReturn;
}

1.4 内存的释放

heap_2.c 版本的 vPortFree 类似,不过多了对 xBlockSize 最高位的处理。

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
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 ) );
xNumberOfSuccessfulFrees++;
}
( void ) xTaskResumeAll();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}

2、总结

相比 heap_2.cheap_4.c 可以实现相邻小内存块的合并,在一定程度上缓解内存碎片化的问题。

五、heap_5

1、源码

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
/* 块大小不能太小 */
#define heapMINIMUM_BLOCK_SIZE ( ( size_t ) ( xHeapStructSize << 1 ) )

/* 假设 8 位字节 */
#define heapBITS_PER_BYTE ( ( size_t ) 8 )

/* 定义链表结构,这用于按内存地址的顺序链接空闲块。 */
typedef struct A_BLOCK_LINK
{
struct A_BLOCK_LINK * pxNextFreeBlock; /* 指向列表中的下一个空闲块 */
size_t xBlockSize; /* 空闲块的大小 */
} BlockLink_t;

/* 位于每个已分配内存块开头的结构体的大小必须正确地进行字节对齐 */
static const size_t xHeapStructSize = ( sizeof( BlockLink_t ) + ( ( size_t ) ( portBYTE_ALIGNMENT - 1 ) ) ) & ~( ( size_t ) portBYTE_ALIGNMENT_MASK );

/* 创建两个列表链接来标记列表的开头和结尾 */
static BlockLink_t xStart, * pxEnd = NULL;

/* 跟踪要分配和释放内存的调用数以及剩余的空闲字节数,但不说明碎片 */
static size_t xFreeBytesRemaining = 0U;
static size_t xMinimumEverFreeBytesRemaining = 0U;
static size_t xNumberOfSuccessfulAllocations = 0;
static size_t xNumberOfSuccessfulFrees = 0;

/* 设置为 size_t 类型的顶部。
* 如果 xBlockAllocatedBit 的值为 0,那么这个内存块还没有被分配;
* 如果 xBlockAllocatedBit 的值为 1,那么这个内存块已经被分配 */
static size_t xBlockAllocatedBit = 0;

1.1 堆的初始化

heap_5.c 的堆初始化由 vPortDefineHeapRegions() 这一函数实现,其传入参数是一个具有特定格式的结构体数组。

1
2
3
4
5
6
7
8
9
HeapRegion_t xHeapRegions[] =
{
/* 起始地址为0x80000000 ,大小为0x10000 的内存块 */
{ ( uint8_t * ) 0x80000000UL, 0x10000 },
/* 起始地址为0x90000000 ,大小为0xa0000 的内存块,地址递增排序 */
{ ( uint8_t * ) 0x90000000UL, 0xa0000 },
/* 结束标识符 */
{ NULL, 0 }
};

vPortDefineHeapRegions() 所做的工作就是读取结构体数组中的每一个内存块信息,并将其编入链表中。以上面的参数为例,初始化后堆的状态如下图所示:

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
void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions )
{
BlockLink_t * pxFirstFreeBlockInRegion = NULL, * pxPreviousFreeBlock;
size_t xAlignedHeap;
size_t xTotalRegionSize, xTotalHeapSize = 0;
BaseType_t xDefinedRegions = 0;
size_t xAddress;
const HeapRegion_t * pxHeapRegion;

/* 只能调用一次! */
configASSERT( pxEnd == NULL );

pxHeapRegion = &( pxHeapRegions[ xDefinedRegions ] );

while( pxHeapRegion->xSizeInBytes > 0 )
{
xTotalRegionSize = pxHeapRegion->xSizeInBytes;

/* 确保内存对齐 */
xAddress = ( size_t ) pxHeapRegion->pucStartAddress;
if( ( xAddress & portBYTE_ALIGNMENT_MASK ) != 0 )
{
xAddress += ( portBYTE_ALIGNMENT - 1 );
xAddress &= ~portBYTE_ALIGNMENT_MASK;

/* 调整因对齐而丢失的字节的大小 */
xTotalRegionSize -= xAddress - ( size_t ) pxHeapRegion->pucStartAddress;
}

xAlignedHeap = xAddress;

/* 如果尚未设置 xStart,就设置 xStart */
if( xDefinedRegions == 0 )
{
/* xStart 用于保存指向空闲块列表中第一项的指针 */
xStart.pxNextFreeBlock = ( BlockLink_t * ) xAlignedHeap;
xStart.xBlockSize = ( size_t ) 0;
}
else
{
/* 仅当已将一个区域添加到堆时,才应到达此处 */
configASSERT( pxEnd != NULL );

/* 检查块的传入增加了起始地址 */
configASSERT( xAddress > ( size_t ) pxEnd );
}

/* 记住结束标记在上一个区域中的位置(如果有) */
pxPreviousFreeBlock = pxEnd;

/* 将 pxEnd 插入到区域空间的末尾 */
xAddress = xAlignedHeap + xTotalRegionSize;
xAddress -= xHeapStructSize;
xAddress &= ~portBYTE_ALIGNMENT_MASK;
pxEnd = ( BlockLink_t * ) xAddress;
pxEnd->xBlockSize = 0;
pxEnd->pxNextFreeBlock = NULL;

/* 初始化第一个内存块,块大小为整个堆空间减去 free block 结构所占用的空间。 */
pxFirstFreeBlockInRegion = ( BlockLink_t * ) xAlignedHeap;
pxFirstFreeBlockInRegion->xBlockSize = xAddress - ( size_t ) pxFirstFreeBlockInRegion;
pxFirstFreeBlockInRegion->pxNextFreeBlock = pxEnd;

/* 如果这不是构成整个堆空间的第一个区域,则将上一个区域链接到此区域 */
if( pxPreviousFreeBlock != NULL )
{
pxPreviousFreeBlock->pxNextFreeBlock = pxFirstFreeBlockInRegion;
}

xTotalHeapSize += pxFirstFreeBlockInRegion->xBlockSize;

xDefinedRegions++;
pxHeapRegion = &( pxHeapRegions[ xDefinedRegions ] );
}

xMinimumEverFreeBytesRemaining = xTotalHeapSize;
xFreeBytesRemaining = xTotalHeapSize;

configASSERT( xTotalHeapSize );

/* 计算出 size_t 变量中最高位的位置 */
xBlockAllocatedBit = ( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 );
}

1.2 链表的插入、内存分配、释放

heap_4 的实现,参照前面即可。

2、总结

之前的 heap_1.cheap_2.cheap_4.c 都将堆定义成了一个大数组,这意味着堆的地址必须是连续的,但在实际使用时,有时需要管理两大块或更多的不连续内存,这时便可以使用 heap_5.c 这一实现。其实在之前的 heap_2.cheap_4.c 中,已经实现了对不连续内存的管理,与 heap_4.c 相比,heap_5.c 的改变仅仅是在堆的初始化上,其将多个堆内存块加入了链表而已。


FreeRTOS 内存管理源码解析
http://example.com/2024/09/28/FreeRTOS-内存管理源码解析/
作者
Yu xin
发布于
2024年9月28日
许可协议