目录
FreeRTOS 提供了 5 种不同的内存管理策略以应对不同的应用场景,本章将对这 5 种不同的内存管理策略的实现进行分析。
我参考的源码是:
FreeRTOS-Kernel-10.4.3-LTS-Patch-3\portable\MemMang
,该路径下记录了heap_1.c
、heap_2.c
、heap_3.c
、heap_4.c
、heap_5.c
,讲解会从这 5 个文件的源码入手。
一、heap_1
1、源码讲解
首先,FreeRTOS 将堆定义为一个大数组,并使用变量 xNextFreeByte
记录已用内存大小。
/* 字节对齐堆起始地址可能会丢失几个字节 */
#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()
函数实现如下:
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
定义如下
#define configADJUSTED_HEAP_SIZE ( configTOTAL_HEAP_SIZE - portBYTE_ALIGNMENT )
这里简单粗暴的丢弃掉了对齐字节数个字节,以此来表示堆的起始地址对齐的操作中损失的字节数(最多不会损失掉对齐字节数个字节)
heap_1.c
剩下的还有如下 3 个函数:
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、源码讲解
/* 字节对齐堆起始地址可能会丢失几个字节 */
#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 还定义了一个将内存块插入链表的宏:
/*
* 将数据块插入到可用数据块列表中
* 该列表按数据块大小排序。列表开头的块是小块和列表末尾的块是大块。
*/
#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 堆的初始化
static void prvHeapInit( void )
{
BlockLink_t * pxFirstFreeBlock;
uint8_t * pucAlignedHeap;
/* 确保堆从正确对齐的边界开始
* 和刚才 heap_1 中讲的一模一样,不理解再回去看 */
pucAlignedHeap =</