RT-Thread分析-动态内存堆管理-小内存算法

目录

1 前言

2 动态管理

3 小内存管理算法

4.1 内存块数据头

4.2 统一的API接口

4.3 rt_system_heap_init()

4.4 rt_malloc()

4.5 rt_free()


1 前言

        内存管理模块管理系统的内存资源,它是操作系统的核心模块之一。主要包括内存的初始化、分配以及释放。

        RT-Thread 的内存管理模块的算法总体上可分为两类:动态内存堆管理和静态内存池管理。其中动态内存堆管理又根据具体设备内存大小划分为三种情况:

  • 针对小内存块的分配管理(小内存管理算法)
  • 针对大内存块的分配管理(slab 管理算法)
  • 针对多内存堆的分配情况(memheap 管理算法)

2 动态管理

        动态内存管理,即在内存资源充足的情况下,从系统配置的一块比较大的连续内存,根据用户需求,在这块内存中分配任意大小的内存块。当用户不需要该内存块时,又可以释放回系统供下一次使用。与静态内存相比,动态内存管理的好处是按需分配,缺点是内存池中容易出现碎片。

        需注意的是,因为动态内存管理器要满足多线程情况下的安全分配,会考虑多线程间的互斥问题,所以不能在中断服务例程中分配或释放动态内存块。

3 小内存管理算法

  • 内核启动时,调用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_system_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_START和结束地址HEAD_ADDR_ADDR来初始化内存堆空间
  • 初始化的内存堆只有一整块内存块(包含数据头)和内存堆结束数据头heap_end
  • 通过数据头的双向链表完成链接,由于只含有一个内存块,所以该内存块的链表指针next直接指向heap_end
  • 全局变量lfree始终指向第一个空闲内存块的数据头,方便分配内存时快速从第一个空闲块开始遍历
  • 全局变量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保证其始终指向第一个空闲块
  • 查看相邻内存块是否空闲,尝试进行合并
  • 释放的内存并未清理脏数据

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值