TLSF内存池管理结构示意图:
TLSF控制器支持对多内存池的管理,但LVGL只使用一个内存池
内存池存储结构示意图
+-------------------+
| lv_tlsf_t |
| block_header_t | - tlsf control structure
+-------------------+
| Pool Header |
| Ctrl Block | - block control_t
+-------------------+
| Free Block 1 |
+-------------------+
| Used Block 1 |
+-------------------+
| Free Block 2 |
+-------------------+
| Used Block 2 |
+-------------------+
| Free Block 3 |
+-------------------+
| ... |
+-------------------+
| Sentinel Block |
+-------------------+
TLSF一二级索引表:
FL为一级索引,SL为二级索引
通常一级索引值都指的是2的指数级数,例如2^7这一级索引管理的是2^7 ~ 2^8范围内的内存块。
二级索引是在一级索引这个大范围下进一步细分各级内存块,细分程度称为二级颗粒度,颗粒度通常为2的指数级,例如颗粒度为5指的是将2^7 ~ 2^8范围内的内存块细分为32(2^5)级,每一级范围大小为2^2,这里只是举例2^7这个一级索引范围内,具体可以查看下表中详细描述。
在这里最核心的是计算一二级索引值,索引值通常为2的指数级。
TLSF一二级索引表
\SL 0 1 2 3 4 ... (MAXn = 2^(SL_INDEX_COUNT_LOG2-1))(二级颗粒度:SL_INDEX_COUNT_LOG2)
FL\-----------------------------------------------------------------------------------------------------
31|2^31+0*2^25 | 2^31+1*2^25 | 2^31+2*2^25 | 2^31+3*2^25 | 2^31+4*2^25 | 2^31+9*2^25 | 2^31+n*2^25 | 0000 0000 0000 0000B = 无空闲块
.| |
.| ... | ... | ... | ... | ... | ... | ... | ......
.| |
16|2^16+0*2^11 | 2^16+1*2^11 | 2^16+2*2^11 | 2^16+3*2^11 | 2^16+4*2^11 | 2^16+9*2^11 | 2^16+n*2^11 | 0000 0000 0000 0000B = 无空闲块
.| |
.| ... | ... | ... | ... | ... | ... | ... | ......
.| |
9| 512 | 528 | 544 | 560 | 576 | ... | 2^6+n*2^4 | 0100 0000 0000 0000B = 528-544 区间有空闲块
8| 256 | 264 | 272 | 280 | 288 | ... | 2^6+n*2^3 | 0001 0100 0000 0000B = 280-288 & 296-304 区间有空闲块
7| 128 | 132 | 136 | 140 | 144 | ... | 2^7+n*2^2 | 1100 0100 0000 0000B = 128-132 & 132-136 & 148-152 区间有空闲块
2^n| MAXn = FL_INDEX_MAX; MINn = FL_INDEX_SHIFT;(一级索引数量: FL_INDEX_COUNT = FL_INDEX_MAX - FL_INDEX_SHIFT + 1)
| Ex:when LV_MEM_SIZE == 48K
| FL_INDEX_MAX = 16
| FL_INDEX_SHIFT = ((SL_INDEX_COUNT_LOG2 = 5) + (ALIGN_SIZE_LOG2 = 2));
上表中:各级取值范围例似2^7+n*2^2式子中,2^7是该二级区间范围的起始值,
n*2^2表明各个区间,n最大为MAXn = (2^SL_INDEX_COUNT_LOG2)-1,
SL_INDEX_COUNT_LOG2为二级链表的颗粒度,既把二级整级范围划分为多少等分,其取值是一个经验值,32位MCU中通常取4/5,既16份/32份
以SL_INDEX_COUNT_LOG2 = 5为例,划分为32等份,一级索引2^7范围128-256,每份为128/32 = 4,既2^2
一级索引: fl_bitmap = fl_bitmap中的每个bit记录每级二级链表中是否有空闲块,1=对应二级列表中有空闲块,0=对应整个二级列表范围内都被使用
Ex:基于上述LV_MEM_SIZE = 48K时的参数举例,fl_bitmap = 00 0000 1010B
表明在2^8 & 2^9两个二级链表中有空闲块
二级索引: sl_bitmap[FL_INDEX_COUNT] = sl_bitmap[n]记录二级链表中哪个区间有空闲块, n的最大值MAXn = FL_INDEX_COUNT - 1, n = 0时sl_bitmap[0]表述2^FL_INDEX_SHIFT这一级
Ex:基于上述LV_MEM_SIZE = 48K时的参数举例,sl_bitmap[1] = 0000 0000 0000 0000 0000 0000 0010 0010B
表明在2^8这一级链表中有两个细分区间上有空闲块分别是528-544区间和592-608区间
根据内存块大小将内存块插入对应链表中,计算一二级索引Index(fl,sl):
1.计算内存块大小size的最高位1所在index
2.index 右移 FL_INDEX_SHIFT 既得一级链表索引
3.计算二级链表索引的公式: (((size - 2^fl)*2^SL_INDEX_COUNT_LOG2) / 2^fl) = (size - 2^fl)/(2^fl/2^SL_INDEX_COUNT_LOG2)、
= (size - 2^fl)/2(fl-SL_INDEX_COUNT_LOG2)
lv_init() -> lv_mem_init_builtin() ->
/* LVGL动态内存池:分配一个大数组来存储动态分配的数据
* #define LV_USE_BUILTIN_MALLOC 1
* #define LV_MEM_SIZE (48U * 1024U)
* #define LV_ATTRIBUTE_LARGE_RAM_ARRAY __attribute__((section(".lvgl.mem")))
* #define MEM_UNIT uint32_t
* */
static LV_ATTRIBUTE_LARGE_RAM_ARRAY MEM_UNIT work_mem_int[LV_MEM_SIZE / sizeof(MEM_UNIT)];
memset(work_mem_int, 0, sizeof(work_mem_int));
static lv_tlsf_t tlsf = lv_tlsf_create_with_pool((void *)work_mem_int, LV_MEM_SIZE);
在lv_conf.h中使能了宏 LV_USE_BUILTIN_MALLOC 后,就可以使用LVGL内置动态内存了,注意必须要设置好宏 LV_MEM_SIZE ,其必须大于2kb。
LVGL-tlsf实现在lv_tlsf.c/lv_tlsf.h文件中
初始创建内存池,根据源码分析,大致分两步:
一、创建TLSF管理器也叫分配器并初始化(control_t),TLSF支持多内存池管理,但LVGL只使用一个内存池;
二、创建并初始化内存池管理器或叫块头(block_header_t),将块头以节点的形式插入TLSF管理器链表中,并更新一二级索引。
TLSF管理的主题就是下面结构体,其所占内存大小由 FL_INDEX_COUNT、SL_INDEX_COUNT 决定,这两值的具体定义详见下述枚举注释信息。
enum tlsf_public {
/*
* 块大小的线性细分数量的Log2值即(2^5=32bits)。更大的值在控制结构中需要更多的内存,典型的值4或5
*/
/*
`SL_INDEX_COUNT_LOG2` 表示二级索引的数量的对数。它的值决定了 TLSF 内存池中二级索引的数量。
在 TLSF 内存池中,内存块的大小是按照对齐大小递增的。每个一级索引 `fl` 对应一定范围的内存块大小,而每个二级索引 `sl` 则对应更细粒度的内存块大小范围。
二级索引的数量决定了内存块大小的粒度。较大的二级索引数量可以提供更细粒度的内存块大小范围,但会增加内存池的管理开销。较小的二级索引数量可以减少管理开销,但会限制内存块大小的粒度。
`SL_INDEX_COUNT_LOG2` 的值是根据具体的应用需求和性能考虑来设置的。一般来说,它的值应该选择一个合理的范围,以满足应用对内存块大小的需求,并在内存池的管理开销和性能之间做出权衡。
例如,如果 `SL_INDEX_COUNT_LOG2` 的值为 4,那么二级索引的数量就是 2^4 = 16。这意味着 TLSF 内存池将支持 16 种不同大小的内存块范围,从而提供更细粒度的内存块大小选择。
*/
SL_INDEX_COUNT_LOG2 = 5,
};
/* Private constants: do not modify. */
/**
* 常量,不得修改
* 这些枚举各成员的定义和计算是为了在TLSF分配器中对内存块进行分类和管理。
* 通过这些成员的值,分配器可以根据内存块的大小快速将其分配到相应的索引中,以提高内存分配的效率和利用率。
***/
enum tlsf_private {
/**
* 这个成员表示内存块的对齐大小的对数值。
* 在64位系统中,内存块的对齐大小为8字节,所以ALIGN_SIZE_LOG2等于3;
* 在32位系统中,内存块的对齐大小为4字节,所以ALIGN_SIZE_LOG2等于2
**/
#if defined (TLSF_64BIT)
/* All allocation sizes and addresses are aligned to 8 bytes. */
ALIGN_SIZE_LOG2 = 3,
#else
/* All allocation sizes and addresses are aligned to 4 bytes. */
/*所有分配大小和地址都对齐为4字节*/
ALIGN_SIZE_LOG2 = 2,
#endif
/*这个成员表示内存块的对齐大小。它的值等于2的ALIGN_SIZE_LOG2次方,也就是8字节或4字节,取决于系统位数,符合物理内存的擦写实际*/
ALIGN_SIZE = (1 << ALIGN_SIZE_LOG2),
/**
* 我们支持的内存分配大小最大为(1 << FL_INDEX_MAX)位。
* 然而,由于我们按线性方式细分第二级列表,并且我们的最小大小粒度为4字节,
* 因此对于小于SL_INDEX_COUNT * 4或(1 << (SL_INDEX_COUNT_LOG2 + 2))字节的大小,创建第一级列表是没有意义的,
* 因为我们将尝试将大小范围分割为比可用槽位更多的槽位。
* 相反,我们计算出最小阈值大小,并将所有小于该大小的块放入第0个第一级列表中。
**/
#if defined (TLSF_MAX_POOL_SIZE)
/**
* 这个成员表示最大的一级索引值。它决定了分配器能够处理的最大内存块大小。
* 在TLSF_MAX_POOL_SIZE被定义时,FL_INDEX_MAX等于TLSF_MAX_POOL_SIZE的对数(以2为底)的上限;
* 在64位系统中,FL_INDEX_MAX <= 32;在32位系统中,FL_INDEX_MAX <= 30。
*
**/
FL_INDEX_MAX = TLSF_LOG2_CEIL(TLSF_MAX_POOL_SIZE),
#elif defined (TLSF_64BIT)
/*
** TODO: We can increase this to support larger sizes, at the expense
** of more overhead in the TLSF structure.
*/
FL_INDEX_MAX = 32,
#else
FL_INDEX_MAX = 30,
#endif
/**
* 这个成员表示二级索引的数量。
* 二级索引用于将内存块按照大小进行分类和管理。
* SL_INDEX_COUNT的值等于2的SL_INDEX_COUNT_LOG2次方。
**/
SL_INDEX_COUNT = (1 << SL_INDEX_COUNT_LOG2),
/**
* 这个成员表示一级索引的偏移量。
* 用于通过内存块大小换算对应的一级索引时右移偏移
* 假设 SL_INDEX_COUNT_LOG2 的值为 5,ALIGN_SIZE_LOG2 的值为 2,那么 FL_INDEX_SHIFT 的值将为 6。
* 这意味着,通过将一个内存块的大小右移 7 位,就可以得到对应的一级索引 fl。
* 例如,一个大小为 128 字节的内存块,右移 7 位后得到 1,表示它属于一级索引为 1 的范围内。
**/
FL_INDEX_SHIFT = (SL_INDEX_COUNT_LOG2 + ALIGN_SIZE_LOG2),
/**
* 这个成员表示一级索引的数量。
* 一级索引用于将内存块按照大小进行分类和管理。
* FL_INDEX_COUNT的值等于FL_INDEX_MAX减去FL_INDEX_SHIFT再加上1。
**/
FL_INDEX_COUNT = (FL_INDEX_MAX - FL_INDEX_SHIFT + 1),
/**
* 这个成员表示最小的内存块大小。
* 它的值等于2的FL_INDEX_SHIFT次方,也就是一级索引对应的内存块大小
**/
SMALL_BLOCK_SIZE = (1 << FL_INDEX_SHIFT),
};
typedef struct control_t {
/* Empty lists point at this block to indicate they are free. */
/**
* 这是一个特殊的内存块,用于表示空闲链表为空。
* 当一个链表为空时,它的指针会指向block_null。
* 同理,通过检查链表的指针是否等于block_null,可以判断链表是否为空
***/
block_header_t block_null;
/* Bitmaps for free lists. */
/**
* 这是一个位图,用于记录每个一级索引对应的二级索引链表是否为空。
* 位图的每一位表示一个二级索引链表,位图的第n位为1表示第n个二级索引链表为非空,为0表示第n个二级索引链表为空
**/
unsigned int fl_bitmap;
/**
* 这个数组用于记录每个一级索引对应的二级索引链表中是否存在空闲内存块。
* 数组的每个元素是一个位图,表示一个一级索引对应的二级索引链表中的每个内存块是否为空闲状态。
* 位图的每一位表示一个内存块,位图的第n位为1表示第n个内存块为非空闲状态,为0表示第n个内存块为空闲状态。
**/
unsigned int sl_bitmap[FL_INDEX_COUNT];
/* 二级链表的指针数组,空闲链表的头 Head of free lists.
* 这个二维数组,用于存储每个一级索引对应的二级索引链表的头指针。
* 数组的第一维表示一级索引,第二维表示二级索引。
* 每个元素是一个指针,指向对应的二级索引链表的头节点。
* 通过这个数组,可以快速找到每个一级索引对应的二级索引链表的头节点,从而可以快速访问和操作链表
**/
block_header_t * blocks[FL_INDEX_COUNT][SL_INDEX_COUNT];
} control_t;
block_null.next_free = block_null;
block_null.prev_free = block_null;
一、一二级索引清零,fl_bitmap = 0;sl_bitmap[i] = 0;
二、二级索引中,各级内存块链表起始节点指向空,blocks[i][j] = block_null;
static void control_constructor(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分配器添加内存池*/
/**
* tlsf: 分配器(实体存储在内存块的开头位置),可以管理多个内存池
* mem: 指向内存块数据存储区域的起始地址
* bytes: 数据存储区大小
**/
lv_pool_t lv_tlsf_add_pool(lv_tlsf_t tlsf, void * mem, size_t bytes)
主要由上述接口实现添加操作,主要步骤是,在块头位置创建块管理器并初始化,将其插入到TLSF管理器中更新一二级索引。
参考文档:
TLSF Github官方库
TLSF官方文档库
TLSF官网
DSA之TLSF内存分配器源码分析 - 知乎 (zhihu.com)
实时系统动态内存算法分析dsa(二)——TLSF代码分析_lv tlsf-CSDN博客
TLSF内存分配英文文档