计算机系统中,变量、中间数据一般存放在 RAM 中,只有在实际使用时才将它们从 RAM 调入到 CPU 中进行运算。一些数据需要的内存大小需要在程序运行过程中根据实际情况确定,这就要求系统具有对内存空间进行动态管理的能力,在用户需要一段内存空间时,向系统申请,系统选择一段合适的内存空间分配给用户,用户使用完毕后,再释放回系统,以便系统将该段内存空间回收再利用。
RT-Thread 操作系统在内存管理上,根据上层应用及系统资源的不同,有针对性地提供了不同的内存分配管理算法。下面介绍一下针对小内存块的分配管理(小内存管理算法)。
注意:因为内存堆管理器要满足多线程情况下的安全分配,会考虑多线程间的互斥问题,所以请不要在中断服务例程中分配或释放动态内存块。因为它可能会引起当前上下文被挂起等待。
小内存管理算法
1.概述
小内存管理算法是一个简单的内存分配算法。初始时,它是一块大的内存。当需要分配内存块时,将从这个大的内存块上分割出相匹配的内存块,然后把分割出来的空闲内存块还回给堆管理系统中。每个内存块都包含一个管理用的数据头,通过这个头把使用块与空闲块用双向链表的方式链接起来,如下图所示:
每个内存块(不管是已分配的内存块还是空闲的内存块)都包含一个数据头,其中包括:
1)magic:变数(或称为幻数),它会被初始化成 0x1ea0(即英文单词 heap),用于标记这个内存块是一个内存管理用的内存数据块;变数不仅仅用于标识这个数据块是一个内存管理用的内存数据块,实质也是一个内存保护字:如果这个区域被改写,那么也就意味着这块内存块被非法改写(正常情况下只有内存管理器才会去碰这块内存)。
2)used:指示出当前内存块是否已经分配。
3)next:指向当前内存块的下一个内存块。
4)prev:指向当前内存块的上一个内存块。
注意:事实上,next与prev这两个字段保存的是目的地址的一个偏移量,偏移的基地址是整个内存堆空间的起始地址。
2. 数据结构描述
下面代码是rtthread中对内存块的结构描述
#define MIN_SIZE 12 //内存申请分配空间的最小值
#define HEAP_MAGIC 0x1ea0 //变数
struct heap_mem
{
/* magic and used flag */
rt_uint16_t magic;
rt_uint16_t used;
rt_size_t next, prev;
};
3. 函数实现
我们需要调用的内存堆管理相关的系统函数有4个,如下所示:
void rt_system_heap_init(void *begin_addr, void *end_addr);//在使用内存分配之前,该函数必须被调用,完成内存堆的初始化。
void *rt_malloc(rt_size_t size); //内存分配函数
void *rt_calloc(rt_size_t count, rt_size_t size); //申请指定大小且初始值为0的内存空间
void rt_free(void *rmem); //内存释放函数
在释放内存中,会有一个内存节点合并功能函数,被rt_free调用,系统编程无需关心。
static void plug_holes(struct heap_mem *mem); //内存释放时节点合并
下面我们来看一下小内存分配算法代码实现(添加注释),其中会忽略部分断言及打印输出代码。
static rt_uint8_t *heap_ptr; //指向内存堆起始地址
static struct heap_mem *lfree; //指向最低可用地址空闲内存块
static struct rt_semaphore heap_sem; //信号量
static rt_size_t mem_size_aligned; //可分配内存总大小
#ifdef RT_MEM_STATS
static rt_size_t used_mem, max_mem; //用于统计内存使用量及最大使用量
#endif
//初始化系统内存堆,begin_addr:起始地址,end_addr:结束地址。
void rt_system_heap_init(void *begin_addr, void *end_addr)
{
struct heap_mem *mem;
//中断中不能使用内存分配函数,如果中断中使用,输出错误信息
RT_DEBUG_NOT_IN_INTERRUPT;
//计算可用内存堆大小(总内存减去2个内存块结构体大小)
if ((end_align > (2 * SIZEOF_STRUCT_MEM)) &&
((end_align - 2 * SIZEOF_STRUCT_MEM) >= begin_align))
{
mem_size_aligned = end_align - begin_align - 2 * SIZEOF_STRUCT_MEM;
}
else
{
return;
}
//指向内存堆起始地址,用于后续rt_malloc申请内存时定位内存堆起始地址
heap_ptr = (rt_uint8_t *)begin_align;
//在内存堆起始处放置一个内存块结构体
mem = (struct heap_mem *)heap_ptr;
mem->magic = HEAP_MAGIC; //内存块标志
mem->next = mem_size_aligned + SIZEOF_STRUCT_MEM; //下一个内存块偏移量
mem->prev = 0; //上一个内存块偏移为0
mem->used = 0; //设置未使用标志
//初始化内存堆的最后一个内存块信息,记录最后一个内存块信息
heap_end = (struct heap_mem *)&heap_ptr[mem->next];
heap_end->magic = HEAP_MAGIC; //内存块标志
heap_end->used = 1; //设置未使用标志
heap_end->next = mem_size_aligned + SIZEOF_STRUCT_MEM; //下一个内存块偏移量
heap_end->prev = mem_size_aligned + SIZEOF_STRUCT_MEM; //上一个内存块偏移量
//初始化内存分配时使用的互斥信号量
rt_sem_init(&heap_sem, "heap", 1, RT_IPC_FLAG_FIFO);
//指向最低可用地址空闲内存块
lfree = (struct heap_mem *)heap_ptr;
}
//内存申请函数
void *rt_malloc(rt_size_t size)
{
rt_size_t ptr, ptr2;
struct heap_mem *mem, *mem2;
//中断中不能使用内存分配函数,如果中断中使用,输出错误信息
RT_DEBUG_NOT_IN_INTERRUPT;
if (size == 0)
return RT_NULL;
//将size修正为内存对齐字节数的整数倍
size = RT_ALIGN(size, RT_ALIGN_SIZE);
//申请长度大于内存堆总大小,失败
if (size > mem_size_aligned)
{
return RT_NULL;
}
//申请空间至少为MIN_SIZE_ALIGNED
if (size < MIN_SIZE_ALIGNED)
size = MIN_SIZE_ALIGNED;
//获取互斥信号量
rt_sem_take(&heap_sem, RT_WAITING_FOREVER);
//从lfree开始遍历,找出第一个长度大于size的空闲内存块
for (ptr = (rt_uint8_t *)lfree - heap_ptr;
ptr < mem_size_aligned - size;
ptr = ((struct heap_mem *)&heap_ptr[ptr])->next)
{
//获得一个内存块的起始地址
mem = (struct heap_mem *)&heap_ptr[ptr];
//若该内存块未用,且其空间不小于(用户请求大小+系统结构体mem)
if ((!mem->used) && (mem->next - (ptr + SIZEOF_STRUCT_MEM)) >= size)
{
//到这里,则满足了从该内存块中分配空间的条件,但是接下来必须要判断是将该
//内存块全部分配给用户,还是截取其中的一部分给用户,判断标准为:若做截取;
//判断剩下的部分能否组成一个最小的内存块,即是否能剩下
//SIZEOF_STRUCT_MEM+MIN_SIZE_ALIGNED的大小。
if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >=
(size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED))
{
//到这里,需要将内存块截取一部分给用户,剩下的重新组成为一个空闲内存块
//分配后,剩余空间起始处的偏移量
ptr2 = ptr + SIZEOF_STRUCT_MEM + size;
//剩余空间起始处为mem结构
mem2 = (struct heap_mem *)&heap_ptr[ptr2];
mem2->magic = HEAP_MAGIC;
mem2->used = 0; //标志为未用
//修改新内存块的偏移量
mem2->next = mem->next;
mem2->prev = ptr;
//修改当前分配的内存块偏移量
mem->next = ptr2;
mem->used = 1; //分配给用户的内存块标志为已用
//若新空闲块不是链表中最后一个,将其最后一个空闲快的prev指针指向它
if (mem2->next != mem_size_aligned + SIZEOF_STRUCT_MEM)
{
((struct heap_mem *)&heap_ptr[mem2->next])->prev = ptr2;
}
#ifdef RT_MEM_STATS
//用于统计当前内存使用量及最大使用量
used_mem += (size + SIZEOF_STRUCT_MEM);
if (max_mem < used_mem)
max_mem = used_mem;
#endif
}
else
{
//直接分配,不用截取,标志为已用
mem->used = 1;
#ifdef RT_MEM_STATS
//用于统计当前内存使用量及最大使用量
used_mem += mem->next - ((rt_uint8_t*)mem - heap_ptr);
if (max_mem < used_mem)
max_mem = used_mem;
#endif
}
//内存块标志
mem->magic = HEAP_MAGIC;
//到这里分配完毕,调整指针lfree
if (mem == lfree)
{
//只有lfree指向的内存块被分配出去了,才更新lfree,查找下一个lfree
while (lfree->used && lfree != heap_end)
lfree = (struct heap_mem *)&heap_ptr[lfree->next];
}
//释放信号量
rt_sem_release(&heap_sem);
//分配成功,返回可用起始区域
return (rt_uint8_t *)mem + SIZEOF_STRUCT_MEM;
}
}
//若到这里,说明所有内存块都不能满足要求,释放信号量
rt_sem_release(&heap_sem);
//返回空指针
return RT_NULL;
}
内存释放函数
void rt_free(void *rmem)
{
struct heap_mem *mem;
//中断中不能使用内存分配函数,如果中断中使用,输出错误信息
RT_DEBUG_NOT_IN_INTERRUPT;
//释放地址空间为空,则直接返回
if (rmem == RT_NULL)
return;
//判断释放内存是否合法,不合法则返回
if ((rt_uint8_t *)rmem < (rt_uint8_t *)heap_ptr ||
(rt_uint8_t *)rmem >= (rt_uint8_t *)heap_end)
{
return;
}
//合法,则偏移一定字节数,拿到内存块信息结构体mem
mem = (struct heap_mem *)((rt_uint8_t *)rmem - SIZEOF_STRUCT_MEM);
/* protect the heap from concurrent access */
rt_sem_take(&heap_sem, RT_WAITING_FOREVER);
//标志为未用
mem->used = 0;
mem->magic = HEAP_MAGIC;
//若释放的地址比lfree低,则更新lfree
if (mem < lfree)
{
lfree = mem;
}
#ifdef RT_MEM_STATS
//更新内存使用量
used_mem -= (mem->next - ((rt_uint8_t*)mem - heap_ptr));
#endif
检查并执行合并操作
plug_holes(mem);
//释放信号量
rt_sem_release(&heap_sem);
}
//内存节点合并函数
static void plug_holes(struct heap_mem *mem)
{
struct heap_mem *nmem;
struct heap_mem *pmem;
//查找相邻的下一个内存块
nmem = (struct heap_mem *)&heap_ptr[mem->next];
if (mem != nmem && nmem->used == 0 && (rt_uint8_t *)nmem != (rt_uint8_t *)heap_end)
{
//若下一个内存块空闲且不是系统最后一个空闲内存块,若lfree指向nmem,则将lfree指向mem
if (lfree == nmem)
{
lfree = mem;
}
//执行合并操作,更新偏移量,即在链表中删除nmem
mem->next = nmem->next;
((struct heap_mem *)&heap_ptr[nmem->next])->prev = (rt_uint8_t *)mem - heap_ptr;
}
//查找相邻的上一个内存块
pmem = (struct heap_mem *)&heap_ptr[mem->prev];
if (pmem != mem && pmem->used == 0)
{
//前一个内存块存在且未用,若lfree指向mem,则将lfree指向pmem,合并后mem将消失
if (lfree == mem)
{
lfree = pmem;
}
//执行合并操作,即在链表中删除mem
pmem->next = mem->next;
((struct heap_mem *)&heap_ptr[mem->next])->prev = (rt_uint8_t *)pmem - heap_ptr;
}
}
内存管理的表现主要体现在内存的分配与释放上,小型内存管理算法可以用以下例子体现出来。
如下图所示的内存分配情况,空闲链表指针 lfree 初始指向 32 字节的内存块。当用户线程要再分配一个 64 字节的内存块时,但此 lfree 指针指向的内存块只有 32 字节并不能满足要求,内存管理器会继续寻找下一内存块,当找到再下一块内存块,128 字节时,它满足分配的要求。因为这个内存块比较大,分配器将把此内存块进行拆分,余下的内存块(52 字节)继续留在 lfree 链表中,如下图分配 64 字节后的链表结构所示。
另外,在每次分配内存块前,都会留出 12 字节数据头用于 magic、used 信息及链表节点使用。返回给应用的地址实际上是这块内存块 12 字节以后的地址,前面的 12 字节数据头是用户永远不应该碰的部分(注:12 字节数据头长度会与系统对齐差异而有所不同)。
释放时则是相反的过程,但分配器会查看前后相邻的内存块是否空闲,如果空闲则合并成一个大的空闲内存块。
遇到的问题: 同事使用rt_malloc申请空间,然后对申请的空间进行操作,操作过程中数组越界,导致其他地方申请内存释放出错,释放时mem->magic的值不是0x1ea0,释放内存失败。
解决思路:mem->magic的值不是0x1ea0,应该是其他地方越界操作,导致当前需要释放的内存块amgic值导致错误,打印出当前释放的内存地址,将申请内存的地址打印出来,查看当前释放地址附近的内存操作是否成在越界。