这篇文章最初发布在RT-Thread官方论坛中,最近准备整理放到博客中来让更多人一块儿探讨学习。算法
2012年9月28日星期五
前言:
母语能力有限微信
概述:
这篇文字和你们分享一下今晚对RT-Thread的内存管理——小内存管理算法的理解。如有不对的地方请你们丢砖。
正文:
分析的源码文件mem.c
主要的几个函数:
一、rt_system_heap_init
二、rt_malloc
三、rt_free
四、plug_holes
armcc编译器中的初始化内存的方式:数据结构
rt_system_heap_init((void*)&Image$$RW_IRAM1$$ZI$$Limit, (void*)STM32_SRAM_END); 函数
接触RTT半年以来,对这里的第一个参数是又爱又恨,爱它的神秘怪僻,恨它的怪僻神秘。布局
extern int Image$$RW_IRAM1$$ZI$$Limit; 学习
从这里能够看得出它是火星人仍是地球人。不错这样的声明咱们大概能知道的是仅仅只是一个变量而已。不过它的定义在何处?我纠结到昨天晚上才见到它真实的面貌(这还多亏aozima的指点)。这是一个连接器导出的符号,表明ZI段的结束(科普:假如芯片的RAM有32Kbyte,那么一般咱们的程序没有占用彻底部的RAM,且ARMCC的连接器会将ZI段排在RAM已使用的RAM区域中的最后面。因此ZI段的后面就是程序未能使用到的RAM区域)。关于这种奇怪的符号均可以在MDK的帮助文档中找到!
第二个参数就是整个RAM的结束地址。因此从这两个参数上能够知道传递进去的是内存的管理区域。
rt_system_heap_init
这个函数是对堆进行初始的过程,堆的大小由传进来的起始地址(begin_addr)和结束地址(end_addr)决定。固然若是咱们还要考虑内存对齐,这样一来,咱们能使用的堆大小就不必定彻底等于(end_addr - begin_addr)了。优化
rt_uint32_t begin_align = RT_ALIGN((rt_uint32_t)begin_addr, RT_ALIGN_SIZE);
rt_uint32_t end_align = RT_ALIGN_DOWN((rt_uint32_t)end_addr, RT_ALIGN_SIZE);
这两句进一步对传递的起始地址后结束地址进行对齐操做了,有可能在起始地址上日后偏移几个字节以保持在对齐的地址上,也有可能在结束地址的基础上往前偏移几个字节以保持在对齐地址上。因此这里就有可能被扣掉一点点的内存。可是这每每是很小的一点点,一般小到几个字节,也许恰好是一个都没有被扣掉。ui
if ((end_align > (2 * SIZEOF_STRUCT_MEM)) && ((end_align - 2 * SIZEOF_STRUCT_MEM) >= begin_align)) { /* calculate the aligned memory size */ mem_size_aligned = end_align - begin_align - 2 * SIZEOF_STRUCT_MEM; }
这部分是计算最大可分配的内存区域,?????,为何说是最大‘可’分配的内存区域呢?由于还将被继续扣除一部份内存,这部分被扣除的内存是用来放置内存管理算法所要用的数据结构。越精巧的算法固然是效率更高,浪费的也最少。
RTT扣掉了2个数据结构的尺寸,由于RTT有一个heap_ptr和一个heap_end这两个标记,分别表示堆的起始和结束。mem_size_aligned就是整个可操做的区域尺寸。
RTT的内存管理用到的数据结构很是的精简。spa
struct heap_mem { /* magic and used flag */ rt_uint16_t magic; rt_uint16_t used; rt_size_t next, prev; };
总共占用了2+2+2xcpu字长(8个字节)=12个字节。其中magic字段用以标记一个合法的内存块,used字段标记这个堆内存块是否被分配,next字段指向当前这个堆内存块的末尾后1个字节(不说成是下一个可分配块,是由于也许next指向了末尾),prev指向当前这个内存块的前一个有效内存块的数据结构起始处,不然指向自身(最前面那个内存块)。因此能够看出这个设计思路是把整个内存块用链表的形式组织在一块儿。固然咱们使用的next和prev只是相对于起始地址heap_ptr的偏移量,而不是真正在一般列表中看到的指针。
接着标记了第一块可分配的内存块,这个内存块把mem_size_aligned个字节大小划分出来,留下SIZEOF_STRUCT_MEM个字节的大小用来恰好放置heap_end,在前SIZEOF_STRUCT_MEM个字节中也就是最前面的SIZEOF_STRUCT_MEM个字节用来放置heap_ptr(堆内存的头指针)。其中第一个可分配点就是从heap_ptr开始的,heap_prt的next指向了heap_end(也就是mem_size_aligned + SIZEOF_STRUCT_MEM),大体的初始时候的布局以下图所示:设计
最后让lfree指向当前活动的可分配的内存块,这样能够迅速找到最进被释放的内存块上。通常只要以前分配的内存块被释放后,lfree就尽可能分配靠前的内存区域,也就是优先从前日后寻找可分配的内存块。
rt_malloc
首先对申请的尺寸作对齐处理,可是这个对齐操做只会有可能比实际分配的尺寸要大一点。
for (ptr = (rt_uint8_t *)lfree - heap_ptr; ptr < mem_size_aligned - size; ptr = ((struct heap_mem *)&heap_ptr[ptr])->next)
循环查找从当前lfree所指向的区域开始,查找一块可以知足区域的内存块。
if ((!mem->used) && (mem->next - (ptr + SIZEOF_STRUCT_MEM)) >= size)
当这块内存区域没有被使用,且这块内存区域扣掉头部数据结构SIZEOF_STRUCT_MEM后的大小知足所需分配的大小那么就可使用这个内存块来分配。
if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >= (size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED))
若是这个即将被分配的内存块除了能分配给当前尺寸后,还余下的有足够的空间可以组织成下一个可分配点的话,那么将对余下的部分组织成下一个可分配点。也就是将多余的空间划出来组织成一个新的可分配区域而后连接到链表中。
不然把当前这个内存块标记为已分配状态used=1。若是当前被分配的内存块是lfree指向的内存块,那么调整lfree,以让其指向下一个可分配的点。
if (mem == lfree) { /* Find next free block after mem and update lowest free pointer */ while (lfree->used && lfree != heap_end) lfree = (struct heap_mem *)&heap_ptr[lfree->next]; RT_ASSERT(((lfree == heap_end) || (!lfree->used))); }
以后将获得的内存点返回,这个时候不是返回这个可分配点的其实地址,而是返回这个可分配点的起始地址偏移一个数据结构尺寸的地址,由于每一个可分配的内存块都在其前面有一个用于管理内存所需用到的一个数据结构(链表,魔数,used等信息)。
return (rt_uint8_t *)mem + SIZEOF_STRUCT_MEM;
若是当前这个循环从lfree开始查找,第一次没有找到合适的内存块,那么继续日后找,循环的判断条件是只要next小于mem_size_aligned - size就或许能找到一个合适尺寸的内存块。不然将分配失败,返回NULL。这里虽然分配的size小于可分配的区域,可是因为屡次分配释放等过程产生了内存碎片,真正连续可用的空间并不是这么多。
rt_free
调用这个函数是释放以前分配的堆内存。因此使用rt_malloc函数返回的内存块地址若是须要释放的时候,就须要调用rt_free。因为在rt_malloc和rt_free的外面使用者来讲,是不知道这个内存地址的前面有一个管理数据结构的,因此在rt_free的时候须要往前偏移SIZEOF_STRUCT_MEM个字节,用以找到数据结构的起点。
mem = (struct heap_mem *)((rt_uint8_t *)rmem - SIZEOF_STRUCT_MEM);
接着将这个内存块标记为未被分配的状态。
mem->used = 0; mem->magic = 0;
若是释放的内存地址在lfree的前面,那么将lfree指向当前释放的内存块上。
if (mem < lfree) { /* the newly freed struct is now the lowest */ lfree = mem; }
最后调用plug_holes函数进行一些附加的优化操做,这个优化操做是必不可少以及体现整个内存分配算法核心价值的地方(先卖个关子,等我慢慢道来)。
plug_holes
这个函数的做用就是合并当前这个内存点的先后紧接着的已经被释放的内存块。这样一来就能够解决内存碎片问题。
nmem = (struct heap_mem *)&heap_ptr[mem->next];
if (mem != nmem && nmem->used == 0 && (rt_uint8_t *)nmem != (rt_uint8_t *)heap_end) { /* if mem->next is unused and not end of heap_ptr, combine mem and mem->next */ if (lfree == nmem) { lfree = mem; } mem->next = nmem->next; ((struct heap_mem *)&heap_ptr[nmem->next])->prev = (rt_uint8_t *)mem - heap_ptr; }
这是看其后面的内存点是否能够被合并,合并其后的内存块的时候只须要调整当前内存块的next值。其次还须要调整当前内存块后的内存块的下一个内存块的prev字段(这里有点绕口,其实就比如普通列表中的p->next->next->prev),使其指向当前自己的内存块(比如p->next->next->prev = p)。
pmem = (struct heap_mem *)&heap_ptr[mem->prev];
if (pmem != mem && pmem->used == 0) { /* if mem->prev is unused, combine mem and mem->prev */ if (lfree == mem) { lfree = pmem; } pmem->next = mem->next; ((struct heap_mem *)&heap_ptr[mem->next])->prev = (rt_uint8_t *)pmem - heap_ptr; }
这是合并当前内存块前面的已被释放的内存块,先取出前面的内存块,而后调整前一个内存块的next指向当前内存块的next所指的地方,接着将当前内存块的下一个内存块的prev字段指向当前内存款的前一个内存块(比如p->next->prev = p->prev)。
这样两大步就能够将当前释放的内存块的先后两个已经被释放的内存块给合并成一个大的内存块。从而避免了碎片问题,提升了内存分配的可靠性。
2012年9月28日3时16分46秒
感谢各位网友的支持,若是想获得最新的文章资讯请关注个人微信公众号:鹏城码夫 (微信号:rocotona)