執行人: a1091150
:question: 提問清單
重做第 6 週測驗題的測驗四,搭配第 5 週教材提及的 tlsf-bsd,探討何以 時間複雜度可落實於記憶體配置器
至少要涵蓋:
在即時系統例如 RTOS (Real-Time Operating Systems) 下,每項操作或同種操作都需要在一定時間內回應。動態記憶體配置 (DSA; Dynamic storage allocation) 相關方法,在多數情況下有良好的回應時間,但是在 worst case 上可能超出時間。因此,在記憶體配置與釋放的操作上,TLSF 限定在 時間複雜度。
在論文中假設的應用場景:一定時間內回應、低程度的記憶體碎片、在有限或固定的記憶體配置上操作
在論文中主要介紹:
mapping
機制將要求的記憶體大小轉換到指定的層
TLSF 使用兩層位元,紀錄擁有的 free block
當程式要求記憶體 size
時,透過 size
計算分別對應第一層的位元與第二層位元,即可查詢是否有 free block。若在該位元沒有 free block,也可透過位元操作查詢鄰近的 free block。整個操作皆在 時間複雜度內。
在論文中,使用mapping
機制轉換 size
成為指定第一層與第二層位元,以 size = 460
為例:
參考 Part 1: Background 文中的mapping_insert
程式碼,該程式碼引用自 GitHub - mattconte/tlsf
fl
數值上,取 fls 數值sl
數值上,與前述取得第一層位元的後 4 個位元相同,透過 ^ 0x20
移除第一層的位元。這裡有個特別的 fl -= 7
的程式碼,根據 Part 2: The floating point 文中敘述,可以將此 mapping_insert
當作把整數轉換程浮點數中 exponent 減去 bias 的過程,唯此轉換前後一定是正數。
tlsf-bsd 依照 TLSF 論文實作 mapping
機制,這裡來查看記憶體是如何被操作。
struct tlsf_block
結構體查看結構體 struct tlsf_block
size_t header;
:
BLOCK_BIT_FREE
設定BLOCK_BIT_PREV_FREE
設定struct tlsf_block *next_free, *prev_free;
:透過串列串接擁有相近大小的 free blockstruct tlsf_block *prev;
:除了串列串接相近大小的 free block 外,每個 free block 的最後一個元素被指派此 free block 的起始位址,讓下個 free block 可以透過此元素直接取得 free block 以合併。block2->prev
指向 block
的最後一個元素,該元素有 block1
的記憶體位址。若把 block1 size
當作 block1
的一部分,可以認為這兩個 block 重疊。當 free block 轉為 in-use block 時,struct block_t
經由 *block_payload()
計算 payload,回傳指定位址,以圖片區分 in-use block 與 free block 使用差異:
tlsf_malloc
了解 struct tlsf_block
結構體位址計算參考 Makefile 檔案,分別在 test.c
與 bench.c
以 tlsf_resize()
函式,向系統取得固定的記憶體空間。這裡解說 gcc test.c tlsf.c
編譯後的程式。
透過修改 test.c
的 main()
函式,觀察其行為
要求記憶體時 void *p1 = tlsf_malloc(&t, (1 << 10) + 1);
,會經由
tlsf_malloc(size)
:取得記憶體位址,將 size
透過 adjust_size(size)
進位到鄰近的 ALIGN_SIZE = 1 << 3
的倍數block_find_free()
: 取得 free block 記憶體位址,但沒有找到 free blockarena_grow()
:申請的記憶體空間,並作為 free block 加入到 tlsf level 中tlsf_resize()
:透過 mmap
申請固定大小記憶體空間block_find_free()
取得 free block觀察 arena_grow
中 block
的記憶體位址計算,來了解 struct tlsf_block
的每個成員作用:
第一次要求記憶體配置 tlsf_malloc(&t, (1 << 10) + 1)
:
req_size = 2 * BLOCK_OVERHEAD + size
block->header |= size | BLOCK_BIT_FREE;
,並加入到 tlsf level 中block_link_next()
連結下一個的 block,相當於指派 sentinel->prev = block
sentinel
表示記憶體中最後一個 block在 to_block()
轉換成 free block 的位址計算,最後以圖表列出 block
與 sentinel
分別指向的位址
block
指向不可寫入的記憶位址,但是可以指派 block->header
; sentinel
記錄最高位址,以重疊的區塊 sentinel->prev
紀錄 block 位址。
最終透過 block_find_free()
與 block_use()
回傳 payload 記憶體位址。
重做第 6 週測驗題的測驗四,搭配上述來解釋何以 時間複雜度可落實於記憶體配置器,又如何改進實作。
此 heap allocator 與 tlsf-bsd 實作不同之處在於
headsbits
陣列,陣列中的每個位元位置對應一種 base size。在此程式碼實作與命名中,每 15 個位元視為同個 level。tlsf_block
儲存該記憶體空間的 metadata,並以記憶體位址做減法以回推 tlsf_block
posix_memalign
使得記憶體對齊,將記憶體位址計算求出對應的 headsbits
與 bitfield
bitfield
用於紀錄鄰近記憶體空間的狀態,可視為二維陣列,若取其中的一維陣列的元素,可表示同個 level 下,該鄰近空間的使用情況。透過修改程式碼,並搭配 GDB 檢查變數數值、部分函式寫成 Python 程式碼,藉此觀察 struct heap
每個成員的作用。
uint32_t headsbits[HEADS_BITS_SIZE];
:某個位元對應的 base size 是否有 free blockuint32_t *bitfield[MAIN_BASE_SIZE_COUNT]
:用於記錄該多個記憶體區塊的使用情況uint8_t *hdata;
:傳入記憶體起始位址uint32_t hdcnt;
:bitfield
陣列元素數量chunk_t *heads[0];
:用於串接 free block。 free block 的記憶體空間會被用於串接每個 chunk_t
資料main()
函式將 SIZE
修改成較小的 SIZE
,藉由引入與修改巨集數值 #define BC 5
來測試 headsbits
陣列數值。
每個位元代表不同的 base size
void *data = memalign(SIZE, SIZE);
使用 posix_memalign()
,取得的記憶體位址與 SIZE
對齊。 可參考 man posix_memalign
說明
int posix_memalign(void **memptr, size_t alignment, size_t size);
The address of the allocated memory will be a multiple of alignment, which must be a power of two and a multiple of sizeof(void *).
因此 SIZE
具有以下特性:
void *
對齊headsbits[HEADS_BITS_SIZE]
成員H1->headbits
是用來表示此 heap 以被設定的位元,此位元對應一種 base size,來表示擁有的 free block
初始化 headsbits
使用到 closest_base_size(SIZE)
, base_size_to_index(used_size)
與 base_size_from_index(index)
函式
closest_base_size(SIZE)
函式將 SIZE
轉換成大於等於 SIZE
的 base size,由於 SIZE
對齊的特性,轉換前後數值相同base_size_to_index(SIZE)
函式將 SIZE
轉換為 base size 的索引值base_size_from_index(index)
函式為 base_size_to_index(SIZE)
的反函式觀察函式 base_size_from_index
的輸出:
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
對應 base size | 16 | 32 | 48 | 64 | 80 | 96 | 112 | 128 | 144 | 160 | 176 | 192 | 208 | 224 | 240 | 256 | 512 | 768 | 1024 | 1280 | 1536 | 1792 | 2048 | 2304 | 2560 | 2816 | 3072 | 3328 | 3584 | 3840 | 4096 | 8192 |
從以上可以推得一個規律,每 15 位元視為同個 level,一共 7 個 level:
在 heap_create()
函式中,透過 populate_heads()
函式設定指定的位元。
h->headsbits[i >> 5] |= 0x80000000U >> (i & 31);
將設定指定的位元,來表示 heap 中擁有的 free block。
bitfield[MAIN_BASE_SIZE_COUNT]
成員用來記錄每個 free block 使用的情形,利用 chunk_status_t
,以兩個位元表示對應的記憶體位置的區塊使用情況
其中可以利用或修改以下函式,來查看對應的 bitfield
數值,或使用經由 heap_alloc
取得的記憶體位址,檢查該對應的 chunk_status_t
數值
觀察 heap_create()
函式中藉由 total_bitfield_count
取得所有 bitfield
需要的記憶體空間,利用 needed_bitfield_count
計算分配給每個區域需要多少空間,set_bf_ptr()
初始化每個 bitfield
的數值。
仿照 needed_bitfield_count()
函式撰寫成 Python 程式碼觀察其數量
輸入參數SIZE = 1 << i
來表示原本 SIZE = 1 << 4 << BC
的輸入,輸出指標陣列中 bitfield[MAIN_BASE_SIZE_COUNT]
每個陣列的元素數量。
**bitfield
型態為 uint32_t
,以兩個位元表示 chunk_status_t
狀態,因此一個 **bitfield
元素可以表示 16 個記憶體區塊的狀態。
以 SIZE = 1024 = 1 << 10 = 1 << 4 << 6
為例,一次最小配置的記憶體是 16 位元組:
heap_alloc(H1, 16)
取得記憶體位址,則需要 個 chunk_status_t
去記錄狀態,可用 個 uint32_t
,符合 Python 程式輸出heap_alloc(H1, 256)
取得記憶體位址,則需要 個 chunk_status_t
去記錄狀態,可用 個 uint32_t
,符合 Python 程式輸出bitfield
數值狀態透過修改 chunk_get_status()
函式中,新增 printf("%u %x\n", idx >> 4, bf[idx >> 4]);
觀察 bitfield
數值變化。
每行 heap_alloc
函式依序輸出 foo
索引值、bitfield
索引值、該 bitfield
數值, 單一行heap_free
函式輸出bitfield
索引值、該 bitfield
數值
觀察 heap_alloc()
函式輸出情況,以第一行 00 0 aaaaaaa9
為例,最低十六進制的數值為 ,取最低兩個位元,得到 chunk_status_t
狀態為 STATUS_ALLOC_HEAD = 0x01U
chunk_t *heads[0];
成員*heads[0];
可參考 GNU Zero Length 說明,GNU Extension 支援
在前述 headsbits
的位元用來表示是否有 free block, chunk_t *heads[0];
則用來串接每個 free block 的記憶體位址。
觀察 heap_create()
初始化的方式與使用 heap_alloc
時候其使用的 chunk_t
的方式
在 heap_create()
中,hd_cnt
的實際數值剛好是 base_size_to_index(size) + 1
。例如 SIZE = 1 << 28
, hd_cnt = 91
且 base_size_to_index(SIZE) = 90
,在 populate_heads
函式中,剛好是指定 h->heads[90]
陣列中擁有 free block
經過多個 heap_alloc()
與 heap_free()
交錯執行後,該 free block 的記憶體空間會被轉換成並用於 chunk_t
heap_alloc()
函式經過前述 heap_t
結構體成員的每個作用,多少可以推得 O(1) 如何的運作模式。
來解讀 heap_alloc(heap_t *const h, uint32_t const sz)
函式實作,主要有以下步驟:
next_available_head_index()
函式取得,尋找符合 needed_sz
的 free block。needed_sz
的 free block,並將剩餘 free block 切割並加入至對應大小的 h->heads
中以 SIZE = 1 << 16 = 65536
為例,查看其中一種程式碼行為:
當 heap_alloc(H1, 16)
時
chunk_t *c
變數為回傳的記憶體位址next_available_head_index
取得 index = 45
對應原本 SIZE = 65536
的 free block65536 - 16 = 65520
,可以預期將 65520
分割成不同的 base size:65520 = 61440 + 3840 + 240
61440
對應 base size index 為 443840
對應 base size index 為 29240
對應 base size index 為 14h->heads[head]
之中,再計算 c
的記憶體位址根據註釋
可以設想最多的執行次數的 worst case,例如 SIZE = 1 << 31
並且 heap_alloc(H1, 16)
取最小單位的空間,最終在所有 7 個 level 中,都放置一個 free block
Linux 核心一度具備同為 時間複雜度的 xvmalloc,不過隨後就被 zsmalloc 取代,請依據 git log 和 LWN 等第一手材料,探討核心開發者的考量因素。
在討論時,避免「舉燭」,應至少將 xvmalloc 移植到 userspace,搭配 tlsf-bsd 的測試和效能評比框架,分析其效益。
參考資訊:
在 git log 中未有標注原因,在 LWN.net 搜尋 xvmalloc,有以下描述:
節錄自 LWN.net: In-kernel memory compression, The zsmalloc allocator
To improve density, the xvmalloc allocator was written, based on the two-level sequential fit (TLSF) algorithm.
Xvmalloc provided high density for some workloads and was the initial default allocator for zram and for the frontswap-driven portion of the initial zcache.
It was found, however, that the high density led to poor fragmentation characteristics, especially for zsize distributions that skewed fat.
Xvmalloc has since been discarded (though remnants of it survive through Linux 3.8.)The "zsmalloc" allocator, proposed by Seth Jennings, is aimed at a specific use case.
The slab allocators work by packing multiple objects into each page of memory; that works well when the objects are small, but can be problematic as they get larger.
In the worst case, if a kernel subsystem routinely needs allocations that are just larger than PAGE_SIZE/2, only one object will fit within a page.staging: zsmalloc: memory allocator for compressed pages
This patchset introduces a new memory allocation library named
zsmalloc. zsmalloc was designed to fulfill the needs
of users where:
- Memory is constrained, preventing contiguous page allocations
larger than order 0(single page) and- Allocations are all/commonly greater than half a page.
在 xvmalloc 實作中,如果絕大多數的要求記憶體空間,接近並大於 PAGE_SIZE / 2
,則該 page 只會有一個物件,形成嚴重的記憶體浪費。 zsmalloc 主要解決此使用情境。
:warning: 應闡述什麼場景特別重視上述的記憶體運用方式。
:notes: jserv
xvmalloc 與前面 TLSF-bsd 實作上不同:
xv_malloc(... size)
只能要求小於一個 page size(Linux 上 page size = 4096)size
會進位成 8 的倍數,包含本身 4 位元組的 block 開銷,最大配置大小為 4088 位元組(4096 - 8)在移植 xvmalloc 的時候,考量以上特性,把 tlsf-bsd 中的測試流程修改成 xvmalloc 的應用情境
目前的程式碼可以在 macOS 與 Ubuntu Linux 22.04 上運作。
在移植的方式上,盡量不更動 xvmalloc 程式碼,透過撰寫缺少的函式、結構體。
xv_malloc.c
, xv_malloc.h
與 xv_malloc_int.h
:移植後的程式碼主體types.h
:移植必要的巨集test_page.h
:移植 linux module header 的函式的標頭檔test.c
:主要撰寫 main()
函式與 benchmark在原本的 xvmalloc 中,每次空間不足時,透過 alloc_page()
向系統申請一個 page,使用 xv_malloc()
會取得 page 結構體與 offset,以此取得記憶體位址。
來查看真如 LWN 敘述的記憶體浪費的情形。在 test.c
透過配置兩組指定記憶體大小,大於 page size / 2
與 4
,並限制總體 page 數量,即可發現記憶體配置失敗,符合 LWN 敘述的記憶體浪費:
使用測量環境
依照 tlsf-bsd 中的測試方式:
realloc()
或高機率 tlsf_free()
tlsf_malloc()
在 xvmalloc 中沒有 realloc
函式,直接移除 realloc
函式,限制記憶體配置範圍 32 ~ 4088。
獲得輸出:
與 tlsf-bsd 對比
用 perf stat --repeat 100 ./a.out -s 32:4088
去測量
用 perf stat --repeat 100 ./a.out -s 32:4088 -c
測量,-c
參數會將取得的記憶體空間指派 0。
使用
得到以下結果
我剛開始學習 Rust,因此可能部分環節有誤。
在有之前 TLSF 的認識之後,來了解如何使用 Rust 去實作,參考 RLSF 範例。
先來了解 Rust 語法跟其作用:
MaybeUninit,可參考 Rust Doc: MaybeUninit
A wrapper type to construct uninitialized instances of T
建立一變數,Rust 會初始化該數值,例如let x: i32
會自動初始化為 0。
透過MaybeUninit
取得未初始化的記憶體空間。[type; length] 語法糖:建立一組陣列 Rust Vec
A contiguous growable array type with heap-allocated contents, written Vec
在記憶體空間上為連續。
pool
變數取得的連續記憶體空間,相當於 C 語言的 void *pool = malloc(65536)
,作為 block 加入至 tlsf allocator 中。
依據 Rust Doc: Generic Type 特性,可在編譯時期決定該型態:
FLBitmap
成員:First level 的位元對應的 Rust 整數型態SLBitmap
成員:Second level 的位元對應的 Rust 整數型態FLLEN
成員:First level 用到的位元數量SLLEN
成員:Second level 用到的位元數量_phantom
成員:根據 Rust Doc: PhantomData: Zero-sized type used to mark things that “act like” they own that data type. Rust 強調所有權(Ownership),這裡假裝擁有資料的所有權。'pool
語法可參考 Rust by Example: Explicit annotation,表示在 TLSF 結構體執行時間,pool
的生命週期夠長(活得夠久)根據註釋,此實作版本的 RLSF 最低配置 16 位元組
將 pool
當作 free block 加入到 TLSF
中
參考 The Rust Programming Guide: unsafe,使用 unsafe
去操作與 C 語言實作中相同的記憶體位址計算:
To switch to
unsafe
Rust, use the unsafe keyword and then start a new block that holds the unsafe code.
You can take five actions in unsafe Rust that you can’t in safe Rust, which we call unsafe superpowers.
Those superpowers include the ability to:
- Dereference a raw pointer
- Call an unsafe function or method
- Access or modify a mutable static variable
- Implement an unsafe trait
- Access fields of unions
透過 Rust raw pointer,將該 block 的記憶體位址取出。
透過文件查詢,可以使用內建的函式 core::ptr::addr_of
取得記憶體位址。
在 Rust Playground 實驗下,可獲得一樣的輸出
NonNull
可當作是支援 raw pointer 的結構體,參考 Rust Doc: NonNull
This is often the correct thing to use when building data structures using raw pointers, but is ultimately more dangerous to use because of its additional properties.
If you’re not sure if you should use NonNull<T>, just use *mut T!