# TLSF 概念與程式解析 > github [cin-cout/tlsf-bsd](https://github.com/cin-cout/tlsf-bsd) ## TLSF 架構 ### mapping 技術 TLSF 跟最基本的記憶體配置器不同,使用一個 free list 串接所有的 free block ,詳見[測驗五第一題](https://hackmd.io/@sysprog/linux2023-quiz5),而是依照 free block 的大小,將每堆類似大小的 free block 建立一個 free list。所以每次在 alloc 一個新的空間時,都直接去最適合的大小的 free list 中尋找 free block,從而實現 good fit,而 **mapping 技術就是用來尋找最適合的 free list**。 TLSF (Two-Level Segregated Fit) 中使用二級的結構,加快記憶體存取速度並減少記憶體碎片化 (fragmentation)。 :::warning a power of 2 的翻譯是「2 的冪」,不用加「次方」 ::: * 第一級:將記憶體劃分為 2 的冪大小的類別。例如,若第一級為 $2^6$,則將記憶體的範圍劃分為 $[2^6, 2^7)$。 * 第二級:將第一級所表示的範圍再劃分為 4 個等距離的區塊,從而善用記憶體空間 舉例來說,假設第一級為 $2^6$,表示記憶體的大小範圍為 $[64, 128)$。第二級會將這個範圍劃分為四個大小相等的區塊,每個區塊的大小為 16。這樣,第二級會將記憶體的大小範圍細分為 $[64, 80)$, $[80, 96)$, $[96, 112)$, $[112, 128)$。 > 注意: 這邊的意思不是用 fl 與 sl 將記憶體分 block ,它的意義是每一層 fl sl 都會儲存一個 free list,我們找到合適的 fl sl 是為了找大小最合適的 free list。 舉例:我們要 alloc 一個 size 為 $2^6 + 17$ 的空間,我們這邊先不加入提級層級的概念,後面會提到。mapping 會找到 fl sl (圖中 fl=2 sl=2)對應在 $2^6 + 17$ 的位置所記錄的 free list ,從這個 free list 中找 free block 分配空間。 <img src="https://hackmd.io/_uploads/HkgY6d1I3.png" width="70%" height="70%"> :::warning 注意用語,參見: * [資訊科技詞彙翻譯](https://hackmd.io/@sysprog/it-vocabulary) * [詞彙對照表](https://hackmd.io/@l10n-tw/glossaries) ::: ### 整體概念 這邊畫一張圖來講解運作中的 tlsf 可能會長怎樣,以便後續再講解程式碼時比較知道狀況。 ``` +------------+ |其他管理TLSL | second level |的參數 | / \ +------------+---------------------------> +-----------+ +-----------+ \ | block[][] | | fl 0 sl 3 | | fl 0 sl 0 | +------------+----+(free list a)<---------- |對應大小 a~b| ... |對應大小 w~x| first | free block |----|--+ +-----------+ +-----------+ level | (size a~b) | | | . +------------+ | | . | blockA | | | . +------------+----|--|----+(free list b)<-- +-----------+ +-----------+ | free block |<---|--|----|--+ |fl 32 sl 3 | |fl 32 sl 0 | | (size c~d) | | | | | |對應大小 c~d| ... |對應大小 y~z| +------------+ | | | | +-----------+ +-----------+ / | blockB | | | | | | | | | | | +------------+<---|--|----+ | | free block |----|--|-------+ | (size c~d) | | | +------------+ | | | blockC | | | +------------+<---| | | free block |-------+ | (size a~b) | +------------+ ``` ## [TLSF](https://github.com/jserv/tlsf-bsd) 實作 以下將解析老師實作的 [jserv/tlsf](https://github.com/jserv/tlsf-bsd) 。 ### 重要常數 64 位元系統對齊的 shift 為 3,亦即以 8 的倍數對齊,符合一個 word_size 大小。 64 位元系統對齊的 shift 為 2,亦即以 4 的倍數對齊,符合一個 word_size 大小。 ```c /* All allocation sizes and addresses are aligned. */ #define ALIGN_SIZE ((size_t) 1 << ALIGN_SHIFT) #if __SIZE_WIDTH__ == 64 #define ALIGN_SHIFT 3 #else #define ALIGN_SHIFT 2 #endif ``` ```c /* First level (FL) and second level (SL) counts */ /*表示使用 4 位元來表示 SL 索引的位移量。這意味著 SL 索引的範圍為 0 到 15。*/ /*通常這個值設定在 4 或 5 。*/ #define SL_SHIFT 4 /*每一層 FL 的 SL 數量*/ #define SL_COUNT (1U << SL_SHIFT) /*FL 最大的 index ,與系統為 64 或 32 有關。*/ /*可以增加其大小,以來處理更大的記憶體配置。*/ #define FL_MAX _TLSF_FL_MAX /*FL[1] 的位置,詳細解釋見下。*/ #define FL_SHIFT (SL_SHIFT + ALIGN_SHIFT) /*FL 的數量,詳細解釋見下。*/ #define FL_COUNT (FL_MAX - FL_SHIFT + 1) ``` **以下以 32 位元的系統舉例:** 雖然最多可以支持 (1 << FL_INDEX_MAX) 位元的大小分配。 但是,因為要以 4 為倍數對齊,所以小於 `SL_INDEX_COUNT * 4` 的大小建立一級列表沒有意義 (根本無法劃分出足夠數量且又滿足對齊的 SL )。 `SL_INDEX_COUNT * 4` = `(1 << (SL_INDEX_COUNT_LOG2 + 2 ))` = `(1 << (SL_INDEX_COUNT_LOG2 + 2 ))` = `(1 << (SL_INDEX_COUNT_LOG2 + ALIGN_SHIFT ))` 所以 FL_SHIFT = SL_SHIFT + ALIGN_SHIFT。 圖例: ``` 以 ALIGN_SHIFT 為 2 ,SL_SHIFT 為 4 舉例。 ┌─────────────────────┬─────────────┬──────────┬──────┐ │ ... | 256~128 | 64~128 | 0~64 | └─────────────────────┴─────────────┴──────────┴──────┘ f[2] f[1] f[0] 若無此機制,f[0] 只會為 0~1,f[1] 為 1~2。 ``` 所以 FL_COUNT 的演算法為 FL_MAX - FL_SHIFT + 1,因為要扣掉前面 SHIFT (0~64) 的位元已經整合為 FL[0], 加 1 則是把加上 FL[0]。 而由於 SHIFT (0~64) 的位元已經整合為 FL[0],FL[0] 區段不能適用於通用的演算法,所以需要 alloc 在 FL[0] 要額外定義,在這個例子中,<64 bytes ,我們定義為 BLOCK_SIZE_SMALL ,小於 BLOCK_SIZE_SMALL 的大小,我們會直接 alloc 在 FL[0]。 ```c #define BLOCK_SIZE_SMALL ((size_t) 1 << FL_SHIFT) ``` ### block 資料結構 先看 block 資料結構會比較理解前面一些關於 block 的常數。 #### block 的資料結構定義 這邊是對於記憶體配置器的概念的基本解釋,詳見[測驗五第一題](https://hackmd.io/@sysprog/linux2023-quiz5),先不考慮 prev。 此段解釋參考 [yanjiew](https://hackmd.io/@yanjiew/linux2023q1-quiz5)。 alloc 的空間為系統由 size 和 payload 組成。malloc 會回傳 payload 部份的指標。payload 為可以利用的空間,size 的值即為 payload 的大小。 ``` -------------------------------------------------- | size | payload | -------------------------------------------------- ``` 未配置的空間除了 size 外,後面還會接續 prev 和 next 二個指標,分別指向上一個和下一個未配置空間,size 的值即為 prev + next + 空閒位置的大小。 ``` -------------------------------------------------- | size | prev | next | .......... | -------------------------------------------------- ``` 上述二種狀態都可用 tlsf_block 結構來表示。size 為已配置和未配置空間都會使用的欄位,而 prev 和 next 只會在未配置空間使用。 ```c typedef struct tlsf_block { /* Points to the previous block. * This field is only valid if the previous block is free and is actually * stored at the end of the previous block. */ struct tlsf_block *prev; /* Size and block bits */ size_t header; /* Next and previous free blocks. * These fields are only valid if the corresponding block is free. */ struct tlsf_block *next_free, *prev_free; } tlsf_block_t; ``` 跟一般記憶體配置器最大的差別在多了 prev 這個指標,用來指向實體上的前一個 block,但他的儲存位置很特別,見下圖,取自 [esp-idf 的記憶體管理: TLSF 演算法](https://blog.csdn.net/gzxb1995/article/details/124504705)。 <img src="https://hackmd.io/_uploads/ByoT05SI3.png" width="50%" height="50%"> 這裡的 `prev_phys_block` 就是 prev,block 是可以找到 prev 的,但其實 prev 是儲存在實體上一個 block 內,當上一個 block 被 alloc 後寫入資料,是可能蓋掉 prev 的,因為 prev 只是作為未被配置的記憶體合併時使用。而空閒 tlsf_block 的 size 指的是 next_free + prev_free + 空的位置 + prev(實體上下一個 block 的 prev)。 再回來看上面的常數設定。 ```c /*一個 ptr 的大小。*/ #define BLOCK_OVERHEAD (sizeof(size_t)) /*一個 block 必須足夠大以存儲其標頭減去 prev 的大小(儲存在上一個 block)。*/ #define BLOCK_SIZE_MIN (sizeof(tlsf_block_t) - sizeof(tlsf_block_t *)) /*不能大於 FL_INDEX 的可尋找的最大位數*/ #define BLOCK_SIZE_MAX ((size_t) 1 << (FL_MAX - 1)) /*見上段*/ #define BLOCK_SIZE_SMALL ((size_t) 1 << FL_SHIFT) ``` 最後如同[測驗三](https://hackmd.io/@IFhfI6MRSZSja7n1T2-DNg/quiz3),因為現在電腦多數都是 32 bits (4 bytes) 以上,所以其位置二進位最後二位必為 0,不會使用到,所以 `block->header` 的第一個 bit 用來儲存目前的 block 是否為 free,第二個 bit 用來儲存前一個的 block 是否為 free。 ```c #define BLOCK_BIT_FREE ((size_t) 1) #define BLOCK_BIT_PREV_FREE ((size_t) 2) #define BLOCK_BITS (BLOCK_BIT_FREE | BLOCK_BIT_PREV_FREE) ``` `block_size`, `block_set_size`, `block_set_prev_free`, `block_set_free` 等函式都是對 header 作對應的 bitmask 操作,得到或更改想要的 bit ,實作很簡單,不一一解釋。 對幾個比較難懂的 block 操作做解釋: #### payload 以及 block 位址互找 很直觀,見下圖,取自 [esp-idf 的記憶體管理: TLSF 演算法](https://blog.csdn.net/gzxb1995/article/details/124504705)。 <img src="https://hackmd.io/_uploads/SJCkqoSL2.png" width="50%" height="50%"> block + prev 的大小 + size 的大小 = payload 的位址,反之亦然。 offsetof 會獲取 `tlsf_block_t` 圖中 size (header) 成員相對於結構體起始地址的偏移量,即為 prev 的大小。 BLOCK_OVERHEAD 即為圖中 size (header) 的大小。 ```c INLINE char *block_payload(tlsf_block_t *block) { return (char *) block + offsetof(tlsf_block_t, header) + BLOCK_OVERHEAD; } ... INLINE tlsf_block_t *block_from_payload(void *ptr) { return to_block((char *) ptr - offsetof(tlsf_block_t, header) - BLOCK_OVERHEAD); } ``` #### prev / next block 找 block **實體上**前一個或後一個的 block。 > Note: 一直強調實體上,是跟 free list 內的前後 block 做對比。 block_prev 因為本來就有存 prev ,直接呼叫即可。 ```c /* Return location of previous block. */ INLINE tlsf_block_t *block_prev(const tlsf_block_t *block) { ASSERT(block_is_prev_free(block), "previous block must be free"); return block->prev; } ``` 示意圖如下: ``` +-----------+ <- block | prev | +-----------+ | header | +-----------+ <- block_payload(block) | next_free | +-----------+ | prev_free | +-----------+ | ... | +-----------+ <- next (want we want) | prev | } BLOCK_OVERHEAD +-----------+ <- block_payload(block) + block_size(block) | header | +-----------+ | next_free | +-----------+ | prev_free | +-----------+ | ... | +-----------+ ``` 要注意 `BLOCK_OVERHEAD` 表達的是一個 ptr 的大小,所以在上面對齊時當作 `header` 的大小,這邊則當作為 `prev` 的大小,可以將其替換成 `siezof(tlsf_block *)`。 ```c /* Return location of next existing block. */ INLINE tlsf_block_t *block_next(tlsf_block_t *block) { tlsf_block_t *next = to_block(block_payload(block) + block_size(block) - BLOCK_OVERHEAD); ASSERT(block_size(block), "block is last"); return next; } ``` ### tlsf_t 資料結構 接下來,介紹另一個重要的資料結構,名為 tlsf_t ,其意義為整個 tlsf memory pool 的 controller,以下用程式碼以及圖例會更好的理解。 ```c typedef struct { uint32_t fl, sl[_TLSF_FL_COUNT]; struct tlsf_block *block[_TLSF_FL_COUNT][_TLSF_SL_COUNT]; size_t size; } tlsf_t; ``` #### fl, sl[_TLSF_FL_COUNT] fl 以及 sl 的 bitmap 用來記錄其對應的 sl 或 fl 是否存在 free block。 sl 中 1 表有 free block 0 表沒 free block,而在 fl 中 1 表所有此 fl 層中的 sl free block 0 表所有此 fl 層中的 sl 沒 free block 。 ``` 舉例: fl 為 0........01:表示只有 fl 為 0 中存在有 sl 存在 free block。 sl[0] 為 0000000000000001 : 表示 fl 為 0 中 sl 為 0 對應的 free block。 fl 為 0......0101:表示只有 fl 為 0 或 2 中存在有 sl 存在 free block。 sl[0] 為 0000000000000011 : 表示 fl 為 0 中 sl 為 0 或 1 存在 free block。 ``` #### block[_TLSF_FL_COUNT][_TLSF_SL_COUNT] 用來儲存每一個 fl sl 的 free list head。 ``` 舉例: block[0][0] 為某個 ptr,則代表 fl 0 sl 0 的 free list 的第一個 block 位址為 ptr。 block[1][1] 為 NULL,則代表 fl 0 sl 0 的 free list 的為 empty。 ``` 用下圖來理解 tlsf_t 在 mem 中的儲存位置,詳細圖見上段 TLSF 架構: ``` +------------+ \ | | | tlsf_t | ------------------> 函 bitmap blocks 等。 | | +------------+ / | block | | | +------------+ | free | +------------+ | block | +------------+ ``` ### mapping 如同上段 TLSF 架構所解釋,mapping 技術就是把 size 轉換為對應的 fl 以及 sl 值從而找到儲存在 `blocks[fl][sl]` 中最合適的 free list。 公式取自〈[TLSF: a New Dynamic Memory Allocator for Real-Time Systems](http://www.gii.upv.es/tlsf/files/ecrts04_tlsf.pdf)〉如下: ![](https://hackmd.io/_uploads/HJXxkB98h.png) fl 很好理解,sl 的概念為 $size - 2^f$ 表示 size > 此層 fl 多少空間,$size - 2^f$ 除此 fl 層的 sl 大小為何即可得到對應的 sl。 $2^{SLI}$ 就是 `SL_COUNT`,$2^{f+1}-2^f/2^{SLI}$ 意思就是 fl 層的 sl 大小,$2^{f+1}-2^f/2^{SLI}$ 可化簡為 $\dfrac{2^f}{2^{SLI}}$,$size - 2^f$ 除 $2^f/2^{SLI}$ 則可表示為 $(size - 2^f) \times (\dfrac{2^{SLI}}{2^f})$。 下面程式碼只是將這個數學是換成用 bitwise 操作,以增加性能,解釋如下: ```c INLINE void mapping(size_t size, uint32_t *fl, uint32_t *sl) { /*原因如同 block 資料結構中的解釋,且不會滿足上述 fl 的公式*/ if (size < BLOCK_SIZE_SMALL) { /*太小的 block 會全部放入 fl 0 */ *fl = 0; /*跟上述公式一模一樣,只是第一個 2^f 因為是起點所以為 0 */ /*第二個 2^f 的數學意義是此層 fl 的大小所以為 BLOCK_SIZE_SMALL*/ *sl = (uint32_t) size / (BLOCK_SIZE_SMALL / SL_COUNT); } else { uint32_t t = log2floor(size); /*與公式相同*/ /*XOR 前可轉換成 size * 2^t / SL_COUNT */ /*XOR 代表減法,SL_COUNT 是把公式括號內的 2^f 提出運算之結果*/ *sl = (uint32_t) (size >> (t - SL_SHIFT)) ^ SL_COUNT; /*原因如同上段 重要參數 解釋 FL_COUNT 一樣*/ *fl = t - FL_SHIFT + 1; } ASSERT(*fl < FL_COUNT, "wrong first level"); ASSERT(*sl < SL_COUNT, "wrong second level"); } ``` ### insert remove free block 操作 #### block_remove block_insert 我們真實在 remove 以及 insert 時,只會知道要處理的 block 的位址,所以要先通過 mapping 尋找此大小的 block 對應的 fl sl,才會知道它存在哪個 free list (blocks[fl][sl])。在進行對於 list 的 node 操作。 ```c /* Remove a given block from the free list. */ INLINE void block_remove(tlsf_t *t, tlsf_block_t *block) { uint32_t fl, sl; mapping(block_size(block), &fl, &sl); remove_free_block(t, block, fl, sl); } /* Insert a given block into the free list. */ INLINE void block_insert(tlsf_t *t, tlsf_block_t *block) { uint32_t fl, sl; mapping(block_size(block), &fl, &sl); insert_free_block(t, block, fl, sl); } ``` #### insert remove block in free list 對於已知 free list (blocks[fl][sl]),這兩個函式是把 block 給在對應的 free list (blocks[fl][sl]) 插入或是刪除,插入是直接插在 list 的最前端,作法就是基本的 linked list 操作,比較特別的是最後會去檢查 bitmap 需不需要更新。 ```c /* Remove a free block from the free list. */ INLINE void remove_free_block(tlsf_t *t, tlsf_block_t *block, uint32_t fl, uint32_t sl) { ASSERT(fl < FL_COUNT, "wrong first level"); ASSERT(sl < SL_COUNT, "wrong second level"); tlsf_block_t *prev = block->prev_free; tlsf_block_t *next = block->next_free; if (next) next->prev_free = prev; if (prev) prev->next_free = next; /* If this block is the head of the free list, set new head. */ if (t->block[fl][sl] == block) { t->block[fl][sl] = next; /* If the new head is null, clear the bitmap. */ if (!next) { t->sl[fl] &= ~(1U << sl); /* If the second bitmap is now empty, clear the fl bitmap. */ if (!t->sl[fl]) t->fl &= ~(1U << fl); } } } /* Insert a free block into the free block list and mark the bitmaps. */ INLINE void insert_free_block(tlsf_t *t, tlsf_block_t *block, uint32_t fl, uint32_t sl) { tlsf_block_t *current = t->block[fl][sl]; ASSERT(block, "cannot insert a null entry into the free list"); block->next_free = current; block->prev_free = 0; if (current) current->prev_free = block; t->block[fl][sl] = block; t->fl |= 1U << fl; t->sl[fl] |= 1U << sl; } ``` ### block 切割實作 這邊講解了 block 的各種切割實作,為甚麼需要切割在後續講解 `alloc` `free` 時會提到,或是直接參考[測驗五第一題](https://hackmd.io/@sysprog/linux2023-quiz5)。 #### block_can_split 檢查的 block 是否能分割 size 出來, 當然 block size 要大於等於 size ,加上 `sizeof(tlsf_block) `要確保底部實體上下一個 block 的 prev 有地方儲存。 見下圖,取自 [esp-idf 的記憶體管理: TLSF 演算法](https://blog.csdn.net/gzxb1995/article/details/124504705)。 <img src="https://hackmd.io/_uploads/ByoT05SI3.png" width="50%" height="50%"> ```c INLINE bool block_can_split(tlsf_block_t *block, size_t size) { return block_size(block) >= sizeof(tlsf_block_t) + size; } ``` #### block_rtrim_free block_rtrim_used block_ltrim_free 有三種不同的切割方式,取決於切割前 block 的狀態以及切割後想要 block 的狀態。 ```c /*把 free block 的頭部分割出指定的大小*/ /*尾部會剩下一個 free block*/ /*將分割完頭部的 free block 插入對應大小的 free list(blocks[fl][sl])*/ INLINE void block_rtrim_free(tlsf_t *t, tlsf_block_t *block, size_t size) { ASSERT(block_is_free(block), "block must be free"); /*先確定可否 split*/ if (!block_can_split(block, size)) return; /*將一個 block 分割成兩塊,函式實作見後面*/ /*rest 代表尾部的 block*/ /*block 代表頭部的 block*/ tlsf_block_t *rest = block_split(block, size); /*頭部的 block 因為後面的 block 變為新的 block*/ /*需要重新 link*/ block_link_next(block); /*block 是 free 所以 rest 的 prev free 要設定為 true*/ block_set_prev_free(rest, true); /*將分割完頭部的 free block 插入對應大小的 free list(blocks[fl][sl])*/ block_insert(t, rest); } /*把 use block 的頭部分割出指定的大小*/ /* 尾部會剩下一個 free block*/ /*將分割完尾部的 free block 插入對應大小的 free list(blocks[fl][sl])*/ INLINE void block_rtrim_used(tlsf_t *t, tlsf_block_t *block, size_t size) { ASSERT(!block_is_free(block), "block must be used"); /*先確定可否 split*/ if (!block_can_split(block, size)) return; /*將一個 block 分割成兩塊,函式實作見後面*/ /*rest 代表尾部的 block*/ /*block 代表頭部的 block*/ tlsf_block_t *rest = block_split(block, size); /*物理上前一個是 free block 才會在 merge 使用到 prev */ /*所以不需要 block_link_next(block) 來更新 next->prev*/ /*block 是 use 所以 rest 的 prev free 要設定為 false*/ block_set_prev_free(rest, false); /*free block 與後面的 free block 合併(若存在)*/ /*在block_rtrim_free沒有這段,因為 free block 後不可能是 free block*/ /*在介紹到 free 實作時會說明這點*/ rest = block_merge_next(t, rest); /*將分割完尾部的 free block 插入對應大小的 free list(blocks[fl][sl])*/ block_insert(t, rest); } /*把 free block 的頭部分割出指定的大小*/ /* 尾部會剩下一個 free block*/ /*將分割完頭部的 free block 插入對應大小的 free list(blocks[fl][sl])*/ /*return 尾部的 free block*/ /*使用於 aalloc*/ INLINE tlsf_block_t *block_ltrim_free(tlsf_t *t, tlsf_block_t *block, size_t size) { ASSERT(block_is_free(block), "block must be free"); ASSERT(block_can_split(block, size), "block is too small"); /*將一個 block 分割成兩塊,函式實作見後面*/ /*rest 代表尾部的 block*/ /*block 代表頭部的 block*/ /*BLOCK_OVERHEAD 是考量到 header*/ tlsf_block_t *rest = block_split(block, size - BLOCK_OVERHEAD); /*block 是 free 所以 rest 的 prev free 要設定為 true*/ block_set_prev_free(rest, true); /*頭部的 block 因為後面的 block 變為新的 block*/ /*需要重新 link*/ block_link_next(block); /*將分割完頭部的 free block 插入對應大小的 free list(blocks[fl][sl])*/ block_insert(t, block); /*return 尾部的 free block*/ return rest; } ``` #### block_merge_prev block_merge_next 合併方式有兩種,注意這邊的合併是查找**物理上**的 block 來做合併,因為實際使用式合併 free block 所以一定要是 free block 才能實行此函式。 ```c /* 把前此 block 跟物理上前面的 block 合併,注意結果不會加到 free list +------------+-------+ | free block | block | +------------+-------+ */ INLINE tlsf_block_t *block_merge_prev(tlsf_t *t, tlsf_block_t *block) { /*因為是用在 free block 合併,前一個一定要是 free block*/ if (block_is_prev_free(block)) { tlsf_block_t *prev = block_prev(block); ASSERT(prev, "prev block can't be null"); ASSERT(block_is_free(prev), "prev block is not free though marked as such"); /*移除 prev 於 free list 中*/ block_remove(t, prev); /*將前後合併*/ block = block_absorb(prev, block); } return block; } /* 把前此 block 跟物理上後面的 block 合併,注意結果不會加到 free list +-------+------------+ | block | free block | +-------+------------+ */ INLINE tlsf_block_t *block_merge_next(tlsf_t *t, tlsf_block_t *block) { tlsf_block_t *next = block_next(block); ASSERT(next, "next block can't be null"); if (block_is_free(next)) { ASSERT(block_size(block), "previous block can't be last"); /*移除 next 於 free list 中*/ block_remove(t, next); /*將前後合併*/ block = block_absorb(block, next); } return block; } ``` #### block_split block_absorb 用來實現切割與合併。 ```c /*將給定 block 切 size 出來,並把剩下的空間定義一個新 block*/ /*並回傳 rest,注意這邊並未把 rest 放入 free list +-----------+ <- block +-----------+ <- block | prev | | prev | +-----------+ +-----------+ | header | | header | +-----------+ +-----------+ \ | next_free | | next_free | +-----------+ +-----------+ | prev_free | | prev_free | +-----------+ +-----------+ size | ... | | ... | | | before-------->after +-----------+ <- rest | | | prev | | | +-----------+ / | | | header | | | +-----------+ | | | next_free | | | +-----------+ | | | prev_free | | | +-----------+ | | | ... | +-----------+ +-----------+ */ INLINE tlsf_block_t *block_split(tlsf_block_t *block, size_t size) { /*扣掉的 BLOCK_OVERHEAD 是因為 rest 要指在 prev*/ tlsf_block_t *rest = to_block(block_payload(block) + size - BLOCK_OVERHEAD); /*扣掉的 BLOCK_OVERHEAD 是因為要分 header 出來,header 不算在 size*/ size_t rest_size = block_size(block) - (size + BLOCK_OVERHEAD); ASSERT(block_size(block) == rest_size + size + BLOCK_OVERHEAD, "rest block size is wrong"); ASSERT(rest_size >= BLOCK_SIZE_MIN, "block split with invalid size"); /*新 block size 設定*/ rest->header = rest_size; ASSERT(!(rest_size % ALIGN_SIZE), "invalid block size"); /*更新切下來以後的空間以及原本的 block*/ block_set_free(rest, true); block_set_size(block, size); return rest; } /* 合併前後 block,前 prev 後 block,示意圖為 split 的反向 +-----------+ <- prev +-----------+ <- prev | prev | | prev | +-----------+ +-----------+ | header | | header | +-----------+ +-----------+ | next_free | | next_free | +-----------+ +-----------+ | prev_free | | prev_free | +-----------+ +-----------+ | ... | | ... | | | after<--------before +-----------+ <- block | | | prev | | | +-----------+ | | | header | | | +-----------+ | | | next_free | | | +-----------+ | | | prev_free | | | +-----------+ | | | ... | +-----------+ +-----------+ */ INLINE tlsf_block_t *block_absorb(tlsf_block_t *prev, tlsf_block_t *block) { ASSERT(block_size(prev), "previous block can't be last"); /*加上的 BLOCK_OVERHEAD 是因為要加 header 回來,header 不算原本的在 size*/ prev->header += block_size(block) + BLOCK_OVERHEAD; block_link_next(prev); return prev; } ``` ### tlsf init 在 [jserv/tlsf](https://github.com/jserv/tlsf-bsd) 中並未找到關於 tlsf pool 的初始化,但初始化的狀態可以大大幫助理解程式碼,這邊先使用 [esp-idf 的記憶體管理: TLSF 演算法](https://blog.csdn.net/gzxb1995/article/details/124504705) 這篇文章的概念,見下圖。 這是一個 TLSF 初始化完的狀態,有幾個重點: 1. tlsf_t 會分配在記憶體最上方的位置。 2. 一開始會將整個 memory pool 初始化為一個 free block,換句話說指會有一個 blocks[fl][sl] 有指向 free list (只有一個 free list)。 所以不難想像,一開始無論 alloc 的 size `mapping` 後的 fl sl 為何,在 `block_find_suitable` 後都會找到那個唯一有 free block 的 fl sl index。 ![](https://hackmd.io/_uploads/SkT54msI2.png) [jserv/tlsf](https://github.com/jserv/tlsf-bsd) 並未有 init 函式 而是使用 `arena_grow` `arena_shrink` 代替。 ```c static bool arena_grow(tlsf_t *t, size_t size) { //找到新的 size size_t req_size = (t->size ? t->size + BLOCK_OVERHEAD : 2 * BLOCK_OVERHEAD) + size; //可不可以給我這個 size void *addr = tlsf_resize(t, req_size); if (!addr) return false; ASSERT((size_t) addr % ALIGN_SIZE == 0, "wrong heap alignment address"); //找到增加空間後最新 block 的位置 tlsf_block_t *block = to_block(t->size ? (char *) addr + t->size - 2 * BLOCK_OVERHEAD : (char *) addr - BLOCK_OVERHEAD); if (!t->size) block->header = 0; check_sentinel(block); //設定新得到的 block block->header |= size | BLOCK_BIT_FREE; block = block_merge_prev(t, block); block_insert(t, block); //設定新的 sentinel tlsf_block_t *sentinel = block_link_next(block); sentinel->header = BLOCK_BIT_PREV_FREE; t->size = req_size; check_sentinel(sentinel); return true; } static void arena_shrink(tlsf_t *t, tlsf_block_t *block) { //新的 free block 是最尾(下一個是 sentinel)就把空間還回去 check_sentinel(block_next(block)); size_t size = block_size(block); ASSERT(t->size + BLOCK_OVERHEAD >= size, "invalid heap size before shrink"); //刪掉 free block 的 size t->size = t->size - size - BLOCK_OVERHEAD; //只剩 sentinel if (t->size == BLOCK_OVERHEAD) t->size = 0; tlsf_resize(t, t->size); //還有其他 block,設定新 sentinel if (t->size) { block->header = 0; check_sentinel(block); } } ``` ### malloc 討論完 init 後,我們來看如何 malloc,具體流程如下: 1. 先把 size 對齊,如果每個申請的 size 都申請 `ALIGN_SIZE` 的倍數,自然他們的 ptr 就會是對齊好的。 2. 找到此 size 最合適的 free list。 3. 從此 free list 中取出一個 free block 作為 alloc 的對象。 4. 將此 free block 從 list 中移除。 5. 將此 free block 切出 size 作為 use block。 6. 將切剩的 free block 放到對應合適大小的 free list。 `adjust_size`:1 `block_find_free`:2~4 `block_use`:5~6 ```c void *tlsf_malloc(tlsf_t *t, size_t size) { size = adjust_size(size, ALIGN_SIZE); if (UNLIKELY(size > TLSF_MAX_SIZE)) return NULL; tlsf_block_t *block = block_find_free(t, size); if (UNLIKELY(!block)) return NULL; return block_use(t, block, size); } ``` 接下來就可以看這些函式具體的實作: #### adjust_size 呼叫 `align` 等對其函式,若對齊後比最小可以存在的 BLOCK 還小,就回傳`BLOCK_SIZE_MIN`。 ```c /* Adjust allocation size to be aligned, and no smaller than internal minimum. */ INLINE size_t adjust_size(size_t size, size_t align) { size = align_up(size, align); return size < BLOCK_SIZE_MIN ? BLOCK_SIZE_MIN : size; } ``` ##### align 對齊的詳細概念以及意義見[你所不知道的 C 語言:記憶體管理、對齊及硬體特性篇](https://hackmd.io/@sysprog/c-memory)。 `align_ptr` 會呼叫 `align_up` 得到向上對齊後的位址 (例如 15 對齊 8 得到 16),即找向上最接近 align 的倍數。因為 size_t align 會是二的次方,`align - 1` 後會是 0+1+ 的形式,做 | 的意義在於將 x 對 align 取餘數後為 align - 1,最後的 `+1` 自然就會得到向上最接近 align 的倍數,一開始做 `x-1` 是確保邊界狀況 (例如 16 對齊 8,所以先把 16 改成 15) 也會成立。 ```c INLINE size_t align_up(size_t x, size_t align) { ASSERT(!(align & (align - 1)), "must align to a power of two"); return (((x - 1) | (align - 1)) + 1); } INLINE char *align_ptr(char *p, size_t align) { return (char *) align_up((size_t) p, align); } ``` #### `block_find_free` 總體用途上面已經解釋過了。 這邊需要特別提的是 `提級申請` 概念,就是會用高一層的 size,去做 mapping。舉例:原本 size 是落在 `blocks[1][1]`,但我們 mapping 完想要的 fl sl 反而是 `blocks[1][2]`。 原因很好理解,假如我們使用原本的 size 介於: $[ 2^f, 2^f + N)$ `mapping` 後得到的 fl sl 的 free block 空間大小也會介於: $[ 2^f, 2^f + N)$ 所以找到的 free block 不一定比 size 大,遍例整個 free list 會浪費時間成本,所以我們做提級,確保找到的 free block 一定大於 size。 我們希望 `mapping` 後得到的 fl sl 的 free block 空間大小會介於: $[ 2^f + N, 2^f + 2N)$ `round_block_size` 就是先把原本的 size 提到下一層的大小,讓我們最後得到的 fl sl 會提級。 ```c INLINE tlsf_block_t *block_find_free(tlsf_t *t, size_t size) { size_t rounded = round_block_size(size); uint32_t fl, sl; /*找到此 size 最合適的 free list*/ mapping(rounded, &fl, &sl); tlsf_block_t *block = block_find_suitable(t, &fl, &sl); if (UNLIKELY(!block)) { if (!arena_grow(t, rounded)) return NULL; block = block_find_suitable(t, &fl, &sl); ASSERT(block, "no block found"); } ASSERT(block_size(block) >= size, "insufficient block size"); /*從此 free list 中取出一個 free block 作為 alloc 的對象*/ /*將此 free block 從 list 中移除*/ remove_free_block(t, block, fl, sl); return block; } ``` ##### round_block_size $((size_t) 1 << (log2floor(size) - SL_SHIFT))$ 等價於,$2^f/SL\_COUNT$ 解釋 ,`mapping` 時講過了,就是這個 size 所屬 SL 層的大小。 $size + t$ 原本的 `size` 加上所屬 SL 層的大小,一定會變到下一個 SL 層區間。 `& ~t` 代表加上後多出來的餘數就算了。 ```c /* Round up to the next block size */ INLINE size_t round_block_size(size_t size) { size_t t = ((size_t) 1 << (log2floor(size) - SL_SHIFT)) - 1; return size >= BLOCK_SIZE_SMALL ? (size + t) & ~t : size; } ``` ##### block_find_suitable 就算找到了儲存了最合適大小的 free block 的 free list 所對應的 fl sl,此 fl sl (blocks[fl][sl]) 也不一定有 node 在 free list 內,意即沒有此大小區間的 free block 存在 memeory 。這時就必須往找尋更大的 free block (更大的 fl sl),來做為未來 alloc 的位址。 這邊也是使用 bitwise 操作,使得效率加快,都蠻容易理解的。 在查找時直接 bitmap ,不用使用遍例找到最合適的 fl sl ,所以搜尋時間為 O(1)。 ```c INLINE tlsf_block_t *block_find_suitable(tlsf_t *t, uint32_t *fl, uint32_t *sl) { ASSERT(*fl < FL_COUNT, "wrong first level"); ASSERT(*sl < SL_COUNT, "wrong second level"); /* 從 sl 的 bitmap 中將此 sl 對應的 bit 及更高位的 bit 都取出來 */ uint32_t sl_map = t->sl[*fl] & (~0U << *sl); /*如果結果為 0,代表此 fl 沒有相應大小的 free block 了*/ if (!sl_map) { /*去更大的 fl 尋找*/ /*取出 fl bitmap 中更高位的 bit*/ uint32_t fl_map = t->fl & (uint32_t) (~(uint64_t) 0 << (*fl + 1)); /*如果結果為 0,代表整個 memory pool 都沒有相應大小的 free block 了*/ if (UNLIKELY(!fl_map)) return NULL; /*若不為 0,就從剛剛取出的更高位的 bit 中找最小那位*/ *fl = bitmap_ffs(fl_map); ASSERT(*fl < FL_COUNT, "wrong first level"); /*取出此區間的 sl bitmap*/ sl_map = t->sl[*fl]; ASSERT(sl_map, "second level bitmap is null"); } /*從 sl_map 中取出最低位的 bit 作為我們最後的 sl 層*/ /*因為確認過 fl bitmap 以及 sl bitmap,現在的 sl_map 一定不為 0*/ *sl = bitmap_ffs(sl_map); ASSERT(*sl < SL_COUNT, "wrong second level"); return t->block[*fl][*sl]; } ``` #### block_use 作用如上面解釋,各個函式實作上面都介紹過了。 ```c INLINE void *block_use(tlsf_t *t, tlsf_block_t *block, size_t size) { /*將此 free block 切出 size*/ /*將切剩的 free block 放到對應合適大小的 free list*/ block_rtrim_free(t, block, size); /*切出來的作為 use block*/ block_set_free(block, false); /*malloc 最後回傳給 usr 是回傳 payload 的 ptr*/ return block_payload(block); } ``` ### free 在進到 free 前先講解 free 的基本運行流程,概念來自對[測驗五第一題](https://hackmd.io/@sysprog/linux2023-quiz5)的理解。 在記憶體管理中,當一個 block 要釋放時,我們會去檢查其物理上前後是否為 free block ,若為 free block 我們會將釋放以後的 block 跟前後的 free block 作合併,得到一個更大的 free block ,這樣我們未來在做 alloc 時就有一個更大的連續空間可以使用。 `merge` 的流程如下: ``` +------------+-------+------------+ | free block | block | free block | +------------+-------+------------+ 在 tlsf 的實作中,會先去跟前面的 free block 做 merge +--------------------+------------+ | free block | free block | +--------------------+------------+ 然後再跟後面的 free block 做 merge 最後得到最大的 free block +---------------------------------+ | free block | +---------------------------------+ 如果要釋放的 block 前或後不為 free block 怎麼辦? 這不用擔心,因為 merge_pre 跟 merge_next 在執行前都會先去檢查 pre 或 next 是不是 free block ,若不是 merge 將甚麼都不會做,最後還是會得到最大的 free block ``` 我們每次 free 後,都會去做合併,所以在 memory pool 中,是不會存在兩個 free block 連在一起的狀況。 這邊要特別注意的是在 tlsf 在合併後,因為 free block 的大小改變了,所以要重新做 mapping 得到對應的 fl sl ,並將新的 free block 放入對應的 free list (blocks[fl][sl])。 ```c void tlsf_free(tlsf_t *t, void *mem) { if (UNLIKELY(!mem)) return; /*usr 只會知道 payload 的位址*/ /*所以傳進來後,要先推回 block 的位址*/ tlsf_block_t *block = block_from_payload(mem); ASSERT(!block_is_free(block), "block already marked as free"); /*把此 block 改為 free*/ block_set_free(block, true); /*先跟前面做合併,若前面不為 free block 這個函式甚麼都不會做*/ block = block_merge_prev(t, block); /*再跟後面做合併,若後面不為 free block 這個函式甚麼都不會做*/ block = block_merge_next(t, block); if (UNLIKELY(!block_size(block_next(block)))) arena_shrink(t, block); else /*最後合併完成的新 free block 要放到對應的 free list (blocks[fl][sl])*/ block_insert(t, block); } ``` 最後,有一些比較延伸的函式還沒寫,未來有機會補上: `tlsf_aalloc`、`tlsf_realloc`、`arena_shrink`、`arena_grow` 、`check_sentinel`、`block_rtrim_used`