前言
buddy的數據結構和初始化
struct sz_info { Bd_list free; // 空閑空間鏈表。 char *alloc; // 用一個bit記錄某個塊是否被分配出去了 char *split; // 用一個bit記錄某個塊是否發生了分裂 }; typedef struct sz_info Sz_info; static Sz_info *bd_sizes; // bd_sizes[k]記錄了2^k * LEAFSIZE大小的塊的分配信息
static void *bd_base; // start address of memory managed by the buddy allocator static struct spinlock lock
buddy_allocator首先需要一段連續內存來存放這些元數據。這段內存的大小可以根據buddy allocator所管理的內存地址范圍高精尖海量算獲得(曹曹草震怒.jpg),我們下面重點分析一下bd_init完成alloc、split初始化的部分:
void bd_init(void *base, void *end) {
......
nsizes = log2(((char *)end-p)/LEAF_SIZE) + 1; if((char*)end-p > BLK_SIZE(MAXSIZE)) { nsizes++; // round up to the next power of 2 }
.....
for (int k = 0; k < nsizes; k++) { lst_init(&bd_sizes[k].free); sz = sizeof(char)* ROUNDUP(NBLK(k), 8)/8; // sz = sizeof(char) * ROUNDUP(NBLK(k), 16)/16; bd_sizes[k].alloc = p; memset(bd_sizes[k].alloc, 0, sz); p += sz; }
......
for (int k = 1; k < nsizes; k++) { sz = sizeof(char)* (ROUNDUP(NBLK(k), 8))/8; bd_sizes[k].split = p; memset(bd_sizes[k].split, 0, sz); p += sz; } p = (char *) ROUNDUP((uint64) p, LEAF_SIZE);
...... }
首先需要計算nsizes,即到底這段空間需要用多少"階"的bd_allocator管理。階的值直接確定了bd_sizes的長度。
當nsizes確定后,需要對每個"階"(下面簡稱k)下的alloc、split進行分配。NBLK宏計算k階下有多少個block可供分配,alloc、split均用一個bit標注這個block是否被分配/分裂,因此alloc、split所需空間大小均為 ROUNDUP(NBLK(k), 8) / 8。除以8是因為一個char可以用8個bit記錄這些信息。
盜個圖來大概展示一下buddy allocator下的內存布局,圖片源於https://blog.csdn.net/RedemptionC/article/details/108012836
buddy的代碼到目前為止還是非常親民的,后面就越來越讓人想錘牆(
標注已經分配和無法分配的空間
已經分配的空間其實就是分配給元數據的空間(元數據包括bd_sizes,bd_sizes[k].alloc,bd_sizes[k].split等)。這段空間從base開始,到執行完第二個for循環結束后的p終止。這段空間需要被我們標注為已分配:
void bd_init(void* base, void* end) { ....... int meta = bd_mark_data_structures(p); int unavailable = bd_mark_unavailable(end, p); void *bd_end = bd_base+BLK_SIZE(MAXSIZE)-unavailable; ....... } int bd_mark_data_structures(char *p) { int meta = p - (char*)bd_base; printf("bd: %d meta bytes for managing %d bytes of memory\n", meta, BLK_SIZE(MAXSIZE)); bd_mark(bd_base, p); return meta; }
我們要注意,當k階的block被標注為已分配時,所有在這個block下,階數小於k的block也必須要被標注為已分配。具體代碼在bd_mark中,也不算太難看懂。
void bd_mark(void *start, void *stop) { int bi, bj; if (((uint64) start % LEAF_SIZE != 0) || ((uint64) stop % LEAF_SIZE != 0)) panic("bd_mark"); for (int k = 0; k < nsizes; k++) { bi = blk_index(k, start); bj = blk_index_next(k, stop); for(; bi < bj; bi++) { if(k > 0) { // if a block is allocated at size k, mark it as split too. bit_set(bd_sizes[k].split, bi); } bitset(bd_sizes[k].alloc, bi); } } }
無法分配的空間可能比較難理解。如果最終的階為nsizes-1,我們實際可以用buddy管理的空間大小為 ((1L << (nsizes - 1)) * LEAF_SIZE),即buddy.c中定義的宏HEAPSIZE,而這個空間大小很可能已經超過了end - base的大小。因此我們必須將[end , HEAPSIZE)間的空間同樣標注為“已分配”,來避免將這片空間分配出去。
下面講講buddy中最為迷惑的代碼 bd_initfree。
bd_initfree
bd_initfree的代碼非常簡潔,但也非常晦澀難懂,比xv6中進程調度的代碼還要難以理解。
int bd_initfree(void *bd_left, void *bd_right) { int free = 0; for (int k = 0; k < MAXSIZE; k++) { // skip max size int left = blk_index_next(k, bd_left); int right = blk_index(k, bd_right); free += bd_initfree_pair(k, left, bd_left, bd_right); if(right <= left) continue; free += bd_initfree_pair(k, right, bd_left, bd_right); } return free; }
簡單來看,bd_initfree的工作非常簡單,就是將[left, right)所有的空間分割成不同階大小的blocks,並將blocks的地址添加到相應階下bd_sizes的free中。而如何將這些空間切割成連續的、buddy間不相鄰的block是一個較為困難的問題。我們重點關注一下bd_initfree是怎么解決這個問題的。
首先我們注意到,同一個階(假設為k)下的所有空閑的blocks間兩兩不能是buddy。如果存在兩兩是buddy的情況,那么這兩個block應該是k+1階下的某一個block。示意圖如下:
bd_initfree針對這個問題,選擇從空閑空間的兩端開始收集空閑塊,且每個階下只收集至多兩個空閑塊。這樣就不會出現空閑塊間相鄰且互為buddy的情況。
這樣,同一階下空閑塊間不能為buddy的問題得以解決,但bd_initfree這種分配方法,真的能讓所有空閑塊在[bd_left,bd_right)間首尾相接么?會不會出現空閑塊間覆蓋的情況?
我們可以證明該算法可以讓空閑塊間首尾相接。
整個證明分為兩部分:
1)證明bd_initfree的for循環每完成一次,自bd_left開始到left的空閑塊是連續的(左連續),自right開始到bd_right的空閑塊是連續的(右連續)
2) 存在某個階數k,使得左連續的塊和右連續的塊在中間某處拼接起來
證明了1和2,即可證明[bd_left,bd_right)間所有的空閑block是首尾相接的。
第一個證明其實很簡單,只要是按照步驟畫一下圖,即可很直觀的看出。下圖中紅色表示空閑區,灰色表示非空閑區。最初始時bd_left的左側是buddy allocator的元數據區域,bd_right的右側是無法分配的區域,這兩塊區域均已被標記為“已分配”。雖然隨着for循環的進行k越來越高,但仍然可以保持左連續這一性質:
上圖中比較迷惑的是k=2,3時的情況。在上圖的例子中我舉的是特例,讓k=2,3時互為buddy的塊都包含了k=0時已分配的空間。因此相應的blocks不應被包含在freelist中,故在上圖中被標為了灰色。
右連續可以由對稱性導出,這樣第一個證明是成立的。
第二個證明同樣可以畫圖證得,也可以用數學更為嚴謹的證明。下圖中是分配[9, 19)這一塊空間時,k每次變動時添加到freelist的空間示意圖:
我們設 k = K(K != 0)時,左側已分配的block中下標最大的leaf下標為l_idx,右側已分配的block中下標最小的leaf的下標為r_idx。如上圖中,當k=2時,
l_idx=11,r_idx=16。
這樣我們就證明了[bd_left,bd_right)間的塊一定是左右相連的。
TODO:
最近時間非常緊,本blog潦草發布僅僅是為了和群友討論一下buddy這塊的算法,請見諒。