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

1 前言

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

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

  • 针对内存比较小的分配管理(小内存管理算法)
  • 针对内存比较大的分配管理(slab 管理算法)
  • 针对多内存堆即内存不连续的分配情况(memheap 管理算法)

        之前介绍过内存池管理和小内存管理,本次介绍的slab管理其实是综合了上述两种方式。

2 slab管理算法

        基于DragonFlyBSD版本,针对嵌入式系统优化的内存分配算法,主要是去掉了其中的对象构造及析构过程,只保留了纯粹的缓冲型的内存池算法。

        主要包含两部分:

1)页分配器

  • 申请大内存时,直接通过页分配器分配内存,其管理方式和小内存管理算法类似
  • 创建zone空间时,使用页分配器申请内存

2)zone管理

  • 将内存堆划分为多个同样大小的zone空间,每个zone的大小在32k ~ 128k字节之间,分配器会在堆初始化时根据堆的大小自动调整。
  • 每个zone再划分为多个同样大小的chunk内存块。申请小内存时,按申请大小找到合适的chunk分配出去。其管理方式和内存池管理类似。不过zone是动态管理的,只有在需要时才会分配zone,当zone完全空闲时也会释放。

3 页分配器

  • 页分配器操作最小单元是页,每页长度为4096字节。每次申请或者释放的内存单位是页数
  • 实际分配时,按申请需求,将内存空间分割为多个内存块,每个内存块首部都有一个数据头rt_page_head,用于链接和管理内存块。该数据头仅管理空闲内存块,分配出去的内存块数据头部分会被用户使用并覆盖(见后续分析)。
  • 内核维护一个空闲块链表头rt_page_list,始终指向第一个空闲块的数据头

3.1 rt_page_head数据头

  • 用于空闲块的首部,链接并管理空闲块
  • 已分配出去的内存块无需数据头管理,其首部空间也是做为内存给用户使用
struct rt_page_head
{
    //链接下一个空闲内存块(不一定链接的是相邻内存块)
    struct rt_page_head *next;   
    //该内存块的page数   
    rt_size_t page; 

    //无实际含义,占用空间,使rt_page_head大小为1个page
    char dummy[RT_MM_PAGE_SIZE - (sizeof(struct rt_page_head *) + sizeof(rt_size_t))];
};

3.2 rt_page_init()

static void rt_page_init(void *addr, rt_size_t npages)
{
    //完成初始化,整个内存做为一个空闲内存块,并将rt_page_list指向之
    rt_page_list = RT_NULL;
    rt_page_free(addr, npages);
}

3.3 rt_page_alloc()

  • 该函数通过信号量进行线程互斥,可能导致挂起,因此不可以在中断上下文中调用
  • 遍历空闲链表头rt_page_list,找到合适大小的空闲块进行分割或者直接分配出去
  • 直接返回内存块的起始地址,数据头此时就失去作用,该空间直接给用户当作申请的内存使用
void *rt_page_alloc(rt_size_t npages)
{
    struct rt_page_head *b, *n;
    struct rt_page_head **prev;

    //1 获取信号量,避免多线程同时操作内存
    rt_sem_take(&heap_sem, RT_WAITING_FOREVER);
    
    //2 遍历空闲内存块链表
    for (prev = &rt_page_list; (b = *prev) != RT_NULL; prev = &(b->next))
    {
        //2.1 空闲内存块足够大,则进行分割,将剩余部分创建为新的空闲内存块
        if (b->page > npages)
        {
            /* splite pages */
            n       = b + npages;
            n->next = b->next;
            n->page = b->page - npages;
            *prev   = n;
            break;
        }

        //2.2 空闲内存块大小正好,则直接分配出去,移出空闲内存块链表(就是将前后内存块相连)
        if (b->page == npages)
        {
            *prev = b->next;
            break;
        }
    }

    //3 释放信号量,允许其他线程进行内存操作
    rt_sem_release(&heap_sem);

    return b;
}

3.4 rt_page_free()

  • 该函数通过信号量进行线程互斥,可能导致挂起,因此不可以在中断上下文中调用
  • 释放内存时,会尝试合并相邻的空闲块;如果无法合并则做为单独的空闲内存块,链接到rt_page_list链表中
  • 由于分配出去的内存块未保留数据头空间,所以释放的时候传入的参数除了地址,还需要page数。
void rt_page_free(void *addr, rt_size_t npages)
{
    struct rt_page_head *b, *n;
    struct rt_page_head **prev;
    
    //1 释放地址必须是页的起始地址
    RT_ASSERT((rt_ubase_t)addr % RT_MM_PAGE_SIZE == 0);

    n = (struct rt_page_head *)addr;

    //2 获取信号量,避免多线程同时操作内存
    rt_sem_take(&heap_sem, RT_WAITING_FOREVER);


    //3 遍历空闲内存块链表,看释放的内存是否与某个空闲内存块相邻,尝试进行合并
    for (prev = &rt_page_list; (b = *prev) != RT_NULL; prev = &(b->next))
    {
        //3.1 该空闲内存块相邻于需释放的内存块前面
        if (b + b->page == n)
        {
            if (b + (b->page += npages) == b->next)
            {
                b->page += b->next->page;
                b->next  = b->next->next;
            }
            
            //疑问: 如果不满足3.1.1直接退出不做任何处理?
            goto _return;
        }

        //3.2 该空闲内存块相邻于需释放的内存块后面,则进行合并
        if (b == n + npages)
        {
            n->page = b->page + npages;
            n->next = b->next;
            *prev   = n;

            goto _return;
        }

        //3.3 空闲内存块起始地址大于需释放的内存块结束地址,再遍历下去也不可能相邻了,直接退出遍历
        if (b > n + npages)
            break;
    }

    //4 释放内存块无法和相邻空闲内存块合并,则直接新建数据头,做为单独的一个空闲内存块,并链接进空闲内存块链表
    n->page = npages;
    n->next = b;
    *prev   = n;


_return:
    //5 释放信号量
    rt_sem_release(&heap_sem);
}

3.5 申请释放流程图

4 zone管理

  • 按一定设定规划多个zone,每个zone划分多个相同大小的chunk,但不同zone的chunk大小会不一样(8,16,...,2048)。具体规划如下

  • 申请内存时,从 zone array 链表表头数组中找到相应的 zone 链表。如果这个链表是空的,则向页分配器分配一个新的zone,然后从 zone 中返回第一个空闲chunk。如果链表非空,则将该zone中空闲的chunk分配一个出去
  • 释放内存时,内核会找到该内存对应的zone,将该chunk归还给zone;如果该zone中chunk全部空闲,则会移出zoneary[zi]链表,加入到zone_free链表中。另外,zone_free链表下空闲zone数量大于设定值时,会通过页分配器释放链表下第一个空闲zone

4.1 struct slab_zone

  • 用于管理zone的数据结构,放置在zone空闲的首部
typedef struct slab_zone
{
    //chunks空闲数量
    rt_int32_t  z_nfree;  
        
    //chunks总数量
    rt_int32_t  z_nmax;         

    //用于zone直接链接
    struct slab_zone *z_next;   
    
    //指向chunk内存块起始地址
    rt_uint8_t  *z_baseptr;     

    //当前初始可分配chunk的序号。怎么理解:
    //假设新创建的zone共有32个chunk,开始分配时会按z_uindex序号顺序分配出去chunk。z_uindex从0递增到32
    //当释放chunk时,释放的chunk是放回*z_freechunk链表头管理
    //再次申请chunk时,还是先按z_uindex序号分配,直至z_uindex增加到最大值32。
    //后续申请chunk时就会去*z_freechunk链表头查找
    rt_int32_t  z_uindex;  
    
    //chunk的大小                                                                                                                                                                                                                                                                                                                                                                                                    
    rt_int32_t  z_chunksize;    

    //zone序号(通过slab_zone *zone_array[NZONES]管理)
    rt_int32_t  z_zoneindex;    
    
    //空闲chunk链表头
    slab_chunk  *z_freechunk;   /* free chunk list */
} slab_zone;

4.2 memusage/btokup()

  • 通过全局变量memusage[]来记录每个page信息
struct memusage
{
    //page使用类型
    rt_uint32_t type: 2 ;       
    
    //如果page用于zone,该page相对于zone起始的偏移量
    rt_uint32_t size: 30;       /* pages allocated or offset from zone */
};
static struct memusage *memusage = RT_NULL;

//通过内存地址计算page序号
#define btokup(addr)    \
    (&memusage[((rt_ubase_t)(addr) - heap_start) >> RT_MM_PAGE_BITS])

4.3 zone_array[NZONES]

//每一个zone_array[index]是一个zone链表,链接着相同index的zone
static slab_zone *zone_array[NZONES];

4.4 zone_free & zone_free_cnt

//空闲zone链表头
static slab_zone *zone_free;            

//zone_free链表下空闲zone数量
static int zone_free_cnt;

4.5 rt_system_heap_init()

  • 通过内存堆大小,动态计算出合理的zone大小
  • 页分配器初始化
  • 为memusage[npages]申请内存
void rt_system_heap_init(void *begin_addr, void *end_addr)
{
    //1 内存堆起始地址和结束地址按page对齐
    heap_start = RT_ALIGN((rt_ubase_t)begin_addr, RT_MM_PAGE_SIZE);
    heap_end   = RT_ALIGN_DOWN((rt_ubase_t)end_addr, RT_MM_PAGE_SIZE);
    
    //2 初始化信号量,用于内存申请释放时线程互斥
    rt_sem_init(&heap_sem, "heap", 1, RT_IPC_FLAG_PRIO);
    
    //3 页分配器初始化 
    limsize = heap_end - heap_start;
    npages  = limsize / RT_MM_PAGE_SIZE;
    rt_page_init((void *)heap_start, npages);
    
    //4 依据内存堆大小,计算出合理的zone大小
    zone_size = ZALLOC_MIN_ZONE_SIZE;
    while (zone_size < ZALLOC_MAX_ZONE_SIZE && (zone_size << 1) < (limsize / 1024))
        zone_size <<= 1;
        
    //5 计算出限定值,但申请内存大小大于该值时,直接使用页分配器分配内存
    zone_limit = zone_size / 4;
    
    //6 申请内存用于memusage[npages],记录每个page使用信息
    limsize  = npages * sizeof(struct memusage);
    limsize  = RT_ALIGN(limsize, RT_MM_PAGE_SIZE);
    memusage = rt_page_alloc(limsize / RT_MM_PAGE_SIZE);
}

4.6 rt_malloc()

  • 申请内存大小大于zone_limit(该值在初始化时计算)时,直接使用页分配器分配内存
  • 通过申请内存大小,找到合适大小的zone_array[zi]链表
  • 如果zone_array[zi]链表非空,则从该链表下的zone分配一个chunk
  • 如果zone_array[zi]为空,则去zone_free链表下寻找未释放的空闲zone。如果有则使用该zone分配一个chunk
  • 如果zone_free链为空,说明无空闲zone。此时需要使用页分配器新创建一个zone,从该zone下分配一个chunk;同时将该zone链接到zone_array[zi]链表中
void *rt_malloc(rt_size_t size)
{
    //1 如果申请内存比较大,则直接使用页分配器
    if (size >= zone_limit)
    {
        //1.1 申请大小按page对齐
        size = RT_ALIGN(size, RT_MM_PAGE_SIZE);

        //1.2 使用page分配内存
        chunk = rt_page_alloc(size >> RT_MM_PAGE_BITS);
        if (chunk == RT_NULL)
            return RT_NULL;

        //1.3 记录page使用情况
        kup = btokup(chunk);
        kup->type = PAGE_TYPE_LARGE;    //类型
        kup->size = size >> RT_MM_PAGE_BITS;    //长度
        
        goto done;
    }
    
    //2 获取信号量,避免多线程同时操作内存 
    rt_sem_take(&heap_sem, RT_WAITING_FOREVER);
    
    //3 通过申请内存大小,计算出使用哪个zone的chunk,返回的就是该zone的序号
    zi = zoneindex(&size);
    
    //4 如果对应zone_array[zi]存在
    if ((z = zone_array[zi]) != RT_NULL)
    {
        //4.1 最后一个chunk分配出去,则将zone从链表头为zone_array[zi]的链表中移除
        if (--z->z_nfree == 0)
        {
            zone_array[zi] = z->z_next;
            z->z_next = RT_NULL;
        }  
        
        //4.2 获取空闲chunk
        //假设新创建的zone共有32个chunk,开始分配时会按z_uindex序号顺序分配出去chunk。z_uindex从0递增到32
        //当释放chunk时,释放的chunk是放回z->z_freechunk链表头管理
        //再次申请chunk时,还是先按z_uindex序号分配,直至z_uindex增加到最大值32。
        //后续申请chunk时就会去*z_freechunk链表头查找                                                                                                                                                                                                
        if (z->z_uindex + 1 != z->z_nmax)
        {
            //4.2.1 初始chunk分配,按序号逐一分配
            z->z_uindex = z->z_uindex + 1;
            chunk = (slab_chunk *)(z->z_baseptr + z->z_uindex * size);
        }
        else
        {
            //4.2.2 初始chunk都分配完了,则从空闲chunk链表头获取
            chunk = z->z_freechunk;
            /* remove this chunk from list */
            z->z_freechunk = z->z_freechunk->c_next;
        } 
        
        goto done;                  
    }
    
    //5 对应zone_array[zi]不存在,则先从空闲zone链表中寻找,没有则使用页分配器分配新的zone
    {
        //5.1 如果有空闲zone则直接使用
        if ((z = zone_free) != RT_NULL)
        {
            // 将该空闲zone从链表头zone_free中移除
            zone_free = z->z_next;
            -- zone_free_cnt;
        }  
        //5.2 没有空闲zone则使用页分配器
        else
        {
            z = rt_page_alloc(zone_size / RT_MM_PAGE_SIZE);

            //记录每个page信息
            for (off = 0, kup = btokup(z); off < zone_page_cnt; off ++)
            {
                kup->type = PAGE_TYPE_SMALL;
                kup->size = off;
                kup ++;
            }
        }   
        
        //5.3 zone空间起始部分用于slab_zone结构,所以计算chunk部分的偏移量
        off = sizeof(slab_zone);
        if ((size | (size - 1)) + 1 == (size << 1))
            off = (off + size - 1) & ~(size - 1);
        else
            off = (off + MIN_CHUNK_MASK) & ~MIN_CHUNK_MASK;    
            
        //5.4 填充slab_zone信息
        z->z_magic     = ZALLOC_SLAB_MAGIC;
        z->z_zoneindex = zi;
        z->z_nmax      = (zone_size - off) / size;
        z->z_nfree     = z->z_nmax - 1;
        z->z_baseptr   = (rt_uint8_t *)z + off;
        z->z_uindex    = 0;
        z->z_chunksize = size;    
        
        //5.5 分配出去第一个空闲chunk
        chunk = (slab_chunk *)(z->z_baseptr + z->z_uindex * size);  
        
        //5.6 将该zone链接到链表头为zone_array[zi]的链表中
        z->z_next = zone_array[zi];
        zone_array[zi] = z;            
    }
           
}

4.7 rt_free()

  • 如果之前是通过页分配器直接分配的内存,对应使用页分配器释放并退出函数
  • 将释放内存归还给对应zone。如果此时zone所有chunk都是空闲,会将该zone从zone_array[zi]链表移除,链接到zone_free链表中
  • 当zone_free链表下的空闲zone数量大于设定值,则会通过页分配器释放一个zone的内存
void rt_free(void *ptr)
{
    //1 获取page信息
    kup = btokup((rt_ubase_t)ptr & ~RT_MM_PAGE_MASK);
    
    //2 PAGE_TYPE_LARGE类型通过页分配器释放
    if (kup->type == PAGE_TYPE_LARGE)
    {
        rt_sem_take(&heap_sem, RT_WAITING_FOREVER);
        //2.1 清空page信息
        size = kup->size;
        kup->size = 0;
        rt_sem_release(&heap_sem);

        //2.2 释放内存        
        rt_page_free(ptr, size);
        return;
    }
    
    //3 信号量用于线程保护
    rt_sem_take(&heap_sem, RT_WAITING_FOREVER);
    
    //4 计算出释放内存是在哪个zone
    z = (slab_zone *)(((rt_ubase_t)ptr & ~RT_MM_PAGE_MASK) -
                      kup->size * RT_MM_PAGE_SIZE);
    
    //5 将该chunk链接到空闲chunk链表中                  
    chunk          = (slab_chunk *)ptr;
    chunk->c_next  = z->z_freechunk;
    z->z_freechunk = chunk;
    
    //6 该zone原先是全满,此时释放一个chunk,需要将其重新放回链表头为zone_array[z->z_zoneindex]的链表中
    //对应rt_malloc()中的4.1
    if (z->z_nfree++ == 0)
    {
        z->z_next = zone_array[z->z_zoneindex];
        zone_array[z->z_zoneindex] = z;
    }
    
    //7 如果该zone所有chunk空闲,并且zone_array[z->z_zoneindex]链表下还有其他可分配的zone
    //将zone从zone_array[z->z_zoneindex]链表下移除,加入到zone_free链表中
    //zone_free链表下空闲zone的数量较多时,则通过页分配器释放内存
    if (z->z_nfree == z->z_nmax &&
        (z->z_next || zone_array[z->z_zoneindex] != z))
    {
        //7.1 从zone_array[z->z_zoneindex]链表下移除
        for (pz = &zone_array[z->z_zoneindex]; z != *pz; pz = &(*pz)->z_next)
            ;
        *pz = z->z_next;   
        
        //7.2 加入到zone_free链表中
        z->z_next = zone_free;
        zone_free = z;
        ++ zone_free_cnt;      
        
        //7.3 如果zone_free链表下的空闲zone数量大于设定值,则通过页分配器释放一个zone
        if (zone_free_cnt > ZONE_RELEASE_THRESH)
        {
            //7.3.1 取出zone_free链表下第一个zone
            z         = zone_free;
            zone_free = z->z_next;
            -- zone_free_cnt;

            //7.3.2 清空该zone下page信息
            for (i = 0, kup = btokup(z); i < zone_page_cnt; i ++)
            {
                kup->type = PAGE_TYPE_FREE;
                kup->size = 0;
                kup ++;
            }

            //7.3.2 页分配器释放内存
            rt_page_free(z, zone_size / RT_MM_PAGE_SIZE);
            return;
        }    
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值