4夥伴系統算法

講了這麼多了,很多人肯定會一頭霧水,前邊提到的都是些數據結構或者是些概念性的東西,真正對動態頁面的管理機制在哪裏?換句話說,如何將每個節點,每個區中的頁框分配給進程?要理清這個思路,我們首先必須學習一種算法 —— 夥伴系統算法。

 

內核要分配一組連續的頁框,必須建立一種健壯、高效的分配策略。爲此,必須解決著名的外部碎片(external fragmentation)問題。頻繁地請求和釋放不同大小的一組連續頁框,必然導致在已分配頁框的塊內分散了許多小塊的空閒頁框。由此帶來的問題是,即使有足夠的空閒頁框可以滿足請求,但要分配一個大塊的連續頁框就可能無法滿足。

 

Linux 採用夥伴系統(buddy system)算法來解決外碎片問題。把所有的空閒頁框分組爲11個塊鏈表,每個塊鏈表分別包含大小爲1, 2, 4, 8, 16, 32, 64, 128, 256,512和1024 個連續的頁框。對1024 個頁框的最大請求對應着4MB 大小的連續RAM塊。每個塊的第一個頁框的物理地址是該塊大小的整數倍。例如,大小爲16 個頁框的塊,其起始地址是16 × 212(212 = 4096,這是一個常規頁的大小)的倍數。

 

我們通過一個簡單的例子來說明該算法的工作原理。

 

假設要請求一個256 個頁框的塊(即1MB)。算法先在256 個頁框的鏈表中檢查是否有一個空閒塊。如果沒有這樣的塊,算法會查找下一個更大的頁塊,也就是,在512 個頁框的鏈表中找一個空閒塊。如果存在這樣的塊,內核就把256 的頁框分成兩等份,一半用作滿足請求,另一半插入到256 個頁框的鏈表中。如果在512 個頁框的塊鏈表中也沒找到空閒塊,就繼續找更大的塊 —— 1024個頁框的塊。如果這樣的塊存在,內核把1024個頁框塊的256 個頁框用作請求,然後從剩餘的768 個頁框中拿512個插入到512個頁框的鏈表中,再把最後的256個插入到256個頁框的鏈表中。如果1024個頁框的鏈表還是空的,算法就放棄併發出錯信號。

 

以上過程的逆過程就是頁框塊的釋放過程,也是該算法名字的由來。內核試圖把大小爲b的一對空閒夥伴塊合併爲一個大小爲2b的單獨塊。滿足以下條件的兩個塊稱爲夥伴:
• 兩個塊具有相同的大小,記作b。
• 它們的物理地址是連續的。
• 第一塊的第一個頁框的物理地址是2×b×212的倍數。

 

該算法是迭代的,如果它成功合併所釋放的塊,它會試圖合併2b 的塊,以再次試圖形成更大的塊。

 

看暈了吧?如果實在理解不了就自己拿筆畫一畫,這個算法的原理還是比較簡單的,下面我們來看看Linux具體是怎麼實現的:

 

1 數據結構

 

Linux 2.6 爲每個管理區使用不同的夥伴系統。因此,在80x86 結構中,有三種夥伴系統:第一種處理適合ISA DMA 的頁框,第二種處理“常規”頁框,第三種處理高端內存頁框。每個夥伴系統使用的主要數據結構如下:
(1)前面介紹過的mem_map數組。實際上,每個管理區都關係到mem_map元素的子集。子集中的第一個元素和元素的個數分別由管理區描述符的zone_mem_map和size字段指定。
(2)包含有11個元素、元素類型爲free_area的一個數組,每個元素對應一種塊大小。該數組存放在管理區描述符zone_t的free_area字段中。

 

夥伴系統算法

 

如圖,我們考慮管理區描述符中free_area數組的第k個元素,它標識所有大小爲2k的空閒塊。這個元素的free_list字段是雙向循環鏈表的頭,這個雙向循環鏈表集中了大小爲2k頁的空閒塊對應的頁描述符。更精確地說,該鏈表包含每個空閒頁框塊(大小爲2k)的起始頁框的頁描述符;指向鏈表中相鄰元素的指針存放在頁描述符page的lru字段中。

 

除了鏈表頭外,free_area數組的第k個元素同樣包含字段nr_free,它指定了大小爲2k頁的空閒塊的個數。當然,如果沒有大小爲2k 的空閒頁框塊,則nr_free等於0且free_list爲空(free_list的兩個指針next和prev都指向它自己的free_list字段)。

 

最後,一個2k的空閒頁塊的第一個頁的描述符的private字段存放了塊的order,也就是數字k。正是由於這個字段,當頁塊被釋放時,內核可以確定這個塊的夥伴是否也空閒。如果是的話,它可以把兩個塊結合成大小爲2k+1頁的單一塊。

 

2 塊分配

 

內核使用__rmqueue()函數來在管理區中找到一個空閒塊。該函數需要兩個參數:管理區描述符的地址zone和order,order表示請求的空閒頁塊大小的對數值(0 表示一個單頁塊,1 表示一個兩頁塊,2表示四個頁塊)。如果頁框被成功分配,__rmqueue()函數就返回第一個被分配頁框的頁描述符。否則,函數返回NULL。

 

在__rmqueue()函數中,從所請求order的鏈表開始,它掃描每個可用塊鏈表進行循環搜索,如果需要搜索更大的order,就繼續搜索:
struct free_area *area;
unsigned int current_order;

for (current_order=order; current_order<11; ++current_order) {
    area = zone->free_area + current_order;
    if (!list_empty(&area->free_list))
        goto block_found;
}
return NULL;

 

如果直到循環結束還沒有找到合適的空閒塊,那麼__rmqueue()就返回NULL。否則,找到了一個合適的空閒塊,在這種情況下,從鏈表中刪除它的第一個頁框描述符,並減少管理區描述符中的free_pages的值:
block_found:
    page = list_entry(area->free_list.next, struct page, lru);
    list_del(&page->lru);
    ClearPagePrivate(page);
    page->private = 0;
    area->nr_free--;
    zone->free_pages -= 1UL << order;

 

如果從curr_order鏈表中找到的塊大於請求的order,就執行一個while循環。這幾行代碼蘊含的原理如下:當爲了滿足2h 個頁框的請求而有必要使用2k個頁框的塊時(h < k),程序就分配前面的2h 個頁框,而把後面2k - 2h 個頁框循環再分配給free_area鏈表中下標在h到k之間的元素:
    size = 1 << curr_order;
    while (curr_order > order) {
        area--;
        curr_order--;
        size >>= 1;
        buddy = page + size;
        /* insert buddy as first element in the list */
        list_add(&buddy->lru, &area->free_list);
        area->nr_free++;
        buddy->private = curr_order;
        SetPagePrivate(buddy);
    }
    return page;

 

因爲__rmqueue()函數已經找到了合適的空閒塊,所以它返回所分配的第一個頁框對應的頁描述符的地址page。

 

3 塊釋放

 

__free_pages_bulk()函數按照夥伴系統的策略釋放頁框。它使用3個基本輸入參數:
page:被釋放塊中所包含的第一個頁框描述符的地址。
zone:管理區描述符的地址。
order:塊大小的對數。

 

__free_pages_bulk()首先聲明和初始化一些局部變量:
struct page * base = zone->zone_mem_map;
unsigned long buddy_idx, page_idx = page - base;
struct page * buddy, * coalesced;
int order_size = 1 << order;

 

page_idx局部變量包含塊中第一個頁框的下標,這是相對於管理區中的第一個頁框而言的。order_size 局部變量用於增加管理區中空閒頁框的計數器:
zone->free_pages += order_size;

 

現在函數開始執行循環,最多循環 (10-order) 次,每次都儘量把一個塊和它的夥伴進行合併。函數以最小的塊開始,然後向上移動到頂部:
while (order < 10) {
    buddy_idx = page_idx ^ (1 << order);
    buddy = base + buddy_idx;
    if (!page_is_buddy(buddy, order))
        break;
    list_del(&buddy->lru);
    zone->free_area[order].nr_free--;
    ClearPagePrivate(buddy);
    buddy->private = 0;
    page_idx &= buddy_idx;   /* 合併 */
    order++;
}

 

這個循環我看了半天沒有看懂,後來舉個例子,再畫個圖才漸漸明白。比如,我們這裏order是4,那麼order_size的值爲24,也就是16,表明要釋放16個連續的page。page_idx爲這個連續16個page的老大的mem_map數組的下標。進入循環後,函數首先尋找該塊的夥伴,即mem_map數組中page_idx-16或page_idx+16的下標buddy_idx,進一步說明一下,就是爲了在下標爲16的free_area中找到一個空閒的塊,並且這個塊與page所帶的那個擁有16個page的塊相鄰。

 

尤其要注意:buddy_idx = page_idx ^ (1 << order)這行代碼。這行代碼很巧妙,短小精幹。因爲order一來就等於4,所以循環從4開始的,即第一個循環爲buddy_idx = page_idx ^ (1<<4),即buddy_idx = page_idx ^ 10000。如果page_idx第5位爲1,比如是20號頁框(10100),那麼在異或以後,buddy_idx爲4號頁框(00100)。如果page_idx第5位爲0,比如是第40號頁框(101000),那麼在異或以後,buddy_idx爲56號頁框(111000)。

 

爲什麼要做這麼一個運算呢?想想我們的目的是什麼。__free_pages_bulk是將以其參數page爲首的2^order個頁面找到一個夥伴,並與其合併。在mem_map數組中,這個夥伴的老大要麼是在這個page的前2^order,要麼就是後2^order。如果單單是加或者減,那麼就會忽略前面的或者後面的夥伴。大家不妨對照上圖好好的琢磨一下。至於爲啥不既加又減呢,我估計Linux的開發者們沒這麼做是因爲性能的問題吧,各個資料上也說了這裏主要是儘量合併而已,我們就不去管他了。

 

找到夥伴以後,把該夥伴的老大page的地址賦給buddy:
buddy = base + buddy_idx;

 

 

現在函數調用page_is_buddy()來檢查buddy是否是真正的值得信賴的夥伴,也就是大小爲order_size的空閒頁框塊的第一個頁。
int page_is_buddy(struct page *page, int order)
{
    if (PagePrivate(buddy) && page->private == order &&
          !PageReserved(buddy) && page_count(page) ==0)
        return 1;
    return 0;
}

 

正如所見,要想成爲夥伴,必須滿足以下四個條件:

(1)buddy的第一個頁必須爲空閒(_count字段等於-1);

(2)它必須屬於動態內存(PG_reserved 位清零);

(3)它的private 字段必須有意義(PG_private 位置位);

(4)它的private字段必須存放將要被釋放的塊的order。

 

如果所有這些條件都符合,就說明有新的夥伴存在啦,那麼夥伴塊就要跟我page結合,先必須得脫離原來的free_list,執行page_idx &= buddy_idx合併(注意,這行代碼與前邊的buddy_idx = page_idx ^ (1 << order)是緊密結合的),並再執行一次循環以尋找兩倍大小的夥伴塊。

 

如果page_is_buddy()中至少有一個條件沒有被滿足,則該函數跳出循環,因爲獲得的空閒塊不能再和其他空閒塊合併。函數將它插入適當的鏈表並以塊大小的order 更新第一個頁框的private 字段。
coalesced = base + page_idx;
coalesced->private = order;
SetPagePrivate(coalesced);
list_add(&coalesced->lru, &zone->free_area[order].free_list);
zone->free_area[order].nr_free++;


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章