本實驗希望做出一個動態記憶體分配器,我們需要用 C 語言實作 malloc
、free
、realloc
函式。目標是開發出正確、快速且記憶體碎片少的顯式分配器。
我們需要實作以下四個函式:
mm_init
: 在呼叫 mm_malloc
與 mm_free
之前,需要先初始化,例如分配 heap 區域的空間mm_malloc
: 分配並回傳指向一塊已分配區域的有效負載(payload)的指標。分配的區域要在 heap 內,並且不能和其他已分配區域重疊。而分配大小始終要是 8 bytes 對齊的mm_free
: 釋放掉 ptr
指標指向的已分配區域空間。要確保 ptr
指向的空間是利用 mm_malloc
或 mm_realloc
分配過的空間,不然就會變成 UBmm_realloc
: 輸入 ptr
與 size
參數,回傳指向至少 size
大小的已分配記憶體空間的指標。對於輸入有以下條件:
ptr
為 NULL 時,等於 mm_malloc(size)
size
為 0 時,等於 mm_free
ptr
不為 NULL 時,要確保 ptr
指向的空間是利用 mm_malloc
或 mm_realloc
分配過的空間以空間利用率與吞吐量作為評分標準。
mm_malloc
與 mm_realloc
分配且並未被 mm_free
釋放的空間大小與 heap 大小的比值。目標希望減少碎片(fragmentation)的存在。公式如下:最後的總分就會統整成:
分配器需要靠一些資料結構來維護這些記憶體資料。會把記憶體空間切分成不同的組成,用來區分邊界以及區分分配塊或位分配塊。下圖是一個已配置或是空閒塊的結構。
malloc
的大小是不固定的,因此不知道要找多久才能找到 header。因此在每個 block 的最後放上 footer,它就是 header 的副本,一樣存放大小與是否分配。malloc
10 個 bytes,那它實際上會給我 24 bytes 的大小。因為 header 與 footer 各需要 4 bytes,而 payload 是我需要的 10 bytes。所以加起來是 ,再根據 8 bytes 對齊,因此需要給到 24 bytes,而 padding 就是 6 bytes課本與 lab 提供了我們一些方便的 Macros,因此涉及到記憶體,所以大多都是指標操作。以下說明:
ALIGNMENT
: 8 bytes 對齊ALIGN
: 向上取大於等於 size
的 8 bytes 對齊SIZE_T_SIZE
: size_t
大小WSIZE
: Word 大小(header 與 footer 也是 word size)DSIZE
: Double word 大小CHUNKSIZE
: 可延伸 heap 的大小,4 KBPACK
: 將 size
與 alloc
資訊放入 word,通常用於 header 或 footerGET
: 讀取地址 p
的值PUT
: 寫 val
進地址 p
GET_SIZE
: 從 header 或 footer 中獲取 size
,去掉低三位資訊GET_ALLOC
: 從 header 或 footer 中獲取是否已分配HDRP
: bp
是指向某 payload 區域,用來計算該 block 的 header addressFTRP
: bp
是指向某 payload 區域,用來計算該 block 的 footer address。減去 8 bytes 是因為區塊的大小包含 header 和 footer 的 8 bytes。NEXT_BLKP
: 計算下一個 block 的 payload 地址PREV_BLKP
: 計算前一個 block 的 payload 地址mm_init
首先需要初始化 heap 空間。在課本 fig 9-44 有給出範例程式。我們就基於這個版本做解釋。
我們要先了解 Implict Free List 的形式。前面說到的 block 是作為普通塊,一般的分配塊與空閒塊會用上面的架構。但初始化 heap 時會用不同的架構。
會需要這兩個 block 是因為要消除合併時邊界條件。分配器會有一個全域指標 heap_listp
指向 Prologue 的 footer。
在 mm_init
的第 7, 8 行中呼叫 mem_sbrk
。其用意是向 kernel 申請輸入 size 大小的 heap 空間。我們知道作業系統會有一個指標指向 heap 的最上面地址。而在這裡就是使用 mem_brk
。它會先去判斷你申請的大小是否超出範圍。若沒有則回傳回原本 heap 的指標。
我們先用 mem_sbrk
向系統申請了 4 words 的空間,要放入前面講的初始化數值。第 9 ~ 12 行中,我們將其 PUT
進去。Alignment padding 放入 0,Prologue block 大小為 double word。而 Epilogue block 大小為 0。所以到第 13 行時 Implict Free List 就如下圖所示。
extend_heap
在 mm_init
中的 16 行呼叫 extend_heap
,作用是用來擴展 heap 空間。只會有兩個時候要用到該函式:
mm_malloc
找不到適合的空閒塊時,為保持對齊,extend_heap
會向上捨入找到最接近 8 bytes 的空間可以看到它實際上也是利用 mem_sbrk
函式獲取空間。接著它就初始化空閒塊與 Epilogue 的配置。
回顧剛剛 mm_init
時我們輸入的大小是 CHUNKSIZE/WSIZE
也就是 1KB 大小。現在來看實際上記憶體的配置是如何做的。
mm_init
第 13 行後,就如剛剛圖片所示extend_heap
到第 8 行後,配置了 1KB 大小的空閒塊。但很明顯這時候 Epilogue 就不應該放在這裡,而是空間的最後面。extend_heap
第 12 ~ 14 行,剛剛拿到的 bp
就是上圖的 old_brk
。因此要做三件事:
HDRP
剛好就是減去一個 word 大小,接著放入大小與配置位,這也剛好蓋過原本的 Epilogueold_brk
加上 1KB 到 mem_brk
,再用 HDRP
減 4 bytes,就可以獲得最後一個 word,就可以填上 Epilogue圖片藍色部份是已分配,灰色部份是未分配
mm_free
釋放記憶體的部份課本上也有給出範例。而從這邊也得知,為什麼 free
函式只須給入指標不用給大小,就是因為動態分配器利用 free list 在管理記憶體。
那釋放的部份很簡單,只須改變 header 與 footer 的分配位變成 0 即可。但是需要執行 coalesce
去合併可能的空閒塊。
coalesce
合併函式就是把前後如果有空閒塊的話可以合併成一個空閒塊。其目的就是為了減上記憶體碎片。有兩種情況會呼叫合併:
extend_heap
最後,因為擴展的空間實際上就是一個空閒塊,可能前面會有小塊的空閒塊mm_free
最後,每當釋放一塊空間時就需要做合併那合併就會有以下四種情況:
mm_malloc
要分配記憶體空間,首先要先檢查 size
。要確認其是 8 bytes 對齊的。最小也需要 16 bytes。接著在 Free list 找到適合的 block,放入所需的空間。若是找不到可以放得下的 block ,就會呼叫 extend_heap
擴展新的空間。再將其放入。
place
當找到一塊空閒塊後,就可以執行 place
函式將其放入。而會有兩種情況:
DSIZE
,這時就要分割。一塊變成已分配塊,另一塊則是新的空閒塊。find_fit
要從 Free list 找到適合的 block 放入,我一共實作了三個放置策略(placement policy):
first_fit
從頭開始找到第一塊可以放置的 block。優點是它趨向將較大的空閒塊保留在後面,而缺點就是靠近 list 前面會留下較多碎片。
next_fit
它是基於說上次已經在這裡找到匹配的 block 了,那很可能下一次就可以很快找到可匹配 block。其優點就是比 first fit 快速,但有研究表明空間利用率比較低。
而實作方面也比較複雜些,在找到可以的空閒塊後,需要檢查下一塊是否為 Epilogue block。這時就要重製 next_listp
。
遇到問題:Checking mm_malloc for correctness, ERROR [trace 6, line 3926]: Payload (0xf4934918:0xf49389a1) overlaps another payload (0xf4937f58:0xf493992a)
best_fit
它會尋找整個 free list,檢查所有可放下的 block 中最小的 block。優點就是空間利用率一定最好,而缺點就是要跑比較久。
mm_realloc
可參考背景知識提及要做的三件事。假設 size
不為 0,這時會出現兩種情況:
size
大於 ptr
的大小: 這代表需要分配一個 size
大小的塊,並將 ptr
裡的值複製到新的塊size
小於 ptr
的大小: 一樣分配一個 size
大小的塊,但這時需要截斷 ptr
的值,抓 size
大小的值複製到新的塊Segregated Free Lists 是 Explicit Free Lists 的改良版本,我打算一起實作。它與 Implicit Free List 的差別為它將 payload 的前兩個 word 當作 Next
與 Prev
指標,以實作 double linked list 將 Free list 串起來。也就是說一個空閒塊最小有 16 bytes (4 words)。
這張圖有點錯,後面實作會把
Next
跟Prev
的前後顛倒
而 Segregated 的核心思想就是將不同大小的 Free list 按 2 的幂比例分開。我們就用一個陣列來管理,每個元素就存放一個 Free list。
因為記憶體操作涉及許多複雜的指標操作,因此我參考 CS:APP Malloc Lab 解題筆記的操作並加以解釋。我們需要額外多做一些操作,用來獲取與放入 Next
與 Prev
指標。
NEXT_PTR
& PREV_PTR
負責獲取 Next
與 Prev
的地址。
GET_NEXT
& GET_PREV
負責獲得下一個空閒塊的地址。但這邊的指標操作有些複雜,考慮以下空閒塊。bp
指標為 0x1000
。考慮執行:
bp
強制轉型成 (char *)
: 因為指標運算的步進單位是基於指標所指向型別的大小。若是指標型態為 int
的話,bp + 1
就會被編譯器翻譯成 bp + 4
。因此為確保 bp + WSIZE
是正確運算須強制轉型。這時就會獲得指標指向 0x1004
,考慮執行:
char
類型指標的指標。再對它 dereference。NEXT_PTR(bp)
回傳的指標是 char
型態的,若是直接 dereference ,編譯器會以為裡面是正常的數值,所以只取出 char
大小,也就是 1 byte 的數據。就會回傳 0x48
,很明顯就有問題。char
類型指標的指標。告訴編譯器:「這個地址上的值是另一個地址」,這樣在 dereference 時就會把整個 word 都抓出來了。也就是 0x2048
。PUT_PTR
負責將 ptr
的內容放入 p
中。這次它將兩邊都強制轉型成 unsigned int
:
p
強制轉型為 unsigned int*
,表示我們準備將地址存儲為一個 4-bytes 無符號整數。ptr
強制轉型為 unsigned int
,表示我們把指標值(記憶體地址)轉換為整數位元組表示。*(unsigned int *)(p)
,將整數值 ptr
存入 p
所指向的記憶體區域。這裡學到許多記憶體底層的指標操作,詳細指標操作可以看:
mm_init
初始化與前面差不多,增加兩件事:
extend_heap
回傳的第一個空閒塊裡的 prev
與 next
初始化。seg_listp_arr
陣列,大小如同下列所示。以二的幂為單位分塊。(最小的塊為 16 bytes),一共 16 個元素。extend_heap
一樣要在新空閒塊中設置 prev
與 next
。
coalesce
合併 free block 與前面雷同,採用 LIFO 策略,有四個 case:
insert_blk
& delete_blk
要從 Free list 中刪除一個 Free block,只要斷開 double linked list 的節點就好。因此找到前後的節點,並檢查是否存在。
prev
指標設置為 NULL,並更新 head 為第二個節點next
指標設置為 NULLnext
設置成 bp
,將後一個節點的 prev
設置成 bp
要插入空閒塊到 Free list 中,我們可以設計使其大小位於適合的位置。也就是說,越大的 block 會在 Free list 的越後面。透過判斷 bp
大小在 Free list 的哪個位置來實作。
bp
節點的 prev
設置為 NULL,next
設置為下一個節點。檢查下一個節點是否存在(若不存在,代表原本為 empty list)將下一個節點的 prev
設置為 bp
。最後更新 head 為 bp
。根據 block 的大小尋找插入與刪除要在哪個 Free list 中使用。而大小從最小的 16 bytes 開始,一共 16 個。
演算法的執行流程與之前相似,只是現在有 Free list 就可以比較方面做搜尋。注意到只須從大小大於該 block 的 Free list 開始搜尋即可。
place
在 mm_malloc
中找到適合的空閒塊後便要放入。如果配置一塊記憶體空間,那勢必要把這個空閒塊從 Free list 中移除。也會分兩種情況:
First fit 的速度確實比 Best fit 還快一點。但是記憶體利用率差不多。因此整體的分數是一樣的。
記憶體(memory)作為磁碟(disk)的緩存,使用 page table 來管理。下圖顯示 Physical memory 是記憶體,而 Virtual memory 是磁碟。而記憶體的一個儲存單位就是 Page frame,裡面會存放磁碟中 swap 過來的數據。而 page table 則是會記錄兩者之間的狀態。
Page table entry 由對應的物理地址與一個 valid bit 組成。物理地址就是記憶體中實際的初始地址,而若 valid bit 為 1,代表記憶體中對應的物理地址緩存了該虛擬頁。
從 CPU 產出的 Virtual address 是由 Virtual page number(VPN) 與 Virtual page offset(VPO) 組成。
假設 page size 為 4KB,page table 如下表所示。VPN 是 3,VPN 是 0x200,那其對應的位置應該是 VP3 再位移 0x200,也就是記憶體 0x3200 的位置。
首先,在控制暫存器中有一個 Page Table Base Register(PTBR)用於指向 Page table 所在位置。接著就用 VPN 查詢對應的 Page table entry,若 valid bit 為 1,則可以直接使用 Physical page number,而虛擬與物理的偏移量是一樣的。因為 page size 一樣大。
PTEA = PTBR + (VPN * page size)
1~3. 與 page hit 一樣
4. 當回傳的 PTE 的 vaild bit 是 0 時,代表資料沒有緩存在記憶體,就會觸發 page fault exception。handler 就會負責選出一個記憶體中要被替換的 page ,稱作 victim page。
5. 從記憶體中將 victim page 放到磁碟
6. 將需要的資料 swap 回記憶體
7. CPU 要重新發送 VA
快取也是可以存放 PTE 的,若發生 cache miss,再去記憶體查詢 page table 即可。
可以發現到每次都需要去記憶體裡的 page table 找 PTE 回到 MMU,產生 PA 後再去記憶體取資料,這樣每次都需要兩輪的時間(記憶體非常耗時)。因此我們可以在 CPU chip 內建立一個小小的 buffer 用來緩存 PTE,稱作 TLB。
在 CPU 產生 VA 後,會先將 VA 傳給 TLB。而 TLB 本身就是一個緩存機制,因此它的架構與 Cache 類似。因此我們可以將 VA 裡面的 VPN 拆分成 tag 與 index 位(位元數根據 n-way assocativity)。
當在 TLB 中找不到緩存的 PTE 時,代表該 PTE 仍存放在記憶體裡的 page table。
我們現在來模擬一次 VM 的運作方式。首先給出以下假設:
基於上面假設,Virtual address 有 14 個 bits,Physical address 有 12 個 bits。而因 page size 為 64 bytes,所以 VPO 與 PPO 都是 6 bits。
假設 CPU 產生一個 VA 為 0x03D4
。因為 TLB 是 4-way,因此 index 佔兩個 bits。這樣我們就可以拿 index 與 tag (VPN)給 TLB。
下圖是目前 TLB 的狀態,因為 index 為 3,所以去 set3 尋找,而因 tag 是 3,所以找到一樣的數據,即是紅框的位置。接著檢查它的 vaild bit ,若為 1 則代表其數據確實存在記憶體中。因此 0D
就是我們要的 PPN。
這時候回到 MMU 將 PPN 與 PPO(與 VPO 一樣) 組合起來就是 PA 了。就可以向記憶體直接尋址抓取資料。