rtthread之小内存管理算法

       计算机系统中,变量、中间数据一般存放在 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 字节后的链表结构所示。

                                   小内存管理算法链表结构示意图 1

                   小内存管理算法链表结构示意图 2

另外,在每次分配内存块前,都会留出 12 字节数据头用于 magic、used 信息及链表节点使用。返回给应用的地址实际上是这块内存块 12 字节以后的地址,前面的 12 字节数据头是用户永远不应该碰的部分(注:12 字节数据头长度会与系统对齐差异而有所不同)。

释放时则是相反的过程,但分配器会查看前后相邻的内存块是否空闲,如果空闲则合并成一个大的空闲内存块。

遇到的问题: 同事使用rt_malloc申请空间,然后对申请的空间进行操作,操作过程中数组越界,导致其他地方申请内存释放出错,释放时mem->magic的值不是0x1ea0,释放内存失败。

解决思路:mem->magic的值不是0x1ea0,应该是其他地方越界操作,导致当前需要释放的内存块amgic值导致错误,打印出当前释放的内存地址,将申请内存的地址打印出来,查看当前释放地址附近的内存操作是否成在越界。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值