LiteOS内存管理:TLSF算法

问题背景

TLSF算法主要是面向实时操作系统提出的,对于RTOS而言,执行时间的确定性是最根本的(吞吐量不一定高),然而传统的动态内存分配器(DMA, Dynamic Memory Allocator)存在两个主要问题

  1. 最坏情况执行时间不确定(not bounded)或者复杂度过高(bounded with a too important bound")
  2. 碎片化问题(fragmentation)

TLSF的提出较好地解决了以上两个问题: 将动态内存的分配与回收时间复杂度都降到了O(1)时间复杂度,并且采用了Good-fit的分配策略保证系统运行时不会产生过多碎片。

TLSF概要

TLSF(全称Two-Level Segregated Fit),从命名来看主要分为三部分

  1. Segregated Free List
  2. Two-Level Bitmap
  3. Good Fit

前两个是数据结构,第三个是分配策略。

TLSF主要采用两级位图(Two-Level Bitmap)与分级空闲块链表(Segregated Free List)的数据结构管理动态内存池(memory pool)以及其中的空闲块(free blocks),用Good-Fit的策略进行分配。下面我们先简单介绍一下这三者。

1. 分级空闲块链表(Segregated Free List)

还是采用拆分理解的方式,继续把Segregated Free List拆开,探究其设计思路和发展演变过程

  1. List: 隐式链表,管理所有内存块
  2. Free List:显式链表,只管理空闲块
  3. 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)。 上图中包含三个虚线矩形框分别是:

  1. 第一级位图(First-Level Bitmap),表示内存块的粗粒度范围,一般是2的幂次粒度(例如[ 2^n ~ 2^n+1 ])
  2. 第二级位图(Secend-Level Bitmap)是一个数组,一级位图中的每一位对应这个数组的一项,表示内存块的细粒度范围(例如[ 2^n + 0*2^n-2 ~ 2^n + 1*2^n-2 ])
  3. 第三个框是内存中真正的空闲内存块(free blocks)

内存分配与释放流程(简版)

有了TLSF的大体框架概念以后,就可以先看一下内存alloc与free的简要流程。(细节下一节结合源码探讨)
内存分配流程

  1. 在位图中搜索合适的空闲块大小范围,找到free list的头指针
  2. 基于free list的头指针检索list分配空闲块

内存释放流程

  1. 将需要释放的内存块置为空闲块
  2. 与该空闲块物理上相邻的空闲块合并
  3. 计算合并后的空闲块大小范围,检索位图找到对应的free list
  4. 将该内存块加入free list,返还给内存池

3. Good-fit 策略

内存结构和分配释放流程已经有了大致的了解,但是其中还有一个小问题并没有说明:

如何检索到大小合适的空闲块链?

常规思路:Best-fit(内部碎片最优)

Best-fit

常规思路是:找到能满足内存请求大小的最小空闲块,就会有下面的流程(以搜索大小为69字节的空闲块为例)

  1. 基于位运算找到请求大小所在的第一级位图(First-Level bitmap)对应的粗粒度范围([ 64 ~ 128 ]),也就是二级位图的索引
  2. 在粗粒度范围内,根据二级位图索引检索第二级位图(Second-Level bitmap)得到细粒度范围([ 68 ~ 70 ])
  3. 如上图所示,沿着右下角空闲块链表可以检索到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工程选中整个工程
    配置头文件

你可能感兴趣的:(LiteOS内存管理:TLSF算法)