首先讲一下RT-Thread的API的三个函数
1、rt_malloc()
填入你需要的字(32bit,4个字节)个数,然后返回分配后空间的指针
void *rt_malloc(rt_size_t size)
2、rt_realloc()
用于给已经被分配内存补充分配内存
3、rt_free()
所有的动态内存分配使用完后,都要使用这个函数分配
小内存管理算法
- 内核启动时,调用rt_system_heap_init()给内存堆规划空间
- 内存堆按分配需求会拆分为多个内存块,每一个内存块在首部空间都包含一个数据头,该数据头的双向链表将内存块链接在一起。内存堆末端使用一个数据头heap_end作为结尾标记
- lfree作为全局变量,时钟指向第一个空闲块,方便分配内存块时快速从第一个空闲块作为入口开始遍历整个内存块链表;全局变量heap_ptr时钟指向内存堆起始地址
- 当用户调用rt_malloc()需要申请内存时,系统会从lfree链表头开始遍历,寻找到合适大小空闲内存块,如果该内存块足够大,会将该内存块进行拆分,剩余的部分单独作为一个新的空闲内存块,同时将lfree指向新的第一块空闲内存块
4.1内存块数据头
struct heap_mem
{
//幻数,设定固定值0x1ea0用于标记控制块
rt_uint16_t magic;
//标记该内存块是否空闲
rt_uint16_t used;
//类似双向链表指针,不过该值记录的是地址偏移量
rt_size_t next, prev;
//记录哪个线程申请的该内存块
#ifdef RT_USING_MEMTRACE
rt_uint8_t thread[4];
#endif /* RT_USING_MEMTRACE */
};
- 每一个内存块的前面部分空间作为数据头,记录该数据库的相关信息以及链接上下内存块
- 另外内存堆结尾处会占用一个数据头空间,作为heap_end
4.2统一的API接口
内核对于三种动态内存堆管理算法是使用一致的API接口定义。典型使用如下:
- 在内核启动时初始化系统堆内存空间:rt_system_heap_init()
- 申请任意大小的动态内存:rt_malloc()
- 释放动态内存 rt_free()
4.3 rt_syatem_heap_init()
void rt_system_heap_init(void *begin_addr, void *end_addr)
{
//1 将内存堆的起始和结束地址按设定进行字节对齐(有的CPU必须对齐访问内存,有的CPU可以非对齐访问内存但效率低)
rt_ubase_t begin_align = RT_ALIGN((rt_ubase_t)begin_addr, RT_ALIGN_SIZE);
rt_ubase_t end_align = RT_ALIGN_DOWN((rt_ubase_t)end_addr, RT_ALIGN_SIZE);
//2 初始化的内存堆由于未分配,只有一个内存块(包含一个数据头)以及内存堆末端的数据头(用于标记内存堆的结束)
// 所以一个内存堆最小空间需容纳下两个数据头
if ((end_align > (2 * SIZEOF_STRUCT_MEM)) &&((end_align - 2 * SIZEOF_STRUCT_MEM) >= begin_align))
{
//2.1 计算初始堆实际数据区的大小。(后续数据区会越来越小,因为每新分配一个内存块需要一个数据头的空间)
mem_size_aligned = end_align - begin_align - 2 * SIZEOF_STRUCT_MEM;
}
else
{
//2.2 内存堆空间太小初始化失败
rt_kprintf("mem init, error begin address 0x%x, and end address 0x%x\n",
(rt_ubase_t)begin_addr, (rt_ubase_t)end_addr);
return;
}
//3 初始化第一个内存块的数据头
heap_ptr = (rt_uint8_t *)begin_align;
mem = (struct heap_mem *)heap_ptr;
mem->magic = HEAP_MAGIC;
//3.1 该值是记录下一个内存块的地址偏移量,由于未做任何分配,整个数据区都属于第一个内存块,所以直接指向heap_end
mem->next = mem_size_aligned + SIZEOF_STRUCT_MEM;
mem->prev = 0;
mem->used = 0;
//4 初始化内存堆结束数据头,heap_end是个全局变量,可用于内存堆使用时的溢出判断等
heap_end = (struct heap_mem *)&heap_ptr[mem->next];
heap_end->magic = HEAP_MAGIC;
heap_end->used = 1;
//4.1 双向链表都指向自己本身heap_end
heap_end->next = mem_size_aligned + SIZEOF_STRUCT_MEM;
heap_end->prev = mem_size_aligned + SIZEOF_STRUCT_MEM;
//5 初始化信号量,用于内存申请释放时的线程互斥
rt_sem_init(&heap_sem, "heap", 1, RT_IPC_FLAG_PRIO);
//6 lfree是个全局链表头,始终指向第一个空闲块
lfree = (struct heap_mem *)heap_ptr;
}
- 用户通过设定起始地址HEAP_ADDR_ATART和结束地址HEAD_ADDR_END来初始化内存堆空间
- 初始化的内存堆只有一整块内存块(包含数据头)和内存堆结束数据头heap_end
- 通过数据头的双向链表完成链接,由于只有一个内存块,所以该内存块的链表指针next直接指向heap_end
- 全局变量lfree时钟指向低于i个空闲内存块的数据头,方便分配内存时快速从第一个空闲块开始遍历
- 全局变量heap_ptr始终指向内存堆起始地址
4.4 rt_malloc()
void *rt_malloc(rt_size_t size)
{
//1 申请内存大小按字节对齐
size = RT_ALIGN(size, RT_ALIGN_SIZE);
//2 mem_size_aligned在rt_system_heap_init()初始化的,表示可申请的最大空间
if (size > mem_size_aligned)
{
RT_DEBUG_LOG(RT_DEBUG_MEM, ("no memory\n"));
return RT_NULL;
}
//3 限定申请内存的最小值(这个值设定有什么测试依据吗)
if (size < MIN_SIZE_ALIGNED)
size = MIN_SIZE_ALIGNED;
//4 获取信号量,如果有其他线程在释放或者申请内存块,则线程挂起,避免操作冲突
rt_sem_take(&heap_sem, RT_WAITING_FOREVER);
//5(lfree始终指向第一个空闲块,heap_ptr始终指向内存堆起始地址)
// 从第一个空闲块开始逐一遍历
for (ptr = (rt_uint8_t *)lfree - heap_ptr;
ptr < mem_size_aligned - size;
ptr = ((struct heap_mem *)&heap_ptr[ptr])->next)
{
//5.1 获取内存块的数据头
mem = (struct heap_mem *)&heap_ptr[ptr];
//5.2 如果该内存未使用并且空间最够大,否则则遍历下一个数据块
if ((!mem->used) && (mem->next - (ptr + SIZEOF_STRUCT_MEM)) >= size)
{
//5.2.1 如果该空闲块分配出申请所需的空间,还有足够多余的空间(MIN_SIZE_ALIGNED)
//则将剩余空间新建立空闲内存块,并与其他空闲块链接起来
if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >=
(size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED))
{
//剩余空间新建立空闲内存块
ptr2 = ptr + SIZEOF_STRUCT_MEM + size;
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;
//这个是判断mem2->next指向的空间是否堆结束标记,如果不是就需要将其链接(堆结束标记数据头是链接自己本身)
if (mem2->next != mem_size_aligned + SIZEOF_STRUCT_MEM)
{
((struct heap_mem *)&heap_ptr[mem2->next])->prev = ptr2;
}
}
else
{
//5.2.2 如果剩余空间比较小则不拆分,直接将整个内存块分配出去(虽然会存在一定内存浪费)
mem->used = 1;
}
//5.2.3 将lfree重新指向新的第一个空闲内存块
if (mem == lfree)
{
while (lfree->used && lfree != heap_end)
lfree = (struct heap_mem *)&heap_ptr[lfree->next];
}
//5.2.4 释放信号量,并返回内存块地址(跳过数据头)
rt_sem_release(&heap_sem);
return (rt_uint8_t *)mem + SIZEOF_STRUCT_MEM;
}
}
rt_sem_release(&heap_sem);
return RT_NULL;
}
- 该函数通过信号量进行线程互斥,肯定导致挂起,因此不可以在中断上下文中调用
- 内核会限定申请内存的最大长度mem_size_aligned和最小长度MIN_SIZE_ALIGNED
- 从lfree指向的第一块空闲内存块开始遍历,找到空间足够的空闲块。将该内存块按一定条件进行分割,剩余的空间做为新的空闲内存块。
4.5 rt_free()
void rt_free(void *rmem)
{
//1 获取该内存块的数据头
mem = (struct heap_mem *)((rt_uint8_t *)rmem - SIZEOF_STRUCT_MEM);
//2 获取信号量,如果有其他线程在释放或者申请内存块,则线程挂起,避免操作冲突
rt_sem_take(&heap_sem, RT_WAITING_FOREVER);
//3 更新数据头
mem->used = 0;
mem->magic = HEAP_MAGIC;
//4 更新lfree,保证lfree始终指向第一个空闲块
if (mem < lfree)
{
lfree = mem;
}
//5 查看相邻内存块是否空闲,如果是的则尝试进行合并
plug_holes(mem);
rt_sem_release(&heap_sem);
}
- 整个过程会通过信号量进行线程互斥,可能导致挂起
- 更新内存块为空闲块,更新lfree保证其始终指向第一个空闲块
- 查看相邻内存块是否空闲,尝试进行合并