内存管理-TLSF算法
- 前言
- TLSF算法:
- 为什么内存又叫内存“块”:
- O(1)查找空闲块:
- 确定fl:
- 确定sl:
- 提级申请:
- 分割块:
- 空闲块如何串成链表?
- 减少外部碎片:
- 查找上下块:
- 合并块:
- 疑问:
前言
Vue框架:
从项目学Vue
OJ算法系列:
神机百炼 - 算法详解
Linux操作系统:
风后奇门 - linux
C++11:
通天箓 - C++11
Python常用模块:
通天箓 - Python
TLSF算法:
- 实时操作系统的内存管理算法
- TLSF的分配速度快,但是相对内存利用率低,容易出现内部碎片,比较常用于嵌入式场景。
为什么内存又叫内存“块”:
-
假设我们现在新买了一个16G内存条,在我们没安装到主板前,其实使用内存就不足16G:
- 初始内存中有两个指针,一个指向首个可用空间首地址,一个指向内存条“末尾”
- 首个可用空间大小为16G-2*sizeof(pointer),而内存条末位的这个“哨兵块”大小为0
-
由此有了内存“块”称呼来源1:初始内存只有两块,“空闲块”&“哨兵块”
-
在16G内存中找空闲块指针很简单,直接111…111111,但是找哨兵块指针就需要技巧了
- 看图中哨兵块指针近邻内存块指针,所以只需要111…111111 - sizeof(pointer)就找到了
- 这里sizeof(pointer)是指以111…111111作为首地址的单元的size
- 也就是说我们需要知道当前指针所在“单元”的size,而这个size可不always等于sizeof()的结果,如分配拿到了64字节,但是我们没用完64字节,只存了一个int,此时sizeof()=4
- 所以除了这两个指针之外,其他内存块都需要一个size字段,表明自己大小
- 到这,我们可画出内存的物理“块”的结构初稿(哨兵块size不准确):
-
由此有了内存“块”称呼来源2:每个内存块都有size字段规定本块大小
-
我们借助一个物理块的size字段可以轻松找到其物理地址的下一块,那么上一块还需要借助其他信息吗?
- 如果只要求到达上一块,则当前块块首指针-1就到了上一块的最后一个字节
- 但是要求到达上一块的块首地址,我们还是需要借助指针,这个指针就在上一块的最后sizeof(Pointer)个字节
- 由此我们可以画出物理空间上的内存块定稿:
-
为了防止用户直接操作物理内存,OS给用户可操控的地址/指针,其实是size邻接的下一地址:
-
当用户用满了这个Busy Block区域,prev_phys_block字段被用作用户数据,就不再指向物理块首地址了,
-
这个时候我们需要通知下一个物理块,prev_phys_block指针失效
-
通知的方法很简单,由于每个物理块大小size+sizeof(size字段)都是8Bytes或者4Bytes的倍数,所以每个物理块的size最后两位必定是0。
-
用size字段第0位标识当前物理块是不是空闲,第1位标识上一物理块是不是空闲。1是空闲,0是被用户使用。这就是为什么要求申请到的内存必须是4或者8的整数倍。
-
当上一个物理块被malloc()给用户使用,走之前需要把自己的size第0位置为0,把下一个物理块的size第1位置为0
-
每个物理块在查找上一个物理块块首时,必须先看size第1位是不是1,是1的话pre_phy_block才有效
-
由此有了内存“块”称呼来源3:每个内存块都要知道上一个块是不是被用户使用
-
最后,就像我们写链表时候的逻辑地址和物理地址一样,在控制物理块的时候,我们要借助struct做一些逻辑上的改变:
- 每个物理块开头是size,结尾是pre_phys_blocks
- 逻辑Block struct在物理块基础上多加了上一块的结尾,prev_phys_blocks,用于找到上一块的struct起始地址
- struct的起始地址始终在物理块的起始地址前一个指针大小
-
由此有了内存“块”称呼来源4:os通过struct来管理每个内存块
O(1)查找空闲块:
-
根据牺牲空间才能换取时间的原则,TLSF有着设计优越的管理结构便于查找空闲块
- 空闲内存(上图size字段)相近的空闲块串成链表,最终有许多条空闲链表
- blocks[][]:二维数组,一级索引成为fl,二级索引称为sl,每个一级索引fl下都有同样个sl,一般是2^5个,blocks[fl][sl]存储空闲链表头
- fl_bitmap:位图,为1的位说明blocks[fl]下有空闲链表,0则无。
- sl_bitmap[]:位图数组,当确定fl后,sl_bitmap[fl]是位图,为1的位说明blocks[fl][sl]下有空闲链表
- 当blocks[fl][sl]不为None,则sl_bitmap[fl]第sl位必须1,则fl_bitmap第fl位必须1。
- 位图和blocks[][]必须时刻相符合,不然说明OS已经奔溃,内存条也受伤。
pub fn init_on_heap(mut tmp : Box<TLSFControl,Global>) -> Box<Self,Global>{ // TODO: YOUR CODE HERE tmp.fl_bitmap = 0; tmp.sl_bitmap = [0; FL_INDEX_COUNT]; for i in 0..FL_INDEX_COUNT { for j in 0..SL_INDEX_COUNT { tmp.blocks[i][j] = RawList::new(); } } tmp // END OF YOUR CODE }
-
TLSF规定,空闲内存不同块按照下面的原则链接到不同的Blocks[fl][sl]中:
-
fl = 0层存放空闲内存不超过256Bytes的空闲块
fl = 0,sl 有32个索引,256 / 32 = 8,则不同空闲范围对应sl如下:
/*fl = 0 sl[0]: 0 ~ 7 sl[1]: 8 ~ 15 sl[2]: 16 ~ 23 ... sl[30]: 240 ~ 247 sl[31]: 248 ~ 255 */
-
fl = 1层存放空闲内存介于256到511Bytes(256和511二进制最高位1占位同)的空闲块
511 - 256 + 1 = 256,256 / 32 = 8,则不同空闲范围对应sl如下:
/*fl = 1 sl[0]: 256 ~ 263 sl[1]: 264 ~ 271 sl[2]: 272 ~ 279 ... sl[30]: 596 ~ 403 sl[31]: 404 ~ 511 */
-
fl = 2层存放空闲内存介于512到1023Bytes(512和1023二进制最高位1占位同)的空闲块
1023 - 512 + 1 = 512,512 / 32 = 16,则不同空闲范围对应sl如下:
/*fl = 2 sl[0]: 512 ~ 527 sl[1]: 528 ~ 543 sl[2]: 544 ~ 559 ... sl[30]: 992 ~ 1007 sl[31]: 1008 ~ 1023 */
-
其余fl层分配相同
-
确定fl:
-
提出需求size大小空间,如何确定fl & sl:
-
首先要对size进行8字节对齐:
pub fn malloc<T>(&mut self, size: usize) -> *mut T { let adjust = if size > 0 { align_up(size, ALIGN_SIZE) } else { panic!("size must be greater than 0"); }; //size = adjust 或之后直接以adjust传参...
-
-
当size < 256Bytes时,先到fl = 0查找空闲块。
-
当size > 256Bytes时,根据公式查找空闲块:[ ]表示向下取整
f l = [ l o g x s i z e ] fl = [log_{x}^{size}] fl=[logxsize] -
上述公式从值相等角度可用取二进制最高位1所在位代替:
#[inline] fn ffs(word: usize) -> u32 { (word as u32).trailing_zeros() as u32 }
确定sl:
-
当size < 256Bytes时,sl = (size - 0) / 8
-
当size > 256Bytes时,根据公式查找空闲块:SLI就是每个fl对应sl数目取log2
s l = ( s i z e − 2 f l ) 2 f / 2 S L I sl = \frac{(size - 2^{fl})}{2^{f}/2^{SLI}} sl=2f/2SLI(size−2fl) -
也就是特殊在size<256时候,2^fl = 2^0 = 1 != 0
提级申请:
- 若此时申请大小为519的空闲内存:
- 519对8向上取整得到520
- 520二进制最高位是第9位,因为fl=0对应的256最高位是第8位,所以fl = 9-8+1=2
- sl = (520 - 512)/16 = 0
- 看上面表:sl[0]: 512 ~ 527,仿佛可用blocks[2][0]
- 但是由于不确定blocks[2][0]内空闲大小是512还是527,就有可能容不下520Bytes内容
- 确保每次申请的空闲块都能存储下size,我们向空闲更大的blocks发起申请
- 寻找空闲更大且最靠近size的空闲块:
- 从低到高查询sl_bitmap[fl]中第x位,要求x>sl,且x位上是1
- 如果在sl_bitmap[fl]中没有查到x,则要到fl_bitmap中查询
- 从低到高查询fl_bitmap中第x位,要求x>fl,且x位上是1
分割块:
-
假设现在内存条1024字节,初始两个指针占用8字节,则可用1008字节
1008字节分为大小是1008的可用块和大小是0的哨兵块
现在只申请8字节,根据上面查找fl和sl方法,肯定查到的空闲块是1008
-
我们不可能把远大于用于申请大小的空闲块直接给用户使用,这样内部碎片很多,所以需要分割块,将一个且不能分割出size是0的块。
-
给定Block struct A,分割为大小为a的B块 & 和大小不为0的C块:
- B块直接改改使用原A的size字段,分割后我们在物理层要为C块增加size,而这个size的开销是8
- 要确保A.size > a + 8,由于最小单元8字节,所以等价于A.size >= a + 8 + 8
-
确定C块size的起始位置:
- 用户data_ptr + 当前块size = 下一物理块size结尾
- 下一块size的起始位置 = 结尾 - 8
- 由于用户申请malloc到的内存块要写入用户数据,所以分割得到的C块的prev_phys_blocks失效,要设置C块size第1位为0,防止C块通过混乱的prev_phys_blocks访问其他内存。
还要设置C块的pre_phys_block,原A块下一块的pre_phys_block
-
如果在malloc阶段没有和8字节对齐,那么当前块的size最后2位必定失效,引起内存访问混乱。
空闲块如何串成链表?
-
上面我们分析之后发现size字段和用户使用的busy block已经占据了所有物理块。
那么链表的next & prev在哪里存储?
-
空闲块的busy block此时没有写入用户数据,那么我们可将next & prev写入size下面的busy block:
- 这样,拿到任意内存块,通过看其size最后两位,我们即确定有无prev & next,又确定可否访问上一物理块
减少外部碎片:
-
malloc的时候,我们分割的目的是减少内部碎片。
free的时候,我们尝试将物理上当前块和上下块合并为更大的块,目的是减少外部碎片。
查找上下块:
- 查找上一块:
- 先看size第0位是不是1
- 再利用prev_phys_block访问上一块的起始地址
- 查找下一块:
- 先向后移动当前块首地址指针,移动size-8个单位
- 再利用下一块size第1位判断下一块是不是空闲
合并块:
-
合并相当于BC块合成A块,A的大小是B.size + C.size + sizeof(size字段)=8
-
合并之后的A块空闲,除了本身的size最后两位要修改之外
还有C块下一块的pre_phys_block及其size最后2位
疑问:
- 开头说TLSF管理器在堆上,堆也在内存里面,为什么init_block()不考虑减去TLSF管理器的开销?
- 哨兵块的size字段到底占用内存吗?
我的理解哨兵块极其可能只是一个字节