TLFS 内存分配算法详解
1. DSA 背景介绍
动态内存管理算法 DSA,Dynamic storage allocation。RTOS 一般情况下动态内存使用malloc申请分配,但是存在两个缺陷:
- 由于分配算法的复杂度,分配的时间不定;
- 在不断申请、释放的过程中,容易因为内存对齐而产生碎片化内存;
这两个缺陷在实时操作系统中是不允许的,所以操作系统必须提供一套有效、合理、时间可确定的动态内存管理机制。
既然传统malloc存在两个缺陷,那就抱着解决这两个缺陷的目的出发,去建立一套更适合于嵌入式系统的动态内存管理系统。目前有两种不同的解决方案:
- 动态内存堆管理算法(mmheap),用于不定长分配
- 静态内存池管理算法(mmblk),用于定长分配
1.1 mmheap
mmheap 一般采用 TLSF 算法。TLSF 全称 Two-Level Segregated Fit memory allocator,两级隔离Fit内存分配器,是一款通用的动态内存分配器,专门设计用于满足实时要求。tlsf source code。具有以下特点:
- malloc,free,realloc,memalign的算法复杂度变为O(1);
- 每次分配的开销极低(4字节);
- 低碎片化
- 支持动态添加和删除内存池区域
- TLSF主要采用两级位图(Two-Level Bitmap)与分级空闲块链表(Segregated Free List)的数据结构管理动态内存池(memory pool)以及其中的空闲块(free blocks),用Good-Fit的策略进行分配。
需要注意:
- TLSF算法分配速度不一定快,只是说能保证分配的时间是个常数(malloc不能保证);
- TLSF也叫多内存堆管理算法,支持动态增加或者删除多块不连续的内存,将它们作为一个内存堆使用;
1.2 mmblk
静态内存池就是将一块内存划分为n个大小相等的块,用户可以动态的申请、释放一个块,假装在使用动态内存。
2. TLFS 原理
2.1 存储结构
TLFS 采用两级链表的形式来加快查找:
第一级链表 First List (简称
fl
)。第一层将空闲内存块的大小根据2的幂进行分类,如(16、32、64…),第一级的索引值fli
决定了这一级内存块的大小,范围为 [2i,2(i+1)] ;第一级索引值计算:
fli
= min(l
o
g
2
(
m
e
m
o
r
y
p
o
o
l
s
i
z
e
)
log_{2}{(memorypoolsize)}
log2(memorypoolsize), 31)
第二级链表 Second List (简称
sl
)。第二层链表在第一层的基础上,按照一定的间隔,线性分段,其范围应该在1-32,对于32bit的处理器,第二级的索引值sli
一般为4或者5(经验值)。
例如上图分为fl
和sl
两级索引,FL_bitmap
和SL_bitmaps[]
的每个bit代表是否被使用:
fl
分为8级。sl
分为4级。这里说明下,图中sl
分了8个小区,我们计算sl时会将8个小区合为4个小区;
比如2的6次方
这一段:
- 第一级的
FL_bitmap
为110...1..0
,次高位为1,次高位对应着2的6次方
这一段,说明这一段有空闲块。 - 第二级链表分为4个小区间
[64,80),[80,96),[96,112),[112,128)
,每一级的链表都有一个bitmap用于标记对应的链表中是否有内存块。SL_bitmap
位00000010
,其对应着[80,96)
这个区间有空闲块,即下面的89 Byte。
通过两级索引来查找或释放内存,malloc与free的所有操作花费是个时间常数,时间响应复杂度都为O(1)
;
2.2 内存池初始化
struct bhdr_struct
内存块的控制头,每个内存块不论是free
还是 malloc
状态都需要一个控制头:
typedef struct bhdr_struct {
// prev_hdr 指向地址上连续的前一内存块,只有在当前内存状态为 malloc 时才有意义。
// 主要在当前内存块释放时,判断前一内存块是否是 free 状态,是否可以合并。
// 在内存释放时会同时判断是否能够和前后连续地址的内存块进行合并,为什么不多加一个指针指向地址上连续的后一内存块?
// 这是因为后一个内存块地址可以根据自己的长度计算出来,而前一内存块地址是没法计算出来,这样就能节省一个指针的空间
struct bhdr_struct *prev_hdr;
// size 存储了剩余内存空间的大小,它是从后面的 ptr->buffer[0] 开始计算的
// 因为 size 是 4字节对齐,所以最低两位用来做使用标识:
// bit0表示当前块是否使用,bit1表示前一内存块是否被使用。(置0表示使用,1表示未使用)
size_t size;
union {
// 在 malloc 状态下,这里是有效数据的开始
u8_t buffer[1];
// 在 free 状态下,这里用来存储 free list 指针
// 注意这里 free_ptr 和前面 prev_hdr 的区别:
// free_ptr 链接的是 free list,内存地址上是不连续的。把相同大小的 free 内存块链接到一起
// prev_hdr 指示的是地址上连续的上一内存块,而不管内存块的状态,只有在释放时尝试合并时才判断状态
struct free_ptr_struct free_ptr;
} ptr;
} bhdr_t;
/* free list 指针结构,只有在内存块 free 时有效 */
typedef struct free_ptr_struct {
struct bhdr_struct *prev;
struct bhdr_struct *next;
} free_ptr_t;
TLSF_struct
内存池的控制头,也支持多个内存池链接到一起:
typedef struct TLSF_struct {
/* the TLSF's structure signature */
// 内存池签名字段
u32_t tlsf_signature;
// 操作锁
#if TLSF_USE_LOCKS
TLSF_MLOCK_T lock;
#endif
// 统计计数
#if TLSF_STATISTIC
/* These can not be calculated outside tlsf because we
* do not know the sizes when freeing/reallocing memory. */
size_t used_size;
size_t max_size;
#endif
/* A linked list holding all the existing areas */
// 链接多内存池扩展指针,实际存储位置放在一个内存块中
area_info_t *area_head;
/* the first-level bitmap */
/* This array should have a size of REAL_FLI bits */
// 一级链表的 bitmap
u32_t fl_bitmap;
/* the second-level bitmap */
// 二级链表的 bitmap
u32_t sl_bitmap[REAL_FLI];
// 空闲链表矩阵
bhdr_t *matrix[REAL_FLI][MAX_SLI];
} tlsf_t;
/* 用于连接多个内存池 */
typedef struct area_info_struct {
// 指向当前内存池的末端内存块
bhdr_t *end;
// 指向下一个内存池,扩展内存
struct area_info_struct *next;
} area_info_t;
- init_memory_pool()
size_t init_memory_pool(size_t mem_pool_size, void *mem_pool)
{
tlsf_t *tlsf;
bhdr_t *b, *ib;
/* (1.1) 参数合法值判断 */
if (!mem_pool || !mem_pool_size || mem_pool_size < sizeof(tlsf_t) + BHDR_OVERHEAD * 8) {
ERROR_MSG("init_memory_pool (): memory_pool invalid\n");
return -1;
}
/* (1.2) 传入起始地址是否对齐判断 */
if (((unsigned long) mem_pool & PTR_MASK)) {
ERROR_MSG("init_memory_pool (): mem_pool must be aligned to a word\n");
return -1;
}
/* (1.3) 防止重复初始化内存池 */
tlsf = (tlsf_t *) mem_pool;
/* Check if already initialised 此内存池已经初始化了*/
if (tlsf->tlsf_signature == TLSF_SIGNATURE) {
mp = mem_pool;
b = GET_NEXT_BLOCK(mp, ROUNDUP_SIZE(sizeof(tlsf_t)));
return b->size & BLOCK_SIZE;
}
mp = mem_pool;
/* Zeroing the memory pool */
/* (2.1) 对内存池所有空间清零 */
memset(mem_pool, 0, sizeof(tlsf_t));
/* (2.2) 合法签名 */
tlsf->tlsf_signature = TLSF_SIGNATURE;
/* (2.3) 锁初始化 */
TLSF_CREATE_LOCK(&tlsf->lock);
/* (3) 初始化内存池中 struct TLSF_struct 结构以后的空间 */
ib = process_area(GET_NEXT_BLOCK
(mem_pool, ROUNDUP_SIZE(sizeof(tlsf_t))), ROUNDDOWN_SIZE(mem_pool_size - sizeof(tlsf_t)));
/* (4) 根据返回结果,得到一块最大的内存 */
b = GET_NEXT_BLOCK(ib->ptr.buffer, ib->size & BLOCK_SIZE);
/* (5) 将最大一块内存 free 到内存池中,内存池就有内存可用了 */
free_ex(b->ptr.buffer, tlsf);
/* (6.1) 将 area_info_t * 指针指向实际的存储位置,内存池中第一个内存块 ib */
tlsf->area_head = (area_info_t *) ib->ptr.buffer;
/* (6.2) 更新统计 */
#if TLSF_STATISTIC
tlsf->used_size = mem_pool_size - (b->size & BLOCK_SIZE);
tlsf->max_size = tlsf->used_size;
#endif
/* (6.3) 返回内存池可用内存的长度 */
return (b->size & BLOCK_SIZE);
}
↓
static __inline__ bhdr_t *process_area(void *area, size_t size)
{
bhdr_t *b, *lb, *ib;
area_info_t *ai;
/* (3.1) 第一个内存块 ib,存储的是 area_info_t 结构。
最后给 tlsf->area_head 指针使用
*/
ib = (bhdr_t *) area;
ib->size =
(sizeof(area_info_t) <
MIN_BLOCK_SIZE) ? MIN_BLOCK_SIZE : ROUNDUP_SIZE(sizeof(area_info_t)) | USED_BLOCK | PREV_USED;
/* (3.2) 第二个内存块 b,存储的是有效内存。
当前是已分配状态,稍后释放给内存池
*/
b = (bhdr_t *) GET_NEXT_BLOCK(ib->ptr.buffer, ib->size & BLOCK_SIZE);
b->size = ROUNDDOWN_SIZE(size - 3 * BHDR_OVERHEAD - (ib->size & BLOCK_SIZE)) | USED_BLOCK | PREV_USED;
b->ptr.free_ptr.prev = b->ptr.free_ptr.next = 0;
/* (3.3) 最后一个内存块 lb,存储的是一个结束标志。
长度为0,没有任何有效数据
*/
lb = GET_NEXT_BLOCK(b->ptr.buffer, b->size & BLOCK_SIZE);
lb->prev_hdr = b;
lb->size = 0 | USED_BLOCK | PREV_FREE;
/* (3.4) 第一个内存块 ib 数据区存储的 area_info_t 结构进行赋值。
*/
ai = (area_info_t *) ib->ptr.buffer;
ai->next = 0;
ai->end = lb;
return ib;
}
经过 process_area() 初始化以后,内存池中的数据结构:
初始化时一共创建了3个内存块:
ib
。系统内存块不会释放,用来存储内存池控制头部area_head
指向的area_info_t
存储空间。b
。用户可用的内存块,后面用户可分配得到的内存都来自于这一块内存。lb
。系统内存块不会释放,表示当前内存池的结束,area_info_t->end
会指向这里。
每个内存块的头部都是 bhdr_t
控制结构,实际数据从 bhdr_t->ptr.buffer[]
开始存储。
2.3 free
在 process_area() 初始化以后,就会调用 free_ex() 函数将内存块 b
释放给内存池。我们继续分析具体过程:
void free_ex(void *ptr, void *mem_pool)
{
tlsf_t *tlsf = (tlsf_t *) mem_pool;
bhdr_t *b, *tmp_b;
int fl = 0, sl = 0;
if (!ptr) {
return;
}
/* (5.1) 从内存块的数据地址,计算出内存块的控制结构地址 */
b = (bhdr_t *) ((char *) ptr - BHDR_OVERHEAD);
/* (5.2) 将当前内存块的状态改为 free */
b->size |= FREE_BLOCK;
TLSF_REMOVE_SIZE(tlsf, b); /* #if TLSF_STATISTIC */
b->ptr.free_ptr.prev = NULL;
b->ptr.free_ptr.next = NULL;
/* (5.3) 尝试和后面的相邻物理块进行合并,如果它也是 free 的话 */
tmp_b = GET_NEXT_BLOCK(b->ptr.buffer, b->size & BLOCK_SIZE); /* 得到b后面的相邻物理块指针*/
if (tmp_b->size & FREE_BLOCK) { /* b后面块是free的? */
MAPPING_INSERT(tmp_b->size & BLOCK_SIZE, &fl, &sl); /* 根据tmp_b大小求出一级与二级索引值 */
EXTRACT_BLOCK(tmp_b, tlsf, fl, sl); /* 摘出可合并块 */
b->size += (tmp_b->size & BLOCK_SIZE) + BHDR_OVERHEAD; /* 把b(ptr)后面的内存块合并到b内存块中,size更新*/
}
/* (5.4) 尝试和前面的相邻物理块进行合并,如果它也是 free 的话 */
if (b->size & PREV_FREE) { /* b前一块free? */
tmp_b = b->prev_hdr; /* 得到b前一物理块指针 */
MAPPING_INSERT(tmp_b->size & BLOCK_SIZE, &fl, &sl); /* 根据tmp_b大小求出一级与二级索引值 */
EXTRACT_BLOCK(tmp_b, tlsf, fl, sl); /* 摘出可合并块 */
tmp_b->size += (b->size & BLOCK_SIZE) + BHDR_OVERHEAD; /* 更新新块的size */
b = tmp_b; /* 更新b指针的值,即b指向合并后的内存块地址 */
}
/* (5.5) 将合并后的空闲块插入 free list 链表 */
MAPPING_INSERT(b->size & BLOCK_SIZE, &fl, &sl); /* */
INSERT_BLOCK(b, tlsf, fl, sl); /* 把释放的内存块插入相应链表的表头 */
/* (5.6) 更新相邻物理后块对当前块的引用指针和free状态 */
tmp_b = GET_NEXT_BLOCK(b->ptr.buffer, b->size & BLOCK_SIZE);
tmp_b->size |= PREV_FREE; /* 更新后一块的信息,以表示释放的内存块空闲的*/
tmp_b->prev_hdr = b; /* 更新后一块内存块的物理块prev_hdr*/
}
#define INSERT_BLOCK(_b, _tlsf, _fl, _sl) do { \
_b -> ptr.free_ptr.prev = NULL; \ /* 插入表头,则前项指针为空 */
_b -> ptr.free_ptr.next = _tlsf -> matrix [_fl][_sl]; \
if (_tlsf -> matrix [_fl][_sl]) \ /* 若原链表非空,原表头的前项指针指向_b内存块,以形成双向链表 */
_tlsf -> matrix [_fl][_sl] -> ptr.free_ptr.prev = _b; \
_tlsf -> matrix [_fl][_sl] = _b; \
set_bit (_sl, &_tlsf -> sl_bitmap [_fl]);\ /* 更新位图标志位 */
set_bit (_fl, &_tlsf -> fl_bitmap); \
} while(0)
经过 free_ex() 以后内存池的状态:
2.4 malloc
malloc 流程就非常清晰和简单了,主要流程就在二级空闲链表 bhdr_t *matrix[REAL_FLI][MAX_SLI]
中查找合适大小的空闲内存块并分配。
其中一个注意的点就是如果分配得到的是大块,还有一个切割的过程:
void *malloc_ex(size_t size, void *mem_pool)
{
tlsf_t *tlsf = (tlsf_t *) mem_pool;
bhdr_t *b, *b2, *next_b;
int fl, sl;
size_t tmp_size;
/* (1) 分配的最小值不能小于MIN_BLOCK_SIZE,并且向上对齐 */
size = (size < MIN_BLOCK_SIZE) ? MIN_BLOCK_SIZE : ROUNDUP_SIZE(size);
/* Rounding up the requested size and calculating fl and sl */
/* (2) 查找 size 对应的 一级索引fl 和 二级索引sl */
MAPPING_SEARCH(&size, &fl, &sl);
/* Searching a free block, recall that this function changes the values of fl and sl,
so they are not longer valid when the function fails */
/* (3) 根据fl与sl的值,得到适合的空闲链表的表头*/
b = FIND_SUITABLE_BLOCK(tlsf, &fl, &sl);
/* 以下部分是用于当前内存池中,没有所需内存块时,从内存中得到新的内存区(使用sbrk or mmap函数)*/
#if USE_MMAP || USE_SBRK
......
#endif
/* (3.1) 如果b空闲链表表头为NULL,表示分配内存失败!*/
if (!b)
return NULL; /* Not found */
/* (3.2) 从对应 free list 中摘出一个内存块 */
EXTRACT_BLOCK_HDR(b, tlsf, fl, sl);
/*-- found: */
/* (3.3) 得到后面物理相邻内存块指针 */
next_b = GET_NEXT_BLOCK(b->ptr.buffer, b->size & BLOCK_SIZE);
/* Should the block be split? */
/* (4.1) 如果分配得到的是大块内存,把内存分割,把多余内存归还给内存池 */
tmp_size = (b->size & BLOCK_SIZE) - size;
if (tmp_size >= sizeof(bhdr_t)) { /* 需要分割 */
tmp_size -= BHDR_OVERHEAD;
b2 = GET_NEXT_BLOCK(b->ptr.buffer, size); /* 切割剩余的空闲内存块 */
b2->size = tmp_size | FREE_BLOCK | PREV_USED; /* 为分割下来的内存块的size赋值*/
next_b->prev_hdr = b2; /* next_b内存块链接相邻的前一个物理内存块*/
MAPPING_INSERT(tmp_size, &fl, &sl); /* 查找剩余内存块的空闲链表的一级与二级索引值*/
INSERT_BLOCK(b2, tlsf, fl, sl); /* 插入内存块,且总是查入表头*/
/*add by vector,right?*/
b2->prev_hdr = b; /* 更新b2块的前一块的内存地址,*/
/* size后两位更新,只把0bit改为USED_BLOCK*/
b->size = size | (b->size & PREV_STATE); /* 参数size为所需内存大小,更新b块的状态*/
/* (4.2) 所得内存块不需要分割,只需更新后面物理相邻内存块的标志 */
} else {
next_b->size &= (~PREV_FREE);
b->size &= (~FREE_BLOCK); /* Now it's used */
}
/* 更新统计值 */
TLSF_ADD_SIZE(tlsf, b);
return (void *) b->ptr.buffer;
}
参考资料
1.动态内存和静态内存管理机制
2.uC/os内存优化——TLSF算法
3.tlsf github
4.TLsf Documentation
5.实时系统动态内存算法分析dsa(一)
6.实时系统动态内存算法分析dsa(二)——TLSF代码分析
TLFS 内存分配算法详解的更多相关文章
- go - 内存分配机制详解
一般程序的内存分配,从高位到低位依次为 全局静态区:用于存储全局变量.静态变量等:这部分内存在程序编译时已经分配好,由操作系统管理,速度快,不易出错. 栈:函数中的基础类型的局部变量:由程序进行系统调 ...
- C++内存分配方式详解——堆、栈、自由存储区、全局/静态存储区和常量存储区
栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区.里面的变量通常是局部变量.函数参数等.在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用.和堆一样 ...
- 【校招面试 之 C/C++】第14题 C++ 内存分配方式详解——堆、栈、自由存储区、全局/静态存储区和常量存储区(堆栈的区别)
栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区.里面的变量通常是局部变量.函数参数等.在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用.和堆一样 ...
- (转)C++内存分配方式详解——堆、栈、自由存储区、全局/静态存储区和常量存储区
程序在内存有五个存在区域: A:动态区域中的栈区 B:动态区域中的栈区 C:静态区域中:全局变量 和静态变量 (这个区域又可以进一步细分为:初始化的全局变量和静态变量 以及 未初始 ...
- linux的vm.overcommit_memory的内存分配参数详解
公司的redis有时background save db不成功,通过log发现下面的告警,很可能由它引起的: [13223] 17 Mar 13:18:02.207 # WARNING overcom ...
- Elasticsearch内存分配设置详解
Elasticsearch默认安装后设置的内存是1GB,对于任何一个现实业务来说,这个设置都太小了.如果你正在使用这个默认堆内存配置,你的集群配置可能会很快发生问题. 这里有两种方式修改Elastic ...
- Elasticsearch内存分配设置详解(转)
Elasticsearch默认安装后设置的内存是1GB,对于任何一个现实业务来说,这个设置都太小了.如果你正在使用这个默认堆内存配置,你的集群配置可能会很快发生问题.这里有两种方式修改Elastics ...
- [Spark内核] 第36课:TaskScheduler内幕天机解密:Spark shell案例运行日志详解、TaskScheduler和SchedulerBackend、FIFO与FAIR、Task运行时本地性算法详解等
本課主題 通过 Spark-shell 窥探程序运行时的状况 TaskScheduler 与 SchedulerBackend 之间的关系 FIFO 与 FAIR 两种调度模式彻底解密 Task 数据 ...
- 八大排序算法详解(动图演示 思路分析 实例代码java 复杂度分析 适用场景)
一.分类 1.内部排序和外部排序 内部排序:待排序记录存放在计算机随机存储器中(说简单点,就是内存)进行的排序过程. 外部排序:待排序记录的数量很大,以致于内存不能一次容纳全部记录,所以在排序过程中需 ...
随机推荐
- requests访问页面时set-cookie获取cookie
import requests headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:57.0) Gecko/ ...
- Java基础(七)——多线程
一.概述 1.介绍 Java VM 启动的时候会有一个进程Java.exe,该进程中至少有一个线程负责Java程序的执行.而且这个线程运行的代码存在于main方法中,该线程称之为主线程.其实从细节上来 ...
- MySQL技术专题(X)该换换你的数据库版本了,让我们一同迎接8.0的到来哦!(初探篇)
前提背景 MySQL关是一种关系数据库管理系统,所使用的 SQL 语言是用于访问数据库的最常用的标准化语言,其特点为体积小.速度快.总体拥有成本低,尤其是开放源码这一特点,在 Web应用方面 MySQ ...
- 感恩笔记之二_SQL语句扩展功能
前言导读: 本章是对SQL语句基础功能中,一些功能用法的扩展使用的总结,都是实际工作中一些经验的积累. 1 select列查询功能组合使用 --1 函数处理+列计算+列改名 select 函数(列) ...
- JVM学习笔记——栈区
栈区 Stack Area 栈是运行时的单位,堆是存储单位,栈解决程序的运行问题,即程序如何执行,如何处理数据. 每个线程在创建时都创建一个该线程私有的虚拟机栈,每个栈里有许多栈帧,一个栈帧对应一个 ...
- C#开发BIMFACE系列47 IIS部署并加载离线数据包
BIMFACE二次开发系列目录 [已更新最新开发文章,点击查看详细] 在前两篇博客<C#开发BIMFACE系列45 服务端API之创建离线数据包>与<C#开发BIMFACE系 ...
- SpringBoot-自动装配2
配置文件到底能写什么?怎么写? SpringBoot官方文档中有大量的配置,直接去记忆的话,好像不是我们程序员的行事风格! 分析自动配置原理 能自动配置的组件一般都有命名为下面规则的两个类: xxxx ...
- 初学Python “登录”案例 更新!!
更新内容:添加了登录次数,如果超过限制的次数,则提示账户被锁定,去某邮箱申请解锁账户! 此次仅把登录系统更新之后源代码放到这里,不在共享源文件在网盘了! 1 ''' 2 登录界面 3 ''' 4 5 ...
- Flink Yarn的2种任务提交方式
Flink Yarn的2种任务提交方式 Pre-Job模式介绍 每次使用flink run运行任务的时候,Yarn都会重新申请Flink集群资源(JobManager和TaskManager),任务执 ...
- 【UE4 C++】UGameplayStatics 源代码
// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "CoreMinimal.h" # ...