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(经验值)。

例如上图分为flsl两级索引,FL_bitmapSL_bitmaps[]的每个bit代表是否被使用:

  • fl分为8级。
  • sl分为4级。这里说明下,图中sl分了8个小区,我们计算sl时会将8个小区合为4个小区;

比如2的6次方这一段:

  • 第一级的FL_bitmap110...1..0,次高位为1,次高位对应着2的6次方这一段,说明这一段有空闲块。
  • 第二级链表分为4个小区间[64,80),[80,96),[96,112),[112,128),每一级的链表都有一个bitmap用于标记对应的链表中是否有内存块。SL_bitmap00000010,其对应着[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 内存分配算法详解的更多相关文章

  1. go - 内存分配机制详解

    一般程序的内存分配,从高位到低位依次为 全局静态区:用于存储全局变量.静态变量等:这部分内存在程序编译时已经分配好,由操作系统管理,速度快,不易出错. 栈:函数中的基础类型的局部变量:由程序进行系统调 ...

  2. C++内存分配方式详解——堆、栈、自由存储区、全局/静态存储区和常量存储区

    栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区.里面的变量通常是局部变量.函数参数等.在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用.和堆一样 ...

  3. 【校招面试 之 C/C++】第14题 C++ 内存分配方式详解——堆、栈、自由存储区、全局/静态存储区和常量存储区(堆栈的区别)

    栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区.里面的变量通常是局部变量.函数参数等.在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用.和堆一样 ...

  4. (转)C++内存分配方式详解——堆、栈、自由存储区、全局/静态存储区和常量存储区

    程序在内存有五个存在区域: A:动态区域中的栈区  B:动态区域中的栈区 C:静态区域中:全局变量 和静态变量    (这个区域又可以进一步细分为:初始化的全局变量和静态变量    以及    未初始 ...

  5. linux的vm.overcommit_memory的内存分配参数详解

    公司的redis有时background save db不成功,通过log发现下面的告警,很可能由它引起的: [13223] 17 Mar 13:18:02.207 # WARNING overcom ...

  6. Elasticsearch内存分配设置详解

    Elasticsearch默认安装后设置的内存是1GB,对于任何一个现实业务来说,这个设置都太小了.如果你正在使用这个默认堆内存配置,你的集群配置可能会很快发生问题. 这里有两种方式修改Elastic ...

  7. Elasticsearch内存分配设置详解(转)

    Elasticsearch默认安装后设置的内存是1GB,对于任何一个现实业务来说,这个设置都太小了.如果你正在使用这个默认堆内存配置,你的集群配置可能会很快发生问题.这里有两种方式修改Elastics ...

  8. [Spark内核] 第36课:TaskScheduler内幕天机解密:Spark shell案例运行日志详解、TaskScheduler和SchedulerBackend、FIFO与FAIR、Task运行时本地性算法详解等

    本課主題 通过 Spark-shell 窥探程序运行时的状况 TaskScheduler 与 SchedulerBackend 之间的关系 FIFO 与 FAIR 两种调度模式彻底解密 Task 数据 ...

  9. 八大排序算法详解(动图演示 思路分析 实例代码java 复杂度分析 适用场景)

    一.分类 1.内部排序和外部排序 内部排序:待排序记录存放在计算机随机存储器中(说简单点,就是内存)进行的排序过程. 外部排序:待排序记录的数量很大,以致于内存不能一次容纳全部记录,所以在排序过程中需 ...

随机推荐

  1. Loj#6053-简单的函数【Min25筛】

    正题 题目链接:https://loj.ac/p/6053 题目大意 定义一个积性函数\(f(p^c)=p\ xor\ c\),求\(\sum_{i=1}^nf(i)\) 解题思路 异或这个东西不太好 ...

  2. NOI.AC#2007-light【根号分治】

    正题 题目链接:http://noi.ac/problem/2007 题目大意 \(n\)个格子排成一排,每个格子有一个\(0/1\)和一个颜色.开始每个格子都是\(0\),\(q\)次操作取反一个颜 ...

  3. 【Golang】三个点(...)用法

    众所周知,Go语言是严格类型语言,而开发的时候又遇到传入参数不定的情况,怎么办? 这里的三个点(-),就给我们编程人员带来很大的灵活性,具体如下 在Golang中,三个点一共会用在四个地方(话说三个点 ...

  4. HTML基本标记

    头部标记 <head></head> 说明:元素的作用范围是整篇文档.元素中可以有元信息定义.文档样式表定义和脚本等信息,定义在HTML语言头部的内容往往不会在网页上直接显示. ...

  5. 【集成学习】:Stacking原理以及Python代码实现

    Stacking集成学习在各类机器学习竞赛当中得到了广泛的应用,尤其是在结构化的机器学习竞赛当中表现非常好.今天我们就来介绍下stacking这个在机器学习模型融合当中的大杀器的原理.并在博文的后面附 ...

  6. verilog specify

    specify block用来描述从源点(source:input/inout port)到终点(destination:output/inout port)的路径延时(path delay),由sp ...

  7. sql递归查询部门数据

    1 with cte as 2 ( 3 select a.DepartCode,a.DepartName,a.ParentDepartCode from tbDeparts a where Paren ...

  8. Java:线程池

    Java:线程池 本笔记是根据bilibili上 尚硅谷 的课程 Java大厂面试题第二季 而做的笔记 获取多线程的方法: 实现 Runnable 接口 实现 Callable 接口 实例化 Thre ...

  9. Seata的一些概念

    Seata的一些概念 一.什么是seata 二.AT模式的介绍 1.前提条件 2.整体机制 3.读写隔离的实现 1.写隔离 2.读隔离 三.事务分组 1.事务分组是什么? 2.通过事务分组如何找到后端 ...

  10. 使用flink实现一个简单的wordcount

    使用flink实现一个简单的wordcount 一.背景 二.需求 三.前置条件 1.jdk版本要求 2.maven版本要求 四.实现步骤 1.创建 flink 项目 2.编写程序步骤 1.创建Str ...