当前位置: 首页 > news >正文

TLSF内存算法适配HTOS

TLSF 内存分配算法详解

在本人写的HTOS基础上进一步优化了原本的静态堆内存分配方法,使用了TLSF进行内存分配,在快速随机分配压测实测下不会出现异常情况。
代码开源于:https://github.com/yeyue1/HTOS
在这里插入图片描述


1. 概念简介

TLSF(Two-Level Segregated Fit)是一种面向实时系统的动态内存分配器设计,旨在在分配(malloc)和释放(free)操作上提供恒定时间复杂度 O(1) 的响应时间,同时保持较低的内部/外部碎片。TLSF 由 Matthew Wilson 等人提出,常用于嵌入式或实时系统中对延迟有严格要求的场景。

核心思想:通过两级分离空闲列表(一级按区间级别划分,二级按子区间细分)并结合位图(bitmap)快速定位合适的空闲块,从而将查找时间限定为常数级别。
在这里插入图片描述
这是从网上找的一个示例。很好的解释了TLSF算法是如何进行分配内存的。

2. 设计目标

  • 分配和释放的最坏时间复杂度为 O(1)。
  • 低延迟抖动(低抖动性),适合实时系统。
  • 保持合理的内存利用率与碎片控制。
  • 简单的分离列表与位图实现,便于在资源受限设备上部署。

3. 基本数据结构

TLSF 的关键数据结构包括:

  • Block(内存块)

    • 每个块有一个头部,通常包含块大小(带标志位,如“是否已使用”)、前一物理块的大小或指针(用于快速合并)等。
    • 空闲块会包含用于链表的指针(next_free / prev_free)。
  • 两级索引(FL/SL)

    • FL(First Level index):按块大小的高阶位分组,通常类似对数分组(例如按 2 的幂划分区间)。
    • SL(Second Level index):在每个 FL 组内细分成若干子区间,用于更精细的匹配。
  • 空闲列表数组:一种二维数组或等价结构,索引为 [FL][SL],每个单元保存对应大小区间的空闲块链表头。

  • 位图(bitmaps)

    • FL bitmap:记录哪些 FL 索引目前有空闲块(用于快速跳转到下一个非空 FL)。
    • 对每个 FL 的 SL bitmap:记录该 FL 下哪些 SL 有空闲块。

这些位图通常用原子或整数操作维护,从而能以常量时间查找下一个可用区间。

4. 大小到索引的映射(核心映射函数)

将请求大小 size 映射到 (fl, sl):

  1. 如果 size 小于最小可分配块(min_block_size),就上调到 min_block_size(并对齐)。
  2. fl = floor_log2(size)(找到 size 的最高位,表示大概的2的幂);
  3. 在该 fl 下,用 size 的低位来计算 sl(即 size 在 [2^fl, 2^{fl+1}) 区间内的偏移),常见做法是:
    • normalization = size - 2^fl
    • sl = normalization >> (fl - SL_BITS)
      其中 SL_BITS 是二级分割的位数(例如 SL_BITS=4 则每个 FL 有 16 个子区间)。

这样可以把连续的大小区间映射到二维索引,保证类似大小被放到同一或相邻的 SL,从而降低碎片和提升命中率。

实现时还有细节:

  • 对齐(alignment)会影响映射:在计算索引前先做向上对齐。
  • 对于非常大的请求,直接映射到最高 FL,并将 sl 设为某个边界值。

5. malloc(分配)流程(高层)

  1. 根据请求大小计算适当的 (fl, sl)。
  2. 在 SL 对应位图中查找当前 SL 或更高 SL 是否存在空闲块(通过先查看 SL bitmap,然后可能向上查找 FL bitmap)。
    • 查找过程:先查看该 FL 的 SL 位图(找到 >= sl 的第一个 set 位),若找不到则在 FL 位图中找到下一个有空闲块的 FL,然后在其 SL 位图中查找第一个 set 位。
  3. 找到目标空闲块后,从对应链表中移除它。
  4. 如果块比请求大小大很多,可以拆分:保留一部分给分配(并标记为已用),剩余部分作为新的空闲块放入合适的 FL/SL。
  5. 返回用户指针(通常是块头之后的地址)。

伪代码(简化):

  • alloc(size):
    • size = align_up(max(size, min_block_size))
    • (fl, sl) = size_to_index(size)
    • (fl2, sl2) = find_suitable(fl, sl) // O(1) via bitmaps
    • if not found: return NULL
    • block = remove_free_block(fl2, sl2)
    • if block.size - size >= min_split_size: split block and insert remainder
    • mark block as used
    • return block_to_user_ptr(block)

6. free(释放)流程(高层)

  1. 将块标记为自由。
  2. 与前后物理邻居检查是否为空闲块;如果是则合并(coalesce)为更大的空闲块。
  3. 将合并后的空闲块插入到对应的 FL/SL 空闲链表,并在位图上置位相应 bit。

伪代码(简化):

  • free(ptr):
    • block = user_ptr_to_block(ptr)
    • mark block as free
    • if prev_phys_is_free: block = merge(prev, block)
    • if next_phys_is_free: block = merge(block, next)
    • (fl, sl) = size_to_index(block.size)
    • insert_free_block(block, fl, sl)
    • set_bitmap(fl, sl)

7. 为什么是 O(1)?

  • 位图允许用常数时间的位运算(比如 ffs / clz / tzcnt)来找到第一个置位。查找空闲 FL 与 SL 都是常量复杂度的位操作。插入和删除空闲块是链表操作,也是 O(1)。
  • 由于所有步骤(索引计算、位图查找、链表插入/删除、可能的拆分/合并)都是常数次操作,所以算法满足在最坏情形下的常数时间上限(在实现上依赖常数级 SL 数量与有效的位扫描指令)。

8. 实现细节与注意点

  • 对齐和最小块大小

    • 必须定义对齐(如 4 或 8 字节)并在所有请求上生效。最小块大小应能容纳空闲链表指针和必要头信息。
  • 块头和标志位

    • 通常把 “已用” 标志藏在大小字段的低位(利用对齐保证低几位为 0)。保存前一块是否空闲的信息可以通过前一块的大小或其他字段实现,便于合并。
  • 位图实现

    • FL bitmap 的位宽等于 FL 数量(例如如果支持 32 个 FL,用一个 32 位整数即可)。
    • SL 位图对每个 FL 保持一段位图(例如 16 或 32 位)。
    • 查找第一个 set 位需要高效指令:在 x86 中可使用 _BitScanForward / __builtin_ctz 等;在没有硬件支持的平台,可用小查表或手写查找(仍是常数时间,因为 SL 和 FL 的范围固定)。
  • 内存边界与大块处理

    • 对超出最大可索引范围的请求,可专门处理(例如直接放入单独的“超大块”链表,或将其映射到最高 FL)。
  • 并发/多线程

    • 原始 TLSF 设计面向单线程或需要外部同步的场景。若在多线程环境中使用,需要在 allocator 层加锁或使用更精细的锁分离策略(例如按 FL 分段锁),或者设计无锁变体(复杂且难以保证在任意平台上的 O(1) 原子性)。
  • 可移植性

    • TLSF 的常数时间假设依赖于能以 O(1) 找到位图第一个 set 位的操作。某些编译器/架构可能没有高效内建指令,这时实现者需要使用小范围查找表来保持常数时间属性。

9. 边界情况与常见陷阱

  • 内存耗尽:find_suitable 可能无法找到合适块,应返回 NULL 并在上层处理。
  • 频繁的小碎片:虽然 TLSF 减少了查找时间,但碎片仍可能在极端分配模式下出现,需结合拆分/合并策略与最小块控制来缓解。
  • 对齐需求很高的请求:可能导致内部碎片增多。
  • 多线程并发导致的竞态:必须一并考虑同步策略。

10. 小示例(参数说明)

  • 假设:SL_BITS = 4(每个 FL 有 16 个 SL),最小对齐 8 字节。
  • 请求 200 字节:
    • 找到 fl = floor_log2(200) = 7(2^7 = 128)
    • normalization = 200 - 128 = 72
    • sl = normalization >> (7 - 4) = 72 >> 3 = 9
    • 索引到 (fl=7, sl=9),在该单元或更高单元中查找第一个空闲块。

11. 性能与应用场景

  • 优点:

    • 实时性好,最坏时间复杂度可控(O(1))。
    • 分配/释放延迟低、抖动小,适合对时延敏感的系统(RTOS、嵌入式)。
    • 实现相对简单,空间开销由位图与空闲链表引入,但通常可控。
  • 缺点:

    • 并发支持需额外工作。
    • 对极端碎片模式并不能完全避免碎片产生。
    • 在没有快速位扫描指令的平台上,常数时间的实现需要额外工作(查表等)。
  • 常见使用场景:实时操作系统(RTOS)、嵌入式系统、游戏引擎某些子系统、低延迟服务。

12. 与其他分配器比较

  • ptmalloc(glibc 的 malloc)和 jemalloc:这些通常优化了总体吞吐量与多线程并发,在一般用途下表现优秀,但最坏时间通常不是 O(1),可能不适合硬实时约束。
  • dlmalloc:是常见的通用分配器,注重平均性能与空间利用。
  • TLSF:在需要对延迟有严格上界时更合适,但不是对吞吐量或多线程场景的万能最优解。

13. 参考与进一步阅读

  • TLSF 原始论文 / 报告(英文):Matthew Wilson 等人的 “TLSF: A Dynamic Memory Allocator for Real-Time Systems”(可在线检索以阅读详尽实现细节与证明)。
  • 开源实现:有若干开源 TLSF 实现,可在 GitHub 上搜索 “tlsf allocator” 以获取示例代码与移植版本。

14. 总结

TLSF 是一种为实时/嵌入式场景设计的分配器,通过二级分离的空闲列表和位图实现,能将分配与释放的最坏时间限定为常数 O(1)。实现时的关键点是合理设计 FL/SL 映射、位图实现、块头布局及对齐与拆分/合并策略。对于需要确定性延迟的系统,TLSF 是一个优秀的选择。

http://www.dtcms.com/a/365306.html

相关文章:

  • React实现列表拖拽排序
  • PyTorch实战(6)——模型微调详解
  • 落地页测试case(Android视角)
  • Redis突然挂了,数据丢了多少?就看你用RDB还是AOF
  • SecureCRT v9.5.2 Mac SSH终端操作工具
  • C++从入门到实战(二十一)List迭代器实现
  • 行业分享丨基于SimSolid的大型汽车连续冲压模具刚度分析
  • 【Axure高保真原型】区间缩放柱状图
  • JavaScript箭头函数与普通函数:两种工作方式的深度解析
  • android studio打开Android Device Monitor
  • Java 鲁棒性:支撑企业级应用稳定运行的核心密码
  • websoket使用记录
  • 马斯克砸钱造AI,却败给最low的“让离职员工轻松拷走代码”
  • OpenLayers 入门篇教程 -- 章节三 :掌控地图的视野和交互
  • 《计算机网络安全》实验报告一 现代网络安全挑战 拒绝服务与分布式拒绝服务攻击的演变与防御策略(1)
  • 【全息投影】幻影成像技术在展厅中的创新应用
  • 求单源最短路(Dijkstra 算法-迪杰斯特拉算法,SPFA)
  • word文档封面中文件编号等标题和内容无法对齐
  • 关于QSharedPointer
  • 清理磁盘:卸载 GitLab CI/CD Multi-Runner 删除docker相关文件
  • linux服务开机自启动之二(forking方式)
  • undo-log
  • 用 “走楼梯” 讲透动态规划!4 个前端场景 + 4 道 LeetCode 题手把手教
  • MySQL的utf8 、utf8mb3 和 utf8mb4 的区别和排序规则
  • 摄像头现代实现WIFI远程实操画面移动
  • Flutter环境搭建全攻略之-Macos环境搭建
  • 【Layui】Layui Table 底部合计栏实现方案
  • CentOS安装vulhub靶场
  • 不同数据类型for循环
  • 从一道面试题开始:如何让同时启动的线程按顺序执行?