在单片机编程时使用动态内存管理可以有效节约空间,一般的嵌入式系统(如UCOS,FreeRTOS),网络协议栈(如Lwip)都实现了自己的内存管理算法。这篇文章主要是分析了FreeRTOS的heap4的内存管理思路,然后作者按照这个思路实现了这种内存管理算法。
内存管理实现的基本思路就是创建一个很大的数组,然后每次申请内存,就从未使用的空间找到足够大的空间返回给申请者,然后对这些空间进行标记,当内存表中已经找不到足够大的连续的空间返回就申请失败。释放空间则是将这些标记去除,让它变成未使用的空间。
heap4主要通过一个结构体来实现内存的管理
#define BLOCK_USE_FLAG 0x80000000
typedef struct BLOCK_T
{
struct BLOCK_T *nextFreeBlock;//指向下一个空闲内存块
//记录自己的的空闲字节,包括此结构体所占的8个字节 最高位用于表示此块空间是否被使用
uint32_t blockSize;
}BLOCK_T;
初始化代码如下:
static BLOCK_T heapStart,*heapEnd = NULL;
uint8_t heapInit(void)
{
BLOCK_T *firstFreeBlock;
uint8_t *alignHeap;
uint32_t address;
uint32_t totalHeapSize = HEAP_SIZE;
//做一个8字节对齐的处理
address = (uint32_t)heap;
if((address & (uint32_t)0x00000007) != 0)
{
address += 7;
address &= ~(uint32_t)0x00000007;
totalHeapSize -= address - (uint32_t)heap;
}
//得到对齐地址
alignHeap = (uint8_t *) address;
//起始指针记录
heapStart.nextFreeBlock = (void*)alignHeap;
heapStart.blockSize = (uint32_t)0;
//偏移至堆尾
address = (uint32_t)alignHeap + totalHeapSize;
address -= (sizeof(BLOCK_T));
heapEnd = (void*)address;
heapEnd->nextFreeBlock = NULL;
heapEnd->blockSize = 0;
//起始指针赋值 当前堆中唯一空闲内存块
firstFreeBlock = (void*)alignHeap;
firstFreeBlock->blockSize = address - (uint32_t)firstFreeBlock;
firstFreeBlock->nextFreeBlock = heapEnd;
//记录剩余内存数和最小空闲块
freeByteRemaining = firstFreeBlock->blockSize;
minFreeByteRemaining = firstFreeBlock->blockSize;
return True;
}
- 首先获取申请的数组的首地址,然后对其进行一个8字节对齐的处理。为什么是8字节呢,因为刚好结构体总共占了8个字节,这样使用方便。
- 头指针heapStart指向字节对齐的数组首地址,以后这个指针会当作遍历整个空闲链表的根指针。
- 偏移至数组尾部前8个字节,然后在此放置一个heapEnd 的结构体指针。
- 在头部放置firstFreeBlock结构体指针,并计算出总共的空闲字节大小。
- 记录剩余内存数和最小空闲块
此时整个数组的头部和尾部分别放置一个结构体,根指针指向头部。目前只存在一个空闲块。
接下来讲一下分配内存的思路,程序根据要分配的字节从头部遍历到尾部,找到第一个足够字节大小的空闲块分配给申请者,如果还存在剩余的字节则重新建立一个空闲块,并调整链表指向。
void *myMalloc(uint32_t size)
{
BLOCK_T *currentBlock,*previousBlock,*newBlock;
void *returnHeap = NULL;
if(size == 0)
{
return returnHeap;
}
//未初始化
if(heapEnd == NULL)
{
heapInit();
}
//申请字节不能大于0x7FFFFFFF - sizeof(BLOCK_T)
if((size & BLOCK_USE_FLAG) == 0)
{
//在申请字节的基础上增加一个记录结构体
size += sizeof(BLOCK_T);
}
//字节对齐处理
if((size & (uint32_t)0x00000007) != 0)
{
size += 7;
size &= ~(uint32_t)0x00000007;
}
if(size <= freeByteRemaining)
{
previousBlock = &heapStart;
currentBlock = heapStart.nextFreeBlock;
while(( currentBlock->blockSize < size ) && currentBlock->nextFreeBlock != NULL)
{
previousBlock = currentBlock;
currentBlock = currentBlock->nextFreeBlock;
}
if(currentBlock != heapEnd)
{
//地址偏移
returnHeap = (void*)( (uint8_t *)(previousBlock->nextFreeBlock)+ sizeof(BLOCK_T)) ;
previousBlock->nextFreeBlock = currentBlock->nextFreeBlock;
//分配后内存块还有剩余 创建新的内存块
if((currentBlock->blockSize - size) > MIN_BLOCK_SIZE )
{
newBlock = (void*) (((uint8_t*)currentBlock) + size);
newBlock->blockSize = currentBlock->blockSize - size;
currentBlock->blockSize = size;
//将新内存块加入链表
insertBLock(newBlock);
}
//更新剩余堆大小和最小空闲块
freeByteRemaining -= currentBlock->blockSize;
if(freeByteRemaining < minFreeByteRemaining)
{
minFreeByteRemaining = freeByteRemaining;
}
//标记此块内存已被使用
currentBlock->blockSize |= BLOCK_USE_FLAG;
currentBlock->nextFreeBlock = NULL;
}
}
return returnHeap;
}
- 对分配的字节数进行调整,首先加上一个结构体大小的字节数,每一个分配出去的内存块头部都需要加上BLOCK_T的结构体,来管理此块内存,便于内存的释放。然后做字节对齐的处理。
- 遍历整个链表,找到第一个拥有足够字节大小的空闲块。如果找到即可将此空闲块的地址偏移(sizeof(BLOCK_T))个字节返回给申请者,
- 判断此内存块分配后是否还存在剩余字节,如果有则新建一个空闲内存快,记录剩余字节数,并将它重新插入到链表中。
- 更新剩余堆大小和最小空闲块和标记内存被使用
接下来对分配内存时使用的insertBLock函数进行分析。此函数将新的空闲块插入至链表中。并且如果存在可合并的项(地址上连续)就进行空闲块的合并
static void insertBLock(BLOCK_T *newBlock)
{
BLOCK_T *iterator;
uint8_t *p;
//找到插入内存块的上一块内存 按地址排序
for(iterator = &heapStart; iterator->nextFreeBlock < newBlock; iterator = iterator->nextFreeBlock)
{
}
p = (uint8_t*)iterator;
//可以合并
if((p + iterator->blockSize) == (uint8_t*)newBlock)
{
iterator->blockSize += newBlock->blockSize;
newBlock = iterator;
}
p = (uint8_t*)newBlock;
//可以和后面合并
if((p + newBlock->blockSize) == (uint8_t*) iterator->nextFreeBlock)
{
if(iterator->nextFreeBlock != heapEnd)
{
newBlock->blockSize += iterator->nextFreeBlock->blockSize;
newBlock->nextFreeBlock = iterator->nextFreeBlock->nextFreeBlock;
}
else
{
newBlock->nextFreeBlock = heapEnd;
}
}
else
{
newBlock->nextFreeBlock = iterator->nextFreeBlock;
}
if(iterator != newBlock)
{
iterator->nextFreeBlock = newBlock;
}
}
- 对链表进行遍历,找到待插入内存块的上一块空闲内存块(按地址排序),判断此空闲内存块与要插入的内存块是否连续,如果连续则合并。
- 同样将要插入的内存块与下一块内存块进行比较,如果地址连续则合并。
最后调整链表指针指向,插入完成。
最后对内存释放函数进行分析
void myFree(void *freeBlock)
{
uint8_t *p = (uint8_t*)freeBlock;
BLOCK_T *blockLink;
if(p != NULL)
{
p -= sizeof(BLOCK_T);
blockLink = (void*) p;
if((blockLink->blockSize & BLOCK_USE_FLAG)!=0)
{
if(blockLink->nextFreeBlock == NULL)
{
//标记此内存块未使用
blockLink->blockSize &= ~BLOCK_USE_FLAG;
//更新剩余内存
freeByteRemaining += blockLink->blockSize;
insertBLock(blockLink);
}
}
}
}
- 偏移一个sizeof(BLOCK_T)的字节长度,得到要释放的内存块原本的结构体数据(申请的字节).
- 如果此内存块确实被使用了(根据使用标志判断),清除使用标志,更新剩余内存大小
将这块内存重新插入至空闲内存块的链表中
至此内存管理所需的两个函数malloc()和free()就完成了。