问题背景
TLSF算法主要是面向实时操作系统提出的,对于RTOS而言,执行时间的确定性是最根本的(吞吐量不一定高),然而传统的动态内存分配器(DMA, Dynamic Memory Allocator)存在两个主要问题
- 最坏情况执行时间不确定(not bounded)或者复杂度过高(bounded with a too important bound")
- 碎片化问题(fragmentation)
TLSF的提出较好地解决了以上两个问题: 将动态内存的分配与回收时间复杂度都降到了O(1)时间复杂度,并且采用了Good-fit的分配策略保证系统运行时不会产生过多碎片。
TLSF概要
TLSF(全称Two-Level Segregated Fit),从命名来看主要分为三部分
- Segregated Free List
- Two-Level Bitmap
- Good Fit
前两个是数据结构,第三个是分配策略。
TLSF主要采用两级位图(Two-Level Bitmap)与分级空闲块链表(Segregated Free List)的数据结构管理动态内存池(memory pool)以及其中的空闲块(free blocks),用Good-Fit的策略进行分配。下面我们先简单介绍一下这三者。
1. 分级空闲块链表(Segregated Free List)
还是采用拆分理解的方式,继续把Segregated Free List拆开,探究其设计思路和发展演变过程
- List: 隐式链表,管理所有内存块
- Free List:显式链表,只管理空闲块
- Segregated Free List:分级空闲块链表,按空闲块大小分级,用多个链表管理
List
链表是内存管理中最常见的数据结构,在一块内存块头部添加一个头结点,记录该block本身的信息以及前后继block的关系。
隐式链表
其中最简单的一种就是隐式链表,链接所有内存块, 只记录内存块大小,由于内存块紧密相连,通过头结点指针加内存块大小即可得到下一个内存块的位置。由于没有显式指明内存块的地址,而是通过计算得到,所以又叫做隐式链表。
当需要分配内存时,需要从第一块内存块开始检索,检查该内存块是否被分配以及内存块大小是被满足,直到找到大小合适的空闲块分配出去。
Free List
显式链表隐式链表主要问题在于当内存分配时并不需要检索已分配的内存块,这浪费了不少时间,只需检索空闲块即可。因此,显式空闲块链表在空闲块头部添加一个指针域,指向下一个空闲块,这样检索时会跳过已分配的内存块(used blocks)。
Segregated Free List
分级空闲链表
上面提到的隐式链表和显式链表主要问题在于当空闲块个数为n时,检索复杂度在O(n)级别,速度较慢,分级空闲块链表优化了空闲块检索的复杂度,粗略计算大概降到O(log(n))级别。
分级空闲块链表(Segregated Free List)的设计思想是将空闲块按照大小分级,形成了不同块大小范围的分级(class),组内空闲块用链表链接起来。每次分配时先按分级大小范围查找到相应链表,再从相应链表挨个检索合适的空闲块,如果找不到,就在大小范围更大的一级查找,直到找到合适的块分配出去。
2. Two-Level Bitmap
上面我们介绍了分级空闲块链表的原理,但是我们并没有提及如何按照内存块大小分级。TLSF算法引入了位图(Bitmap)来解决这个问题。
位图(Bitmap)
Bitmap示意图位图的优势:
- 节省存储空间:用1-bit表示某个区间范围大小的空闲块是否存在
- 位操作速度快:部分体系结构有加速特殊位操作的指令(如clz, ffs,fls)
维基百科上还有更多关于bitmap的优劣势,这里不详述,参照Free Space Bitmap
Segregated List + Two-Level Bitmap
当分级空闲块链表碰上位图,动态内存管理结构变化稍微有些大,下面这张图画得还算清晰(能依稀看到分级空闲块链表的影子就好:-P)。
TLSF采用了两级位图(Two-Level Bitmap)来管理不同大小范围的空闲块链(free block lists)。 上图中包含三个虚线矩形框分别是:
- 第一级位图(First-Level Bitmap),表示内存块的粗粒度范围,一般是2的幂次粒度(例如[ 2^n ~ 2^n+1 ])
- 第二级位图(Secend-Level Bitmap)是一个数组,一级位图中的每一位对应这个数组的一项,表示内存块的细粒度范围(例如[ 2^n + 0*2^n-2 ~ 2^n + 1*2^n-2 ])
- 第三个框是内存中真正的空闲内存块(free blocks)
内存分配与释放流程(简版)
有了TLSF的大体框架概念以后,就可以先看一下内存alloc与free的简要流程。(细节下一节结合源码探讨)
内存分配流程
- 在位图中搜索合适的空闲块大小范围,找到free list的头指针
- 基于free list的头指针检索list分配空闲块
内存释放流程
- 将需要释放的内存块置为空闲块
- 与该空闲块物理上相邻的空闲块合并
- 计算合并后的空闲块大小范围,检索位图找到对应的free list
- 将该内存块加入free list,返还给内存池
3. Good-fit 策略
内存结构和分配释放流程已经有了大致的了解,但是其中还有一个小问题并没有说明:
如何检索到大小合适的空闲块链?
常规思路:Best-fit(内部碎片最优)
Best-fit常规思路是:找到能满足内存请求大小的最小空闲块,就会有下面的流程(以搜索大小为69字节的空闲块为例)
- 基于位运算找到请求大小所在的第一级位图(First-Level bitmap)对应的粗粒度范围([ 64 ~ 128 ]),也就是二级位图的索引
- 在粗粒度范围内,根据二级位图索引检索第二级位图(Second-Level bitmap)得到细粒度范围([ 68 ~ 70 ])
- 如上图所示,沿着右下角空闲块链表可以检索到69字节的那一块是Best-fit
看起来Best-fit 已经很不错了,但仍然还有提升空间。Best-fit策略最主要的问题还在于第三步,仍然需要检索对应范围的那一条空闲块链表,存在潜在的时间复杂度。
Good-fit(少量碎片换取O(1)时间复杂度)
Good-fit思路与Best-fit不同之处在于,Good-fit并不保证找到满足需求的最小空闲块,而是尽可能接近要分配的大小。
还以上述搜索大小为69字节的空闲块为例,Good-fit并不是找到[68 ~ 70]这一范围,而是比这个范围稍微大一点儿的范围(例如[71 ~ 73])。这样设计的好处就是[71 ~ 73]对应的空闲块链中每一块都能满足需求,不需要检索空闲块链表找到最小的,而是直接取空闲块链中第一块即可。整体上还不会造成太多碎片。
妙啊
TLSF源码剖析
这一节我们扒一扒LiteOS的源码,分析其中的数据结构和内存布局。
容我吐个槽,单纯学习TLSF算法可以参考这份代码,注释较多,命名方式也相对好理解
数据结构
1. 内存池管理:LOS_MEM_POOL_INFO
LOS_MEM_POOL_INFO对照示意图// LOS_MEM_POOL_INFO负责管理一个内存池(Memory Pool),位于该内存池的头部(低地址)
typedef struct LOS_MEM_POOL_INFO {
// 相当于一个NULL指针,空闲块链的头指针指向这里说明为空链
LOS_MEM_DYN_NODE stBlock_null;
// 内存池大小
UINT32 uwPoolSize;
// 空闲块list的位图信息
// fl指空闲空间大小的粗粒度区间(例如2^16~2^17这样跨度比较大的区间)
// sl指空闲空间大小的细粒度区间(例如(2^16 + 1*2^13) ~ (2^16 + 2*2^13)这样跨度稍微小的区间)
UINT32 fl_bitmap;
UINT32 sl_bitmap[FL_INDEX_COUNT];
// 对应sl_bitmap,每一bit对应一个头指针,指向一个空闲块链
// 若指向stBlock_null表示链为空,不存在该链不存在空闲块
LOS_MEM_DYN_NODE *pstBlocks[FL_INDEX_COUNT][SL_INDEX_COUNT];
// 如果有多个内存池,则通过pNextPool链接
#if (LOSCFG_MEM_MUL_POOL == YES)
VOID *pNextPool;
#endif
} LOS_MEM_POOL_INFO;
2. 内存块管理LOS_MEM_DYN_NODE
LOS_MEM_DYN_NODE/*
* Block header structure.
*
* There are several implementation subtleties involved:
* - The prev_phys_block field is only valid if the previous pNode is free.
* - The prev_phys_block field is actually stored at the end of the
* previous pNode. It appears at the beginning of this structure only to
* simplify the implementation.
* - The pNext_free / pPrev_free fields are only valid if the pNode is free.
*/
typedef struct LOS_MEM_DYN_NODE {
// 该字段不在本内存块内,实际在物理地址相邻的前一个节点末尾
/* Points to the previous physical pNode. */
struct LOS_MEM_DYN_NODE *pstPreNode;
/* The size of this pNode, excluding the pNode header. */
UINT32 uwSize;
/* Next and previous free blocks. */
// 空闲块(free block)通过双向链表链接
// 使用块(used block)无此字段,当作存储使用
struct LOS_MEM_DYN_NODE *pNext_free;
struct LOS_MEM_DYN_NODE *pPrev_free;
} LOS_MEM_DYN_NODE;
空闲块和使用块复用同一数据结构,但在使用时并非所有字段都使用。
参考链接
- 论文
- TLSF: a New Dynamic Memory Allocator for Real-Time Systems
- TLSF Performance
- TLSF Slide
- TLSF Description
- implicit list 、explicit list、segregated list
- http://www.cs.cmu.edu/afs/cs/academic/class/15213-s11/www/lectures/20-allocation-advanced.pdf
- https://en.wikipedia.org/wiki/Free_space_bitmap
附:LiteOS在Eclipse+QEMU虚拟机环境搭建
主要参考下面这两篇博客,从安装eclipe、配置到项目编译运行,挺完整的,Mac下也没什么问题,就是eclipse有点卡-_- !
Windows10如何安装LiteOS开发环境(一)
Windows10如何安装LiteOS开发环境(二)
提个醒:
- bilibili复制粘贴不知道混入了啥字符,还是亲手打字比较好,不然编译错误
- 第二篇博客中配置头文件前一定要先点击LiteOS工程选中整个工程
配置头文件
网友评论