MIT 6.S081 Lab Allocator 聊聊buddy allocator


前言

Lab Allocator代碼量很少,主要是用xv6已經寫好的buddy allocator替換掉kallocator。kallocator簡單的將內存分為4096bytes(下簡稱PGSIZE)頁面,將空閑頁面串接成雙向鏈表。這種方式很簡潔,可以很好的應用First Fit算法,且沒有用多余的數據結構去管理內存。但一次只能分配完整的一頁。buddy allocator可以靈活的分配2^k * LEAF_SIZE字節的內存,且分配和回收的時間復雜度並不高,是一種很優良的內存分配算法。
 
盡管如此,我個人並不認為在內核上使用buddy_allocator是一種比較好的方法,讓內存去管理這么多的小內存塊是極不理智的。假如我們真的要在xv6上開發用戶程序,我們也肯定不是通過sbrk去請求堆空間,而是通過user/umalloc.c下提供的api來請求堆內存。使用umalloc.c提供的api時,如果調用free(ptr),進程並不會調用sbrk去消除堆內存,而是將這塊空間存放到自己的freelist中。當進程下次請求堆空間時會首先查看freelist,如果空間足夠就不再調用sbrk向內核請求堆空間。
由於xv6並沒有實現內存置換算法,因此 進程申請的內存只能等到進程結束時被內核回收。很明顯,相比於直接將buddy allocator應用到內核上,更明智的方法是把buddy allocator應用到user/umalloc.c下,進程仍然一次向內核請求完整的一頁,然后在umalloc.c下使用buddy allocator細化空間粒度。
不過既然Lab要求我們直接替換kallocator,那我們就不管它了。
 
項目已提供的buddy allocator路徑為kernel/buddy.c。在本項目中我們要完成三個任務:
1) 使用buddy alloctor來管理空閑內存
2) 修改file.c下的ftable(系統文件表),使系統最多可打開文件不再受NFILES的限制
3) 優化buddy allocator的空間消耗

 

buddy算法的執行過程不再贅述,如果不了解建議看一下wiki pedia:https://en.wikipedia.org/wiki/Buddy_memory_allocation
在這里感謝 ,強烈推薦關注他寫的關於6.S081的blog:https://blog.csdn.net/redemptionc/category_10065273.html
 
本blog僅討論 buddy的實現buddy空間的優化

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這塊的算法,請見諒。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM