1 最初还不是tlsf
早几年(2019年之前)idf使用的堆管理器实现很简单,使用一个链表将所有(空闲和非空闲)的内存块串起来。对此,已经有同学分析过,可以参考这篇博客:esp32 heap 内存管理简析。下面这张图也是来自这篇博客,一目了然:
2 为什么要引入tlsf
旧的堆管理器的实现比较简单,性能一般,无论是申请还是释放都需要遍历整个堆,时间复杂度是O(N) ,这对于RTOS是难以接受的。对此,有很多改进的方向:
- free list
- separate fit
- two level separate fit
可以认为上述三种堆管理器的实现是层层改进的。free list 只会将空闲的内存块串起来,减少了申请和释放时需要遍历的链表长度。separate fit 更进一步,将不同大小或不同大小范围的空闲内存块分别使用链表串起来,进一步减少了申请和释放时的时间消耗,如下图所示: 但这种实现仍然存在问题,如果坚持一个链表中的空闲内存块保持一个固定的大小,那么申请和释放固然能做到O(1) ,但不同大小内存块数量的分配很难做到灵活通用,需要开发者自己根据应用来配置,不是很方便,且几乎不可能避免内部碎片的问题。如果坚持一个链表中的空闲块大小保持在一定的范围,再辅之以申请时的内存分割和释放时的内存合并,那么内存的使用会更灵活,内部碎片也更小,但申请的时候就难免需要遍历链表,做不到O(1) 。
对此,two level separate fit 也即tlsf 被提出,具体的说,进一步细化separate fit 的划分,第一级指示一个粗略的内存大小范围(这就是separate fit 所做的),在第一级划分的基础上进行第二级细化——将第一级的范围进一步划分成间隔更小的子区间,进而实现在内部碎片可控的情况下,申请和释放做到O(1) 。
3 tlsf算法概览
tlsf 早在十多年前就被提出了,相关论文可在此处下载。本节将介绍这个算法的总体设计。
先给出tlsf 的设计示意图,再对着图介绍会更清晰: tlsf 首先将内存块的大小按照2的次幂进行第一级的划分,以上图为例,第一级区间的范围分别是(起点不是固定的):
- [24, 25 - 1)
- [25, 26 - 1)
- [26, 27 - 1)
- …
不难看出,第一级区间的个数取决于要管理的内存块的大小,第一级区间中最大的那个区间不可能超过实际的最大内存块的大小。在第一级区间划分的基础上进行均匀分割,得到第二级区间。若将第一级区间均匀划分为2SLI份(也即任一一级区间下第二级区间的个数 为2SLI,SLI 在实现的时候是固定不变的,是一个常量),且使用2f表示第一级区间大小范围的左边界 (为方便叙述,对于一个区间的边界,本文采用左小右大),则第二级区间的区间大小为:
- 2f / 2SLI
note:其中,左边界2f表示此时第一级区间的区间范围为[2f, 2f+1 - 1)
不妨将第二级区间的区间大小记为N ,则第二级区间的范围分别是:
- [2f, 2f + N)
- [2f + N, 2f + 2N)
- [2f + 2N, 2f + 3N)
- …
每个二级区间都存在一个链表(可能是空链表),这个链表上串着所有大小处于这个二级区间的空闲内存块。此外,对于一二级区间,使用位图来标识其中是否存在空闲内存块,这将使得我们可以快速查找可用区间。
不难注意到,不管是划分一级区间还是划分二级区间,都使用2的次幂,这当然是刻意为之,可以在给定内存大小,计算其应当属于哪个一二级区间时提供方便。具体的说,给定大小size ,则其所属一二级区间的索引如下计算: 因为2的幂次的使用,上式可以使用位运算快速得出。至于为什么是这样算,一级索引f 的计算很好理解,s 的计算可以参考tlsf 设计示意图中的红色标注。size 减去2f得到size 在所属一级区间中的偏移,用这个偏移除以二级区间的大小即可得到二级索引。得到f 及s 之后,通过位图可以快速判断出相应的二级区间是否存在空闲内存块。
为了实现申请内存O(1) 的复杂度,tlsf 使用提级申请 的方式。具体的说,若申请的size 落入二级区间为:
则我们不会在该区间遍历链表,而是提级到下一个区间去申请:
显然提级之后的区间中任一内存块都符合内存大小的要求,这就避免了链表遍历,实现了O(1) 。这样做当然有代价——会造成内部碎片。虽然由于两级划分的机制,使得内部碎片的大小控制在2N 以内。但如果size 足够大,也即2f足够大,那么2N 也会很大,所以tlsf 还配套有内存块的申请时分割机制,控制内部碎片的大小处于一个极低的水平。
申请和释放都做到了O(1) ,且内部碎片也得以控制,是否就意味着tlsf 完美了呢?当然不是,还要考虑外部碎片,过多的分割会导致外部碎片增加。对此,tlsf 也提供了内存块的释放时合并机制,虽然不能完全解决外部碎片,但至少能加以控制。世上并不存在完美的堆管理器,只是工程会不断权衡取舍以及改进,最终得到一个各方面性能都相对较好的。或许,对于小型嵌入式系统来说,tlsf 就是那个工程选择的结果,除了idf,还有其它一些RTOS或开发框架也在使用这个算法。
4 idf中使用的tlsf算法的设计与实现
idf中使用的tlsf 的实现来自一个开源项目:GitHub - mattconte/tlsf: Two-Level Segregated Fit memory allocator implementation.。基于tlsf ,idf增加了一些封装,实现了上层接口与底层算法的分离,以及堆调试等特性。相关源码全部位于heap 组件。下文就将介绍其中的tlsf 的设计与实现,其它内容将在后续博文中介绍。
4.1 先看结构
4.1.1 管理内存块的结构
每个内存块(block )都存在一个block_header_t ,这个类型定义在heap_tlsf.h :
typedef struct block_header_t
{
struct block_header_t* prev_phys_block;
size_t size;
struct block_header_t* next_free;
struct block_header_t* prev_free;
} block_header_t;
对于其中前两个字段需要进一步说明:
- prev_phys_block:这个字段的存在用于内存释放时的合并,当尝试与物理上上一个block合并时,必须知道物理上上一个内存块的位置。
- size:由于
tlsf 默认会对申请的内存大小进行向上4字节对齐,因此size 的最低两个bit可以用作表示其它含义。具体的,bit0 为1 表示当前block空闲;bit0 为0 表示当前block已被申请;bit1 为1 表示物理上前一个block空闲;bit1 为0 表示物理上前一个block已被申请。
tlsf 在heap_tlsf_block_functions.h 中定义了一些block 相关的功能接口,这些功能接口大部分都通俗易懂,基本代码本身等同于注释,但也有少数需要说明一下:
为了标识tlsf 堆的物理边界,最后一个block 为空,也就是一个size 为0的block ,可以利用这个特性来判断一个block 是不是最后的那个:
static inline __attribute__((__always_inline__)) int block_is_last(const block_header_t* block)
{
return block_size(block) == 0;
}
在源码中经常看到block 指针与ptr 指针,两者的转换关系如下:
static inline __attribute__((__always_inline__)) block_header_t* block_from_ptr(const void* ptr)
{
return tlsf_cast(block_header_t*,
tlsf_cast(unsigned char*, ptr) - block_start_offset);
}
static inline __attribute__((__always_inline__)) void* block_to_ptr(const block_header_t* block)
{
return tlsf_cast(void*,
tlsf_cast(unsigned char*, block) + block_start_offset);
}
用图来解释更形象:
为了优化元数据开销,block 的邻接关系不是很直观。当前block 的prev_phys_block 字段被藏在了物理上前一个block 的最后一个字,其实这本身也不难理解,但源码中有一个稍微恶心人的地方是使用sizeof(size_t) 替代sizeof(block_header_t *) ,虽然两者在数值上相等,但含义不一样呀。
还是以图来解释: 当我们想获取当前block 的前一个block 时,直接返回当前block 的prev_phys_block 字段即可:
static inline __attribute__((__always_inline__)) block_header_t* block_prev(const block_header_t* block)
{
return block->prev_phys_block;
}
当我们想获取当前block 的后一个block 时,我们需要先把ptr 指针往地址增加的方向挪size 个字节,再将其往地址减少的方向挪sizeof(block_header_t *) 个字节:
static inline __attribute__((__always_inline__)) block_header_t* offset_to_block(const void* ptr, size_t size)
{
return tlsf_cast(block_header_t*, tlsf_cast(tlsfptr_t, ptr) + size);
}
static inline __attribute__((__always_inline__)) block_header_t* block_next(const block_header_t* block)
{
block_header_t* next = offset_to_block(block_to_ptr(block),
block_size(block) - block_header_overhead);
return next;
}
当我们想链接当前block 及其后一个block 时,只需要设置其后一个block 的prev_phys_block 字段:
static inline __attribute__((__always_inline__)) block_header_t* block_link_next(block_header_t* block)
{
block_header_t* next = block_next(block);
next->prev_phys_block = block;
return next;
}
当我们标记当前block 已被申请时,除了标记其自身相应比特(size 字段的bit0 ),还需要标记其后一个block 的相应比特(size 字段的bit1 ):
static inline __attribute__((__always_inline__)) void block_mark_as_used(block_header_t* block)
{
block_header_t* next = block_next(block);
block_set_prev_used(next);
block_set_used(block);
}
当我们标记当前block 处于空闲状态时,也要考虑其后一个block 。此外,在相应接口中,还会链接当前block 及其后一个block :
static inline __attribute__((__always_inline__)) void block_mark_as_free(block_header_t* block)
{
block_header_t* next = block_link_next(block);
block_set_prev_free(next);
block_set_free(block);
}
这似乎也说得通,毕竟标记空闲的接口在释放内存或分割内存时使用,彼时,肯定伴随有链接操作(prev_phys_block 字段在内存块空闲时有意义)。但我个人觉得这里的封装是不太好的,至少我从block_mark_as_free 这个名字里丝毫看不出含有链接两个block 的意思。这也违反了一个接口只做一件事的原则。
4.1.2 管理tlsf堆的结构
每个tlsf堆都存在一个control_t 用于堆管理,这是tlsf 实现的核心结构,也是定义在heap_tlsf.h :
typedef struct control_t
{
block_header_t block_null;
unsigned int fl_bitmap;
unsigned int sl_bitmap[FL_INDEX_COUNT];
block_header_t* blocks[FL_INDEX_COUNT][SL_INDEX_COUNT];
} control_t;
仍然用图来说明:
二级区间和一级区间的数量等信息定义在heap_tlsf_config.h :
#if !CONFIG_SPIRAM
#define TLSF_MAX_POOL_SIZE (SOC_DIRAM_DRAM_HIGH - SOC_DIRAM_DRAM_LOW)
#else
#define TLSF_MAX_POOL_SIZE SOC_EXTRAM_DATA_SIZE
#endif
enum tlsf_config
{
SL_INDEX_COUNT_LOG2 = 5,
ALIGN_SIZE_LOG2 = 2,
ALIGN_SIZE = (1 << ALIGN_SIZE_LOG2),
#if (TLSF_MAX_POOL_SIZE <= (256 * 1024))
FL_INDEX_MAX = 18,
#elif (TLSF_MAX_POOL_SIZE <= (512 * 1024))
FL_INDEX_MAX = 19,
#elif (TLSF_MAX_POOL_SIZE <= (1 * 1024 * 1024))
FL_INDEX_MAX = 20,
#elif (TLSF_MAX_POOL_SIZE <= (2 * 1024 * 1024))
FL_INDEX_MAX = 21,
#elif (TLSF_MAX_POOL_SIZE <= (4 * 1024 * 1024))
FL_INDEX_MAX = 22,
#elif (TLSF_MAX_POOL_SIZE <= (8 * 1024 * 1024))
FL_INDEX_MAX = 23,
#elif (TLSF_MAX_POOL_SIZE <= (16 * 1024 * 1024))
FL_INDEX_MAX = 24,
#elif (TLSF_MAX_POOL_SIZE <= (32 * 1024 * 1024))
FL_INDEX_MAX = 25,
#else
#error "Higher TLSF pool sizes should be added for this new config"
#endif
SL_INDEX_COUNT = (1 << SL_INDEX_COUNT_LOG2),
FL_INDEX_SHIFT = (SL_INDEX_COUNT_LOG2 + ALIGN_SIZE_LOG2),
FL_INDEX_COUNT = (FL_INDEX_MAX - FL_INDEX_SHIFT + 1),
SMALL_BLOCK_SIZE = (1 << FL_INDEX_SHIFT),
};
需要进一步说明的是,由于存在4字节对齐的操作,因此申请的内存的大小会被调整为4的倍数,因此二级区间划分时,若区间大小低于4字节,那就没意义了。由于二级区间的数量为SL_INDEX_COUNT ,所以,一级区间的起点应当为SL_INDEX_COUNT * 4 。具体到上面的定义,一级区间的起点为128 字节。
但显然我们不可能把内存块的大小都保持在不小于128 字节的水平,这对于小型嵌入式系统太不合理了。对于小于128 字节的内存块,不妨将其称为细块 ,这些细块将统一存放于第0 个一级区间,对应着一级位图的bit0 。自然的,[128, 255) 范围的内存块位于第1 个一级区间,对应一级位图的bit1 。再后面的,以此类推:
4.2 优化内存块的元数据开销
上文说过,为了优化元数据开销,当前block的prev_phys_block 字段被藏在了物理上前一个block的最后一个字。这样说其实是不够准确的,更准确的说:当前block的prev_phys_block 字段被藏在了物理上上一个空闲 block的最后一个字。之所以强调空闲 ,是因为只有在物理上上一个block空闲时,当前block的prev_phys_block 字段才有意义。稍微有点绕,但不难解释清楚。
还是祭出这幅图: 不难看出,当内存块被申请时,就要从空闲链表中摘出来,这时,next_free 以及prev_free 字段所在的位置就可以给申请者使用了。再加上prev_phys_block 字段位于物理上上一个block的尾部,因此一个被申请的内存块的元数据开销只有4 个字节。
block的可用区域的尾部在未被申请出来时存放的是物理上下一个block的prev_phys_block 字段,一旦被申请这部分内容可能会被覆盖,因此prev_phys_block 字段就不再有意义了。那么,prev_phys_block 字段被破坏了会存在问题嘛?当然是不存在问题的。因为当前block被申请出去之后,其物理上下一个block的prev_phys_block 字段根本不会使用到,该字段只在释放block时尝试合并其物理上上一个block时才会用到。若当前block不为空闲,那么释放其物理上下一个block时进行的向上合并尝试就会及时终止,不用去访问该字段。破坏一个根本用不到的数据,自然不会有问题。但总有一天要用到该字段的,比如释放当前block,使之回归空闲状态之后,就有可能访问该字段。这也没什么,只要我们在释放时顺便恢复它就行了。唯一的问题就是,还能不能恢复?当然是可以的,显然,我们有ptr ,有size ,就很容易找到这个字段,找到之后将其赋值为当前block 指针即可,这个恢复的操作就在上文介绍的block_link_next 接口中。
4.3 一二级位图索引的计算
给定size ,计算其对应的一二级位图索引(也是二维数组blocks 的索引),这已经在本文的第3 节介绍过了,但考虑到代码实现不是那么直观,所以这里还是专门给一节进行说明。
首先看一级位图索引的计算,也就是对log2(size)进行向下取整,实际上可以转化为计算最高位的1 所在的位置(从0开始计数):
static inline __attribute__((__always_inline__)) int tlsf_fls(unsigned int word)
{
const int bit = word ? 32 - __builtin_clz(word) : 0;
return bit - 1;
}
函数mapping_insert 基本上就本文第3 节中公式的直译,但还是稍微绕了一点弯。这一点弯主要在于减去2SLI时,使用的不是剑法操作,而是亦或 操作,将性能考虑到极致(这很嵌入式):
static inline __attribute__((__always_inline__)) void mapping_insert(size_t size, int* fli, int* sli)
{
int fl, sl;
if (size < SMALL_BLOCK_SIZE)
{
fl = 0;
sl = tlsf_cast(int, size) >> 2;
}
else
{
fl = tlsf_fls(size);
sl = tlsf_cast(int, size >> (fl - SL_INDEX_COUNT_LOG2)) ^ (1 << SL_INDEX_COUNT_LOG2);
fl -= (FL_INDEX_SHIFT - 1);
}
*fli = fl;
*sli = sl;
}
上文提到,tlsf 在申请内存时,会提级申请 ,提级操作在mapping_search 中实现。mapping_search 在调用mapping_insert 计算fli 和sli 之前,会先对size 做一个增加的操作,具体来说,将size增加其所属二级区间的大小再减1(同属一个一级区间的所有二级区间有着相同的大小),使其落入未增加之前所属二级区间的下一个区间,也即提升到邻接的更大的二级区间。有一个特殊情况:size 等于其所属二级区间的左边界,此时,即便增加size 也无法实现提升。但这不存在问题,因为此时size 所属的二级区间内的任一空闲内存块的大小都是满足要求的(>=size ),换句话说,此时根本无需提升。
static inline __attribute__((__always_inline__)) void mapping_search(size_t size, int* fli, int* sli)
{
if (size >= SMALL_BLOCK_SIZE)
{
const size_t round = (1 << (tlsf_fls(size) - SL_INDEX_COUNT_LOG2)) - 1;
size += round;
}
mapping_insert(size, fli, sli);
}
需要注意的是,这样一个提级 的操作,使得我们在计算最大空闲内存块(largest_free_block )的大小时,需要将真实的最大内存块的大小向减小的方向,按其所属的二级区间的大小对齐。这是显然的,不考虑特殊情况的话,size 提升之后得到size' > size ,如果最大内存块的大小为size ,那么也是不够的,因为提级 之后,我们需要的是size' 。
4.4 tlsf堆的创建与销毁
4.4.1 tlsf堆的创建
tlsf 提供接口tlsf_create_with_pool 用于创建堆,先梳理一下函数的调用关系:
- tlsf_create_with_pool
- tlsf_create
- tlsf_add_pool
然后按部就班的看一下这几个函数的实现,首先是tlsf_create_with_pool :
tlsf_t tlsf_create_with_pool(void* mem, size_t bytes)
{
tlsf_t tlsf = tlsf_create(mem);
tlsf_add_pool(tlsf, (char*)mem + tlsf_size(), bytes - tlsf_size());
return tlsf;
}
tlsf_create 基本上就是control_construct 套了个壳,多了一个地址对齐校验:
tlsf_t tlsf_create(void* mem)
{
if (((tlsfptr_t)mem % ALIGN_SIZE) != 0)
{
printf("tlsf_create: Memory must be aligned to %u bytes.\n",
(unsigned int)ALIGN_SIZE);
return 0;
}
control_construct(tlsf_cast(control_t*, mem));
return tlsf_cast(tlsf_t, mem);
}
control_construct 做了control_t 的初始化:
static void control_construct(control_t* control)
{
int i, j;
control->block_null.next_free = &control->block_null;
control->block_null.prev_free = &control->block_null;
control->fl_bitmap = 0;
for (i = 0; i < FL_INDEX_COUNT; ++i)
{
control->sl_bitmap[i] = 0;
for (j = 0; j < SL_INDEX_COUNT; ++j)
{
control->blocks[i][j] = &control->block_null;
}
}
}
tlsf_add_pool 主要做了两件事:
- 初始化两个
block ,一个位于开头,作为最初也是最大的block,另一个位于结尾,是哨兵block - 将开头的那个block插入(头插)空闲链表
#define block_size_min (sizeof(block_header_t) - sizeof(block_header_t*))
#define block_size_max (tlsf_cast(size_t, 1) << FL_INDEX_MAX)
......
pool_t tlsf_add_pool(tlsf_t tlsf, void* mem, size_t bytes)
{
block_header_t* block;
block_header_t* next;
const size_t pool_overhead = tlsf_pool_overhead();
const size_t pool_bytes = align_down(bytes - pool_overhead, ALIGN_SIZE);
......
if (pool_bytes < block_size_min || pool_bytes > block_size_max)
{
......
return 0;
}
block = offset_to_block(mem, -(tlsfptr_t)block_header_overhead);
block_set_size(block, pool_bytes);
block_set_free(block);
block_set_prev_used(block);
block_insert(tlsf_cast(control_t*, tlsf), block);
next = block_link_next(block);
block_set_size(next, 0);
block_set_used(next);
block_set_prev_free(next);
return mem;
}
用一幅图来描述刚刚建立的堆的样子:
4.4.2 tlsf堆的销毁
tlsf 并没有正儿八经的销毁堆的函数,tlsf_remove_pool 勉强算一个吧,但要求pool 处于刚刚建立或整体空闲的状态,也即tlsf_add_pool 之后并未申请内存,或申请了但全部释放了。听起来就不像有什么卵用的样子,实际上这个函数也确实没被调用。这里就一带而过了:
void tlsf_remove_pool(tlsf_t tlsf, pool_t pool)
{
control_t* control = tlsf_cast(control_t*, tlsf);
block_header_t* block = offset_to_block(pool, -(int)block_header_overhead);
int fl = 0, sl = 0;
tlsf_assert(block_is_free(block) && "block should be free");
tlsf_assert(!block_is_free(block_next(block)) && "next block should not be free");
tlsf_assert(block_size(block_next(block)) == 0 && "next block size should be zero");
mapping_insert(block_size(block), &fl, &sl);
remove_free_block(control, block, fl, sl);
}
4.5 内存块的申请与释放
4.5.1 内存块的插入与移除
block_insert 用于将一个给定的block 插入到空闲链表,插入分两步,第一步是根据block 的size 得到第一二级索引,第二步是采用头插的方式,将block 插入相应的空闲链表:
static inline __attribute__((__always_inline__)) void block_insert(control_t* control, block_header_t* block)
{
int fl, sl;
mapping_insert(block_size(block), &fl, &sl);
insert_free_block(control, block, fl, sl);
}
insert_free_block 用于完成插入操作:
static inline __attribute__((__always_inline__)) void insert_free_block(control_t* control, block_header_t* block, int fl, int sl)
{
block_header_t* current = control->blocks[fl][sl];
tlsf_assert(current && "free list cannot have a null entry");
tlsf_assert(block && "cannot insert a null entry into the free list");
block->next_free = current;
block->prev_free = &control->block_null;
current->prev_free = block;
tlsf_assert(block_to_ptr(block) == align_ptr(block_to_ptr(block), ALIGN_SIZE)
&& "block not aligned properly");
control->blocks[fl][sl] = block;
control->fl_bitmap |= (1 << fl);
control->sl_bitmap[fl] |= (1 << sl);
}
block_remove 用于从空闲链表中移除一个给定的block ,也是分两步操作。值得注意的是,tlsf 不会检查给定的block 是否存在于空闲链表,这一点需要由调用者保证:
static inline __attribute__((__always_inline__)) void block_remove(control_t* control, block_header_t* block)
{
int fl, sl;
mapping_insert(block_size(block), &fl, &sl);
remove_free_block(control, block, fl, sl);
}
remove_free_block 用于完成移除操作:
static inline __attribute__((__always_inline__)) void remove_free_block(control_t* control, block_header_t* block, int fl, int sl)
{
block_header_t* prev = block->prev_free;
block_header_t* next = block->next_free;
tlsf_assert(prev && "prev_free field can not be null");
tlsf_assert(next && "next_free field can not be null");
next->prev_free = prev;
prev->next_free = next;
if (control->blocks[fl][sl] == block)
{
control->blocks[fl][sl] = next;
if (next == &control->block_null)
{
control->sl_bitmap[fl] &= ~(1 << sl);
if (!control->sl_bitmap[fl])
{
control->fl_bitmap &= ~(1 << fl);
}
}
}
}
用一幅图总结一下,内存块在空闲链表中的状态:
4.5.2 内存块的分割与合并
内存分割与合并的动机已在上文中解释了,这里介绍一下它们的实现。不妨先看合并操作,合并有两个方向:
- 合并物理上前一个
block
static inline __attribute__((__always_inline__)) block_header_t* block_merge_prev(control_t* control, block_header_t* block)
{
if (block_is_prev_free(block))
{
block_header_t* prev = block_prev(block);
tlsf_assert(prev && "prev physical block can't be null");
tlsf_assert(block_is_free(prev) && "prev block is not free though marked as such");
block_remove(control, prev);
block = block_absorb(prev, block);
}
return block;
}
- 合并物理上后一个
block
static inline __attribute__((__always_inline__)) block_header_t* block_merge_next(control_t* control, block_header_t* block)
{
block_header_t* next = block_next(block);
tlsf_assert(next && "next physical block can't be null");
if (block_is_free(next))
{
tlsf_assert(!block_is_last(block) && "previous block can't be last");
block_remove(control, next);
block = block_absorb(block, next);
}
return block;
}
两者都调用block_absorb 实现功能:
static inline __attribute__((__always_inline__)) block_header_t* block_absorb(block_header_t* prev, block_header_t* block)
{
tlsf_assert(!block_is_last(prev) && "previous block can't be last");
prev->size += block_size(block) + block_header_overhead;
block_link_next(prev);
#ifdef MULTI_HEAP_POISONING_SLOW
multi_heap_internal_poison_fill_region(block, sizeof(block_header_t), true );
#endif
return prev;
}
再看分割操作,分割操作分为三种:
- 从
空闲 块的头部 分割出指定大小的内存,并将剩下的内存块 归还给堆
static inline __attribute__((__always_inline__)) void block_trim_free(control_t* control, block_header_t* block, size_t size)
{
tlsf_assert(block_is_free(block) && "block must be free");
if (block_can_split(block, size))
{
block_header_t* remaining_block = block_split(block, size);
block_link_next(block);
block_set_prev_free(remaining_block);
block_insert(control, remaining_block);
}
}
- 从
非空闲 块的头部 分割出指定大小的内存,并将剩下的内存块 归还给堆
static inline __attribute__((__always_inline__)) void block_trim_used(control_t* control, block_header_t* block, size_t size)
{
tlsf_assert(!block_is_free(block) && "block must be used");
if (block_can_split(block, size))
{
block_header_t* remaining_block = block_split(block, size);
block_set_prev_used(remaining_block);
remaining_block = block_merge_next(control, remaining_block);
block_insert(control, remaining_block);
}
}
- 从
空闲 块的头部 分割出指定大小的内存,并将分割出的内存块 归还给堆,返回剩下的内存块 (不要对这种做法感到奇怪,看4.6节)
static inline __attribute__((__always_inline__)) block_header_t* block_trim_free_leading(control_t* control, block_header_t* block, size_t size)
{
block_header_t* remaining_block = block;
if (block_can_split(block, size))
{
remaining_block = block_split(block, size - block_header_overhead);
block_set_prev_free(remaining_block);
block_link_next(block);
block_insert(control, block);
}
return remaining_block;
}
三者都调用block_split 实现功能:
static inline __attribute__((__always_inline__)) block_header_t* block_split(block_header_t* block, size_t size)
{
block_header_t* remaining =
offset_to_block(block_to_ptr(block), size - block_header_overhead);
const size_t remain_size = block_size(block) - (size + block_header_overhead);
tlsf_assert(block_to_ptr(remaining) == align_ptr(block_to_ptr(remaining), ALIGN_SIZE)
&& "remaining block not aligned properly");
tlsf_assert(block_size(block) == remain_size + size + block_header_overhead);
block_set_size(remaining, remain_size);
tlsf_assert(block_size(remaining) >= block_size_min && "block split with invalid size");
block_set_size(block, size);
block_mark_as_free(remaining);
return remaining;
}
还是用图来示意,一图胜千言(尽管画图的人很累-_-!!):
4.5.3 内存块的申请
先看几个操作铺垫一下:
- 计算最低位的1所在的位置(从0开始计)
static inline __attribute__((__always_inline__)) int tlsf_ffs(unsigned int word)
{
const unsigned int reverse = word & (~word + 1);
const int bit = 32 - __builtin_clz(reverse);
return bit - 1;
}
- 调整申请的大小(
tlsf 堆会对申请的大小进行向上4字节对齐)
static inline __attribute__((__always_inline__)) size_t adjust_request_size(size_t size, size_t align)
{
size_t adjust = 0;
if (size)
{
const size_t aligned = align_up(size, align);
if (aligned < block_size_max)
{
adjust = tlsf_max(aligned, block_size_min);
}
}
return adjust;
}
- 查找可用的内存块
static inline __attribute__((__always_inline__)) block_header_t* block_locate_free(control_t* control, size_t size)
{
int fl = 0, sl = 0;
block_header_t* block = 0;
if (size)
{
mapping_search(size, &fl, &sl);
if (fl < FL_INDEX_COUNT)
{
block = search_suitable_block(control, &fl, &sl);
}
}
if (block)
{
tlsf_assert(block_size(block) >= size);
remove_free_block(control, block, fl, sl);
}
return block;
}
再看search_suitable_block 做什么:
static inline __attribute__((__always_inline__)) block_header_t* search_suitable_block(control_t* control, int* fli, int* sli)
{
int fl = *fli;
int sl = *sli;
unsigned int sl_map = control->sl_bitmap[fl] & (~0U << sl);
if (!sl_map)
{
const unsigned int fl_map = control->fl_bitmap & (~0U << (fl + 1));
if (!fl_map)
{
return 0;
}
fl = tlsf_ffs(fl_map);
*fli = fl;
sl_map = control->sl_bitmap[fl];
}
tlsf_assert(sl_map && "internal error - second level bitmap is null");
sl = tlsf_ffs(sl_map);
*sli = sl;
return control->blocks[fl][sl];
}
- 完成申请后尝试分割
static inline __attribute__((__always_inline__)) void* block_prepare_used(control_t* control, block_header_t* block, size_t size)
{
void* p = 0;
if (block)
{
tlsf_assert(size && "size must be non-zero");
block_trim_free(control, block, size);
block_mark_as_used(block);
p = block_to_ptr(block);
}
return p;
}
有了上述铺垫之后,再看内存申请函数tlsf_malloc 就水到渠成了:
void* tlsf_malloc(tlsf_t tlsf, size_t size)
{
control_t* control = tlsf_cast(control_t*, tlsf);
size_t adjust = adjust_request_size(size, ALIGN_SIZE);
block_header_t* block = block_locate_free(control, adjust);
return block_prepare_used(control, block, adjust);
}
上层存在realloc 接口,自然tlsf 堆也提供了tlsf_realloc :
void* tlsf_realloc(tlsf_t tlsf, void* ptr, size_t size)
{
control_t* control = tlsf_cast(control_t*, tlsf);
void* p = 0;
if (ptr && size == 0)
{
tlsf_free(tlsf, ptr);
}
else if (!ptr)
{
p = tlsf_malloc(tlsf, size);
}
else
{
block_header_t* block = block_from_ptr(ptr);
block_header_t* next = block_next(block);
const size_t cursize = block_size(block);
const size_t combined = cursize + block_size(next) + block_header_overhead;
const size_t adjust = adjust_request_size(size, ALIGN_SIZE);
tlsf_assert(!block_is_free(block) && "block already marked as free");
if (adjust > cursize && (!block_is_free(next) || adjust > combined))
{
p = tlsf_malloc(tlsf, size);
if (p)
{
const size_t minsize = tlsf_min(cursize, size);
memcpy(p, ptr, minsize);
tlsf_free(tlsf, ptr);
}
}
else
{
if (adjust > cursize)
{
block_merge_next(control, block);
block_mark_as_used(block);
}
block_trim_used(control, block, adjust);
p = ptr;
}
}
return p;
}
4.5.4 内存块的释放
内存释放的接口tlsf_free 就很好理解了,内多少内容:
void tlsf_free(tlsf_t tlsf, void* ptr)
{
if (ptr)
{
control_t* control = tlsf_cast(control_t*, tlsf);
block_header_t* block = block_from_ptr(ptr);
tlsf_assert(!block_is_free(block) && "block already marked as free");
block_mark_as_free(block);
block = block_merge_prev(control, block);
block = block_merge_next(control, block);
block_insert(control, block);
}
}
值得说明的是:
在内存释放时,会尝试合并block:block_merge_prev、block_merge_next。往前、后一个block各尝试一次合并操作,而不是一直遍历前面和后面的节点,从而使得释放也是O(1)的复杂度。那么这样的合并操作足够吗,会不会出现相邻block本可以合并但没有合并的情况?当然是不会出现的。这不难想明白,如果每次释放都执行上述的合并逻辑,那么就不可能出现合并不彻底的情况。举例来说,a b c三个相邻的block,释放c的时候,b若处于free状态,那么b、c会合并,此时a不可能是free的,因为如果a是free的,那么释放b的时候就会将它合并。
4.6 内存块的地址对齐申请
地址对齐这种申请方式相对少见一些,但也有些应用确实存在这样的要求,比如申请cacheline 对齐的内存,存放关键数据,从而优化性能。对此,提供了tlsf_memalign_offs 实现地址对齐申请。先说明一下这个函数的几个参数:
- tlsf:指向tlsf堆(也即指向
control_t ) - align:地址对齐参数
- size:要申请的内存的大小
- data_offset:偏移参数,需要注意的是,该参数需要是
4 字节的整数倍,可以为0。不管传入的值是否满足对齐要求,函数总会进行向上对齐操作。其含义具体是,返回给用户的ptr 指针指向可用内存的起始,则((unsigned int)ptr + data_offset_aligned) % align == 0 ,为了方便后面的叙述,不妨把该条件称为偏移对齐条件 。
在分析源码之前,还得做一些铺垫。若是我们自己设计实现这个函数,应该怎么做呢?不难想到的是,我们肯定要申请比参数指定的size 更大的内存,因为很难保证偏移对齐条件 一申请就满足,不太可能这么巧合。只有申请足够大的内存,才能在不满足偏移对齐条件 的时候,通过调整返回给用户的地址来达成条件。这里所谓的调整 ,实质上就是将最初返回给用户的地址向后 挪一挪。
向后挪一挪必然导致前面空出一块,这空出来的内存应该怎么处理,是任其浪费还是归还给堆?答案是只能 归还给堆,理由如下:
- 避免内存浪费(当然,这不能解释"只能")
- 还有一个刚性的理由,free的时候并没有对地址对齐申请的情况做特殊处理,仍然把按照地址对齐申请的内存块按一般的内存块归还,因此它必须是一般的内存块,不允许前面还有一个gap。否则tlsf对元数据的处理就会出错。
既然要归还给堆,那么空出来的内存的大小就是有要求的,比如只空出4 字节是没法归还给堆的,因为最小的block 也不止4 字节。源码中要求这个空出来的大小不小于sizeof(block_header_t) ,这个大小就足够进行归还操作了。
现在还剩下一个问题,解决了,我们的思路就通了。这个问题就是,申请足够大 的内存,到底是多大。这个问题用文字解释很麻烦,还是得上图(看到这里的同学,我画了这么多图,你们不点个赞心里过的去嘛?)。当data_offset_aligned < align 的时候,一般最初申请出的内存,大体可以归为以下两种位置分布:
再看data_offset_aligned > align 的时候:
不难看出来,有这么个结论:
现在是时候过一遍源码了:
void* tlsf_memalign_offs(tlsf_t tlsf, size_t align, size_t size, size_t data_offset)
{
control_t* control = tlsf_cast(control_t*, tlsf);
const size_t adjust = adjust_request_size(size, ALIGN_SIZE);
const size_t off_adjust = align_up(data_offset, ALIGN_SIZE);
const size_t gap_minimum = sizeof(block_header_t) + off_adjust;
const size_t size_with_gap = adjust_request_size(adjust + align + gap_minimum - off_adjust, align);
const size_t aligned_size = (adjust && align > ALIGN_SIZE) ? size_with_gap : adjust;
block_header_t* block = block_locate_free(control, aligned_size);
tlsf_assert(sizeof(block_header_t) == block_size_min + block_header_overhead);
if (block)
{
void* ptr = block_to_ptr(block);
void* aligned = align_ptr(ptr, align);
size_t gap = tlsf_cast(size_t,
tlsf_cast(tlsfptr_t, aligned) - tlsf_cast(tlsfptr_t, ptr));
if ((gap && gap < gap_minimum) || (!gap && off_adjust && align > ALIGN_SIZE))
{
const size_t gap_remain = gap_minimum - gap;
const size_t offset = tlsf_max(gap_remain, align);
const void* next_aligned = tlsf_cast(void*,
tlsf_cast(tlsfptr_t, aligned) + offset);
aligned = align_ptr(next_aligned, align);
gap = tlsf_cast(size_t,
tlsf_cast(tlsfptr_t, aligned) - tlsf_cast(tlsfptr_t, ptr));
}
if (gap)
{
tlsf_assert(gap >= gap_minimum && "gap size too small");
block = block_trim_free_leading(control, block, gap - off_adjust);
}
}
return block_prepare_used(control, block, adjust);
}
最后再补充说明一下,最终,局部变量gap 表示的含义如下:
tlsf_memalign 在tlsf_memalign_offs 的基础上做了一个简单的封装,也就是设置data_offset_aligned 为0:
void* tlsf_memalign(tlsf_t tlsf, size_t align, size_t size)
{
return tlsf_memalign_offs(tlsf, align, size, 0);
}
4.7 tlsf堆的完整性检测
所谓堆的完整性 ,指的是管理堆的元数据是否遭到破坏。通常发生堆内存溢出时会导致堆的完整性被破坏。对此,tlsf 提供了一些堆完整性检测的接口:
tlsf_check 从control_t 出发,验证其正确性,并进一步遍历所有空闲链表,验证其中每个空闲块的元数据的正确性:
int tlsf_check(tlsf_t tlsf)
{
int i, j;
control_t* control = tlsf_cast(control_t*, tlsf);
int status = 0;
for (i = 0; i < FL_INDEX_COUNT; ++i)
{
for (j = 0; j < SL_INDEX_COUNT; ++j)
{
const int fl_map = control->fl_bitmap & (1 << i);
const int sl_list = control->sl_bitmap[i];
const int sl_map = sl_list & (1 << j);
const block_header_t* block = control->blocks[i][j];
if (!fl_map)
{
tlsf_insist(!sl_map && "second-level map must be null");
}
if (!sl_map)
{
tlsf_insist(block == &control->block_null && "block list must be null");
continue;
}
tlsf_insist(sl_list && "no free blocks in second-level map");
tlsf_insist(block != &control->block_null && "block should not be null");
while (block != &control->block_null)
{
int fli, sli;
tlsf_insist(block_is_free(block) && "block should be free");
tlsf_insist(!block_is_prev_free(block) && "blocks should have coalesced");
tlsf_insist(!block_is_free(block_next(block)) && "blocks should have coalesced");
tlsf_insist(block_is_prev_free(block_next(block)) && "block should be free");
tlsf_insist(block_size(block) >= block_size_min && "block not minimum size");
mapping_insert(block_size(block), &fli, &sli);
tlsf_insist(fli == i && sli == j && "block size indexed in wrong list");
block = block->next_free;
}
}
}
return status;
}
tlsf_check_pool 从头开始遍历所有内存块(包括空闲和非空闲),并调用integrity_walker 检查所有block 的完整性:
int tlsf_check_pool(pool_t pool)
{
integrity_t integ = { 0, 0 };
tlsf_walk_pool(pool, integrity_walker, &integ);
return integ.status;
}
void tlsf_walk_pool(pool_t pool, tlsf_walker walker, void* user)
{
tlsf_walker pool_walker = walker ? walker : default_walker;
block_header_t* block =
offset_to_block(pool, -(int)block_header_overhead);
while (block && !block_is_last(block))
{
pool_walker(
block_to_ptr(block),
block_size(block),
!block_is_free(block),
user);
block = block_next(block);
}
}
static void integrity_walker(void* ptr, size_t size, int used, void* user)
{
block_header_t* block = block_from_ptr(ptr);
integrity_t* integ = tlsf_cast(integrity_t*, user);
const int this_prev_status = block_is_prev_free(block) ? 1 : 0;
const int this_status = block_is_free(block) ? 1 : 0;
const size_t this_block_size = block_size(block);
int status = 0;
(void)used;
tlsf_insist(integ->prev_status == this_prev_status && "prev status incorrect");
tlsf_insist(size == this_block_size && "block size incorrect");
integ->prev_status = this_status;
integ->status += status;
}
上述堆完整性检测接口也是更上层接口的基础,更多关于堆调试的内容会在后续博客介绍。
参考
[1] esp32 heap 内存管理简析 [2] esp-idf [3] GitHub - mattconte/tlsf: Two-Level Segregated Fit memory allocator implementation. [4] LiteOS内存管理:TLSF算法 [5] TLSF算法分析
|