contributed by < WangHanChi
>
block_t
這個結構體包括了該 block 的有效的空閒大小也有指向下一個以及前一個 block 的指標。
如上圖,將閒置的 block 以 list 進行串接。
而 prev
和 next
則用於將該塊 block 連接到空閒記憶體一堆 block 雙向 list 中,以便於在執行 malloc() 時快速地查找可用的空間。當此 block 被釋放時,也可以通過這個結構體的指標在 list 中搜索前一個和後一個 block,以實現 block 合併。因此,這個結構體的用途是為了管理 memory pool 中的可用 block 所設計的。
這邊是根據機器的架構來設定 word_size,並且根據 word_size 來決定 log2_word_size
最後還設定了 header_size 的大小為 3 個 word_size。
而我的機器是 X86-64 的,所以我的 word_size 會是 64 / 8
也就是 8
, log2_word_size
的大小會是 3
。
在這邊可以看到他在假設沒有發生 memory leak 的情況下,設定了 pool 的大小 pool_size
,以及剩下可以使用的空間 pool_free_space
pool_init
上面這段程式碼主要做的就進行初始化。並且檢查了初始化池的大小被需要大於 3 個 word_size
才可以進行初始化。
round_up
這段程式碼主要在進行的是進位到指定的數字,來確保每次要進行 alloc 的空間都會是 word_size 的倍數。
下方的註解也同時印證了功能
先以 64 bit 的機器為例
再以 32 bit 的機器為例
由上面的例子可以發現這邊有小錯誤,因此將其進行修改
這樣就可以在應對 64 bit 的機器時,加上 7
; 而在面對 32 bit 的機器的時候可以加上 3
get_loc_to_place
這個函式是用來找尋有沒有一個可以用的空閒 block 的。
可以看到它會先去搜尋 prev
的部份,如果沒有找到足夠大小的話,就會搜尋 next
的部份。
pool_malloc
:pencil: 由於這個函式比較長,所以擷取部份的程式碼來講解
這邊先將 size 進行對齊 word_size ,再根據新的 _size ,來找到大小合適的閒置 block。
這邊主要在設定新的節點的大小以及 size 。可以看到它都是先將原本的 next
與 prev
存在 next_pt
與 prev_pt
中。在 size 改變完成之後,再把這兩個指標設定回 current 的 next
與 prev
。
pool_calloc
可以明顯的看到 calloc
就是使用了 malloc 加上 memset 的組合。
先將一段記憶體分配之後在初始化為 0
。
pool_realloc
這邊可以看到它會先使用新的 size 進行一段記憶體的分配。
再來會把 addr
的值複製到 ptr
中。最後再將 addr
釋放。
目前看起來沒有什麼大礙,但是感覺在處理字串的時候會出問題 ('\0') !?
但是後來想想,這個問題應該交由使用者來處理,不應該是在配置記憶體時考慮
get_loc_to_free
以上為部份程式碼
這邊可以看到他的他先將 loc 設定為 NULL,若是在兩個 for 迴圈(prev
與 next
)透過比大小的方式找到合適的 free 區域,就會回傳那個區域的指標。若是沒有找到,就會回傳 NULL。
pool_free
這邊先對 pool_free_size 進行釋放後到大小的調整,接著透過上面所提到的 get_loc_to_free
來找到哪個部分可以進行釋放,再將其轉型成我們所定義的 block_t
。
接下來會依據 block_pt
與 free_pt
分成兩個部分
這邊可以看到他先檢查了要釋放的區域是否符合 block 的地址
+ block 的大小
+ word_size
若是符合的話,就會進行 next
與 prev
的連接。
這邊是 (uintptr_t) block_pt >= (uintptr_t) free_pt
的情況
同樣也是確認了釋放的區域是否符合 block 的地址
+ block 的大小
+ word_size
但是這邊沒有做連接的動作,推測是當 block_pt
的記憶體位址大於等於 free_pt
的記憶體位址時,代表需要把 free_block
與 block
合併。在此情況下,已經把 free_block
的空間加入到 block
中了,而 block
的起始位置是在原先的 free_block
之前,因此已經不需要再次與前面的記憶體空間連接。所以在這個情況下,沒有必要再次連接前後的記憶體。
首先先複習 First-fit 與 Best-fit,可以透過 Best-Fit Allocation in Operating System 與恐龍書歸納出幾個重點。
First-fit 演算法是指從頭開始搜尋可用的記憶體區塊,當找到第一個大於等於要求大小的區塊時,就將該區塊分配給申請記憶體的程序。這種演算法的優點是簡單且快速,但可能會造成內部碎片(未使用的小塊記憶體),進而浪費可用的記憶體空間。
Best-fit 演算法是指從所有可用的記憶體區塊中,選擇最小但大於等於要求大小的區塊分配給申請記憶體的程序。該演算法可以最小化內部碎片,但需要額外的搜索時間來找到最佳的可用區塊,因此較為耗時。
由上面可以知道 best-fit 的重點是他會找到最小的記憶體區塊,所以我們可以在目前 first-fit 的基礎上,加上一個暫存 block,來代表符合 block 的大小同時又是最小值,
因此可以將 get_loc_to_place_best_fit
如下表示
可以看到他在進行走訪時,不會因為找到一個可用的區塊就回傳,而是會繼續走訪,嘗試找到更小的記憶體區塊。
接下來 get_loc_to_free_best_fit
也要進行修改
其實在閱讀這份程式碼的時候覺得很抽象 –-,可能是因為實作上有缺失或是我不了解 memory pool
不要濫用「抽象」一詞,以下摘錄國語辭典:
不懂就說不懂,不要用不相關的詞彙搪塞。
:notes: jserv
這邊程式碼可以看到他在一開始的全域變數中就已經使用到了 current 這個變數名稱,但是在函式 get_loc_to_plcae
中又同樣使用了一樣的變數名稱
這邊可以看到它先用了 ret 來儲存要配置的位置,但是後來用直接修改掉了這個位置,所以這邊應該的缺失是應該用以 ret 來做為要配置的位置。
這邊定義了三個結構體 metadata_t
, rbnode_t
與 malloc_t
metadata_t 紀錄了大小與空閒空間的大小還有以 list 的方式指向前一個及下一個節點的資訊。
這個結構主要在實作紅黑樹的節點,但是同時也參雜了其他的資訊,像是 metadata_t 等等
其中還有一些目前不確定用意的資訊,像是 size_t n_active
,等看到下面理解了再回來補!
這個結構包含了紅黑樹的根節點, metadata 的最後一個節點, 指向最後一個 page 的指標, page_size 等等的。
特別看到它塞了一個 mutex 在裡面,目的是確保在進行 malloc 的時候是 thread-safe 的。
因為在標準函式庫中的 malloc 也是 thread-safe 的,因此這裡應該是想要做一樣的功能。
Interface Attribute Value malloc(), free(), calloc(), realloc() Thread safety MT-Safe
再來是很多的巨集 define
這兩個巨集代表了記憶體的狀態,其中 YFREE 代表該塊記憶體已被釋放,NFREE 代表該塊記憶體未被釋放。這些數值都是為了避免特定的數值被誤認為有效的指標或記憶體狀態而設定的。
可以看到 64-bit 的機器在進行 malloc 的時候會以 16 個 byte 進行 align,詳情可以參考 你所不知道的 C 語言:記憶體管理、對齊及硬體特性 中有詳細的介紹並且也有實驗證明。
可以參考下面的圖(出處:〈自己擼了一個 malloc 內存分配器〉) 或是 CSAPP 第 592 頁中的圖 9-35,x
就是紅色的那個區塊,而 META_DATA
就是藍色的那個區塊,因此這個函式可以得到 PAYLOAD 的地址,也就是下圖中灰色的區塊。
重新製圖,留意使用一致的術語。
:notes: jserv
檢查所傳入的指標是否是有效的記憶體區塊。
YFREE
或 NFREE
,則視為有效的記憶體區塊。YFREE
也不等於 NFREE
,則視為無效的記憶體區塊。由於有些操作比較基本,就不特別說明
insert_node
這個函式是用來在紅黑樹的節點中插入一個新的記憶體區塊(metadata_t)
可以看到如果節點中已經有 n_active
個有效值了,表示 tab_values 陣列已滿,需要擴大陣列,使用到了 resize_tab_size 這個函式; 若是 tab_values 陣列沒有滿,則會進到 else-statement 從 tab_values 陣列的第0個位置開始,查找一個空的元素,直到找到一個空的位置或陣列結束為止。
在找到了之後, tab_values 陣列中的第 i
個位置上,插入新的記憶體區塊 (metadata_t),再來就是更新節點中n_active變數的數值。
get_new_page
可以在 get_new_page
中看到使用 sbrk() 系統呼叫來分配這些 page 。如果分配失敗,它會回傳 -1 並將錯誤設定為 ENOMEM。如果是第一次呼叫的話,就會進到這個 if-statement
它會將 g_info.end_in_page
設定為現在 heap 的結尾。
get_in_page
這段程式碼是用來在 page 中分配新的記憶體區塊。它會在 g_info.end_in_page
指向的位置建立一個新的 metadata_t,並使用 size 設定其大小和其他屬性。然後它會將這個新的區塊插入到 g_info 的 list 中,並將 g_info.end_in_page
指向下一個可用的位置。
get_heap
這段程式碼是用來從 heap 中分配新的區塊。它會檢查 g_info.page_remaining
是否足夠分配這個區塊,如果不足則使用 get_new_page()
分配更多的 page 。然後它會從這些 page 中使用 get_in_page() 分配區塊,並更新 g_info.page_remaining
的值。最後,它會回傳新分配區塊的指標。
split_block
這個函是的功能是會將一個很大的記憶體區塊,切分成兩個比較小的區塊。
可以看到他先將這個 node 移出了閒置的 list ,再來判斷它可不可以進行切割,如果可以的話,再進行分割,切完之後,再把切下來的空間 new
插入到閒置的 list 裡面。
malloc
可以看到在 malloc 的過程中,使用了 mutex 來對共享的資料進行保護,他的 critial section 是從決定分配的大小開始,到回傳之前
在函式開始時,先檢查要分配的記憶體大小是否小於預設的區塊大小 SIZE_DEFAULT_BLOCK (32),如果是,就把大小設為 SIZE_DEFAULT_BLOCK。然後使用 ALIGN_BYTES 巨集對記憶體大小進行記憶體位元對齊,並且加上一個 metadata_t 的大小,詳情可以看到上面的記憶體區段的圖。
接下來,使用 search_freed_block()
函式在空閒記憶體區塊的紅黑樹中查找是否有足夠大的區塊,如果有就使用 split_block()
函式將該區塊切成兩個部分:一部分用於分配所需大小的記憶體,另一部分放回空閒記憶體塊的紅黑樹中,以便後續的分配使用。如果找不到足夠大的區塊,就使用 get_heap()
函式從作業系統中獲取一個新的記憶體區塊。
free
在進行 free 時候,同樣的也是要進行多執行緒的保護。
接下來就進行一些檢查,確定是否為有效的指標,還有重複釋放的問題
再來就是把要釋放的記憶體區塊前後嘗試連接成更大的區塊,結束後檢查這個區塊的下一個區塊是否存在,若是不存在的話,就要通過呼叫 brk() 調整 break
的位置; 若是存在的話,就將這個要是放的區塊放入空閒的 list 裡面。
calloc
這邊也是通過 malloc()
先進行記憶體配置,接下再將 memset
的動作用 mutex 鎖住
可以從 測驗題說明 看到編譯以及測試的方式
也從 〈你所不知道的 C 語言:動態連結器篇〉 看到可以在 malloc
這個函式中加入
來確定動態連結庫有成功被載入並且使用
可以看到結果如實配置指定的空間:
先從 man page 以及 CSAPP 來學習 brk
, sbrk
, mmap
, munmap
這幾個系統呼叫
sbrk
可以透過將 kernel 管理的 heap 指標上移或是下移來以此分配空間。
因此 increment 可以是正, 負還有 0 ,0 就不配置空間, 大於 0 就配置空間, 小於 0 就釋放空間。
如果成功的話,就會回傳舊的 brk
指標,如果出錯的話就會回傳 -1
brk
使用的方式就是根據傳入的 addr
作為新的 heap 頂端指標,因此從舊的 brk
指標到新的 addr
距離就是我們所需要分配的空間。
搭配參照第 11 週測驗題
:notes: jserv
引數 start
通常被定義為 NULL, length
被定義為要分配的大小
port
被定義為描述新映射虛擬記憶體的訪問權限共有幾種,例如
flags
指定映射對象的類型,映射的選項及是否可以共享
fd
是有效的檔案描述子,如果在 flags
有設定 MAP_ANONYMOUS
,就要把這個設成 -1
offset
是要從檔案的哪裡開始映射,通常設為 0
清除會從引數 addr
開始進行清除,並且從起點 addr
開始 length
的記憶體空間都會被釋放。
首先先添加所需的標頭檔 #include <sys/mman.h>
再來需要修改的地方是配置記憶體的函式
change_break
原本是用來進行釋放記憶體 (移動 heap 指標) ,而我們需要將它變成 munmap 的方式進行釋放,修改如下
以及 get_new_pages
這個函式需要改動
將原本使用 sbrk 進行配置的部份都改成 mmap,改動如下
接下來進行編譯
可以正常的編譯,接著開始測試:
發現執行到一半的時候會產生 Segmentation fault。
思考一下 bug 出現在哪!
在參考了 quiz11 以及 mmap-malloc 後,取消上面的修改,改成引入 mmap-malloc,並且用巨集讓 malloc 可以從 brk/sbrk 切換到 mmap。詳細程式碼在 WangHanChi/rbtmalloc
主要透過 MMAP
這個巨集決定要使用 brk/sbrk 或是 mmap 來分配記憶體。
我將結構體 malloc_t
稍微做了修改
主要都是將 mmap 所需要的結構成員獨立出來,避免不必要的定義。
接著在 malloc
, realloc
, free
, calloc
這些函式中可以看到都使用了 define MMAP
來決定要使用哪一種方式進行分配
可以看到在執行測試 make test
了之後,確實有成功動態載入並呼叫了 ls
這個命令
關於 rbtmalloc 更多的開發紀錄在 rbtmalloc 開發紀錄 !
先閱讀權威材料,例如 CS:APP,第 9 章提到如何建構記憶體配置器。
:notes: jserv