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;
}
}
}