freertos三大管理:内存管理、通信管理、任务管理之内存管理
目录
freertos三大管理:内存管理、通信管理、任务管理之内存管理
freertos的内存管理 其中heap4和heap5为嵌入式运用freertos内存管理的重点,其他可以了解一下
1. heap_1.c:静态分配,不支持释放
想象你有一个固定大小的钱包,里面装着一定数量的钱。一开始你就知道钱包里有多少钱,而且这些钱一旦花出去就不能再收回来。在 FreeRTOS 里,heap_1.c 就像这个钱包,在系统启动时就确定了内存池的大小,分配出去的内存就不能再释放。这种方式很简单,就像花钱不用考虑还钱一样,不用操心内存回收和碎片问题,但缺点也很明显,不够灵活,一旦钱花完了就没办法再买东西,也就是内存分配完了就不能再进行新的分配。它适合那种一开始就知道要花多少钱,而且不需要退钱的场景,比如任务创建后就一直运行,不会被删除的情况。
2. heap_2.c:最佳匹配算法,碎片化严重
这就好比你有一堆不同大小的积木,当你需要一块特定大小的积木时,会在这堆积木里仔细找,找到最接近你需要大小的那块。在 heap_2.c 里,当你请求内存分配时,它会从空闲内存块列表中找到最匹配你需求大小的空闲块给你。而且你用完积木后可以把它放回积木堆里(也就是支持释放内存)。但是,随着你不断地拿积木和放积木,积木堆会变得越来越乱,小的积木块到处都是,当你需要一块大积木时,可能就找不到合适的了。在 FreeRTOS 中,这就意味着会产生很多内存碎片,当需要分配大内存块时可能会失败。这种方式适合每次需要的积木大小差不多,而且拿放积木比较频繁的场景。
3. heap_3.c:封装标准库 malloc/free(线程安全)
想象你去银行存钱和取钱,银行有一套自己的管理系统,你只需要按照银行的规定去存钱和取钱就行。在 heap_3.c 里,它就像去银行存钱取钱,直接调用标准 C 库的 malloc 和 free 函数来分配和释放内存,这两个函数就像是银行的管理系统。而且,为了防止多个人同时去银行办理业务时出现混乱,heap_3.c 会使用互斥锁来保证线程安全。这种方式很灵活,就像银行可以处理各种复杂的存钱取钱业务一样,能满足复杂的内存分配和释放需求,但前提是底层平台得有可靠的标准 C 库。
4. heap_4.c:首次适应算法 + 空闲块合并
还是以积木为例,当你需要一块积木时,你不会像 heap_2.c 那样仔细找最匹配的,而是从积木堆的最上面开始找,找到第一个大小够的积木就拿走。这就是 heap_4.c 里的首次适应算法,简单又快。而且,当你把积木放回积木堆时,如果发现旁边的积木也是空闲的,就会把它们拼在一起变成一个更大的积木。在 FreeRTOS 中,这就是空闲块合并功能,能有效减少内存碎片。这种方式适合经常拿放积木,而且有时候需要拿大积木的场景。
没耐心的直接看总结:首次假设需要使用一块内存20字节,会先寻找一个足够大小的内存被用于分配,当持续使用的时候,有很多断断续续的碎片,那我free其中某些用过的内存,那么相邻的内存碎片会合并
5. heap_5.c:heap_4 增强版,支持非连续内存区域
假如你有好几个不同的盒子来装积木,每个盒子里都有一些积木,而且这些盒子可能放在不同的地方。heap_5.c 就像管理这些分散在不同盒子里的积木,它继承了 heap_4.c 的首次适应算法和空闲块合并功能,能把不同盒子里的空闲积木拼在一起。但是,管理这些盒子比较麻烦,一开始你得告诉它每个盒子在哪里,里面有多少积木,也就是初始化过程比较复杂。这种方式适合系统里有多个分散的内存区域,需要灵活管理内存的场景。
总结:heap5就是对多个非连续的内存块使用heap4管理方式。
内存管理的代码实现
typedef struct BlockLink_t {
size_t xBlockSize; // 当前块大小(含头部)
struct BlockLink_t *pxNextFree; // 空闲链表指针
} BlockLink_t;
static BlockLink_t xStart, *pxEnd = NULL;
xBlockSize成员:该成员用来记录当前内存块的大小,这里的大小包含了内存块本身以及用于管理该内存块的头部信息。在进行内存分配和释放操作时,系统需要知道每个内存块的大小,以此判断是否满足分配需求,或者在释放内存时能正确处理内存块的合并等操作。
pxNextFree 成员:这是一个指向 BlockLink_t 类型的指针,其作用是构建一个单向链表。每个空闲内存块都有一个 BlockLink_t 类型的头部,通过 pxNextFree 指针可以将各个空闲内存块连接起来,形成一个空闲链表。系统在进行内存分配时,就能通过遍历这个链表来查找合适的空闲内存块。
xStart 变量:它是一个 BlockLink_t 类型的静态变量,作为空闲链表的头节点。头节点本身并不代表实际的空闲内存块,主要起到标记链表起始位置的作用。在进行内存分配时,系统从 xStart 开始遍历空闲链表,查找满足需求的空闲内存块。
pxEnd 变量:这是一个指向 BlockLink_t 类型的静态指针,初始值为 NULL。它用于标记空闲链表的末尾。在内存管理过程中,当系统需要知道空闲链表是否遍历到末尾时,就会检查是否到达 pxEnd 所指向的位置。同时,在初始化空闲链表时,会将 pxEnd 指向链表的最后一个节点。
整体作用
通过 BlockLink_t 结构体和 xStart、pxEnd 变量,系统能够构建一个有效的空闲内存块管理机制。在进行内存分配时,从 xStart 开始遍历空闲链表,利用 xBlockSize 筛选出合适的空闲内存块;在释放内存时,将释放的内存块插入到空闲链表中,并根据 xBlockSize 进行空闲块的合并操作,以减少内存碎片。这种设计使得内存管理更加高效和灵活。
内存分配函数
分配内存时,会从空闲链表的头部开始遍历,找到第一个大小足够的空闲块,若该空闲块比所需大小大很多,会将其分割成两部分,一部分用于分配,另一部分作为新的空闲块留在链表中。
c
void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxPreviousBlock, *pxBlock, *pxNewBlockLink;
void *pvReturn = NULL;
/* 确保请求的大小是对齐的 */
if( xWantedSize > 0 )
{
xWantedSize += heapSTRUCT_SIZE;
if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
{
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
}
vTaskSuspendAll();
{
pxPreviousBlock = &xStart;
pxBlock = xStart.pxNextFree;
/* 遍历空闲链表 */
while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFree != NULL ) )
{
pxPreviousBlock = pxBlock;
pxBlock = pxBlock->pxNextFree;
}
/* 找到合适的空闲块 */
if( pxBlock != pxEnd )
{
pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFree ) + heapSTRUCT_SIZE );
/* 如果空闲块比所需大小大很多,进行分割 */
if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
{
pxNewBlockLink = ( BlockLink_t * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );
pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
pxBlock->xBlockSize = xWantedSize;
pxNewBlockLink->pxNextFree = pxBlock->pxNextFree;
pxPreviousBlock->pxNextFree = pxNewBlockLink;
}
else
{
pxPreviousBlock->pxNextFree = pxBlock->pxNextFree;
}
}
}
( void ) xTaskResumeAll();
}
return pvReturn;
}
步骤总结
初始化:把整个可用内存区域当作一个大的空闲块,插入到以 xStart 为头节点的空闲链表中,pxEnd 指向链表的末尾。
分配内存:从 xStart 开始遍历空闲链表,找到第一个大小足够的空闲块。
分割空闲块:若空闲块比所需大小大很多,将其分割成两部分,一部分用于分配,另一部分作为新的空闲块留在链表中。
更新链表:把分配出去的内存块从链表中移除,更新链表指针。
通过以上步骤,就能利用 BlockLink_t 结构体和 xStart、pxEnd 实现内存分配。
内存释放函数
在上述使用 BlockLink_t 结构体构建的空闲内存块链表基础上,实现内存释放函数的核心思路是将释放的内存块重新插入到空闲链表中,并尝试与相邻的空闲块进行合并,以减少内存碎片。以下是一个简化版的内存释放函数实现及详细解释:
内存释放函数实现
c
void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink, *pxPreviousLink, *pxNextLink;
if( pv != NULL )
{
/* 定位到释放内存块对应的 BlockLink_t 结构体 */
puc -= heapSTRUCT_SIZE;
pxLink = ( BlockLink_t * ) puc;
vTaskSuspendAll();
{
pxPreviousLink = &xStart;
pxNextLink = xStart.pxNextFree;
/* 找到合适的插入位置,使链表按地址顺序排列 */
while( ( pxNextLink != pxEnd ) && ( pxNextLink < pxLink ) )
{
pxPreviousLink = pxNextLink;
pxNextLink = pxNextLink->pxNextFree;
}
/* 尝试与前一个空闲块合并 */
if( ( ( uint8_t * ) pxPreviousLink ) + pxPreviousLink->xBlockSize == ( uint8_t * ) pxLink )
{
pxPreviousLink->xBlockSize += pxLink->xBlockSize;
pxLink = pxPreviousLink;
}
else
{
pxPreviousLink->pxNextFree = pxLink;
}
/* 尝试与后一个空闲块合并 */
if( ( ( uint8_t * ) pxLink ) + pxLink->xBlockSize == ( uint8_t * ) pxNextLink )
{
pxLink->xBlockSize += pxNextLink->xBlockSize;
pxLink->pxNextFree = pxNextLink->pxNextFree;
}
else
{
pxLink->pxNextFree = pxNextLink;
}
}
( void ) xTaskResumeAll();
}
}
代码解释
1. 定位释放内存块对应的 BlockLink_t 结构体
c
puc -= heapSTRUCT_SIZE;
pxLink = ( BlockLink_t * ) puc;
在分配内存时,返回给用户的指针是跳过了 BlockLink_t 结构体的,所以在释放内存时,需要将指针减去 heapSTRUCT_SIZE(BlockLink_t 结构体的大小),以定位到该内存块对应的 BlockLink_t 结构体。
2. 找到合适的插入位置
c
pxPreviousLink = &xStart;
pxNextLink = xStart.pxNextFree;
while( ( pxNextLink != pxEnd ) && ( pxNextLink < pxLink ) )
{
pxPreviousLink = pxNextLink;
pxNextLink = pxNextLink->pxNextFree;
}
为了方便后续的空闲块合并操作,需要将释放的内存块按地址顺序插入到空闲链表中。这里通过遍历链表,找到合适的插入位置,使得 pxPreviousLink 指向插入位置的前一个节点,pxNextLink 指向插入位置的后一个节点。
3. 尝试与前一个空闲块合并
c
if( ( ( uint8_t * ) pxPreviousLink ) + pxPreviousLink->xBlockSize == ( uint8_t * ) pxLink )
{
pxPreviousLink->xBlockSize += pxLink->xBlockSize;
pxLink = pxPreviousLink;
}
else
{
pxPreviousLink->pxNextFree = pxLink;
}
检查释放的内存块是否与前一个空闲块相邻,如果相邻,则将两个块合并为一个更大的空闲块,并更新 pxPreviousLink 的大小。否则,将释放的内存块插入到 pxPreviousLink 之后。
4. 尝试与后一个空闲块合并
c
if( ( ( uint8_t * ) pxLink ) + pxLink->xBlockSize == ( uint8_t * ) pxNextLink )
{
pxLink->xBlockSize += pxNextLink->xBlockSize;
pxLink->pxNextFree = pxNextLink->pxNextFree;
}
else
{
pxLink->pxNextFree = pxNextLink;
}
同样地,检查释放的内存块(或合并后的块)是否与后一个空闲块相邻,如果相邻,则将两个块合并为一个更大的空闲块,并更新 pxLink 的大小和指针。否则,将 pxLink 的 pxNextFree 指向 pxNextLink。
5. 恢复任务调度
c
( void ) xTaskResumeAll();
最后,恢复任务调度,允许其他任务继续执行。
总结
通过以上步骤,内存释放函数将释放的内存块重新插入到空闲链表中,并尝试与相邻的空闲块进行合并,从而减少内存碎片,提高内存利用率。