夥伴系統演算法
為什麼使用夥伴系統演算法?
在系統的使用過程中,頻繁的請求和釋放頁框,勢必會導致已分配的頁框之間分散存在了許多小塊的空閒頁框。由次帶來的問題是,即使有足夠的空葉匡可以滿足請求,但要分配一個大塊的連續頁框就可能無法滿足。
本質上說,避免外碎片由兩種方法
- 利用分頁單元把一組非連續空閒頁框對映到連續的線性地址上
- 開發一種適當的技術來記錄現存的空閒頁框的分配情況,以儘量避免為滿足對小塊的請求二分割大的空閒塊。
基於以下原因,linux核心首選第二種情況:
- 在某些情況下,如DMA。僅僅是連續頁框並不滿足請求。MDA在一次單獨的IO操作傳送磁碟資料時,DMA忽略分頁單元而直接訪問地址匯流排。因此,所請求的緩衝區必須是一段連續的頁框
- 連續頁框分配在保持核心頁表不變方面所起到的作用是不容小覷的。頻繁的訪問頁表勢必導致平均訪問記憶體的次數增加,這會使CPU頻繁的重新整理TBL的內容
- 核心透過4Mb的頁可以訪問到大塊的連續實體記憶體。這樣就減小了TBL的失效率,因此提高了訪問記憶體的平均速度
Linux採用著名的夥伴系統(buddy ststem)演算法來解決外碎片問題。把所有的空閒頁框分組為11和連結串列塊,每個連結串列塊分別包含1,2,4,5,16,32,62,128,256,512,和1024
和連續的頁框。對1024個頁框的最大請求對應著4MB大小的來連續RAM塊。每個塊的第一個實體地址是這個塊大小的整數倍。例如,大小為16個頁框的塊,其起始地址是 16*12^2
的倍數。
linux將 大小為b的一對空間合併為一個大小為 2b的單獨塊。滿足以下條件的稱為 夥伴
- 兩個塊具有相同的大小, 記作b
- 他們的實體地址是連續的
- 第一塊的第一個頁框的實體地址是 2 * b * 12^12的倍數
資料結構
linux 2.6為每個記憶體管理區都使用了不同的夥伴系統。 ZONE_DMA ZONE_NORMAL ZONE_HIGH
夥伴系統使用的主要資料結構如下:
- linux將實體記憶體使用
mem_map
來管理。實際上 每個管理區都對應著mem_map
的子集。子集中的第一個元素和元素大小由管理區資料結構的zone_mem_map
和size
欄位指定 - 包含11個元素 (struct free_are; free_area[MAX_ORDER]; MAX_ORDER == 11), 每個元素代表一種塊的大小。該陣列存放在管理區描述符的free_area欄位中
struct zone { // 記憶體管理區資料結構 省略了一部分欄位
unsigned long free_pages;
unsigned long pages_min, pages_low, pages_high;
spinlock_t lock;
struct free_area free_area[MAX_ORDER];
struct page *zone_mem_map;
}
struct free_area {
struct list_head free_list; // 空閒塊連結串列
unsigned long nr_free; // 空閒塊數目
};
struct page {
page_flags_t flags;
atomic_t _count;
atomic_t _mapcount;
unsigned long private;
struct address_space *mapping;
pgoff_t index;
struct list_head lru; // page 的 lru 成員 連結串列本體
};
free_area 中的 struct list_head 儲存了第一個空閒塊首個page的 雙向連結串列欄位(lru)。這些 order相同的空閒塊使用 lru 串聯起來。
nr_free 指定了大小為 order 的空閒塊的個數。如果沒有空閒塊, nr_free == 0, 且 free_list 的前後指標指向自己
另外,一個 2^k 的空閒塊的第一個 page
的描述符的 private
欄位 (也就是free_list 連結串列中的每個 page 所代表 的 page 的private欄位
)儲存了 order
的值,透過這個值,核心可以確定這個空閒塊的夥伴是否也空閒
分配塊
核心使用 __rmqueue
函式來在管理區中找到一個空閒塊。該函式需要兩個引數。zone
記憶體管理區描述符, order
空閒區的大小
/*
* Do the hard work of removing an element from the buddy allocator.
執行從夥伴系統移除元素是一個艱鉅的任務
* Call me with the zone->lock already held.
在 以獲取 zone->lock 鎖後呼叫我
*/
static struct page *__rmqueue(struct zone *zone, unsigned int order)
{
struct free_area * area;
unsigned int current_order;
struct page *page;
for (current_order = order; current_order < MAX_ORDER; ++current_order) {
// 找到從管理區的空閒區域 使用 zone 和 order定位要申請的區域和塊的大小
area = zone->free_area + current_order;
// 查詢有無空閒塊
if (list_empty(&area->free_list))
// 如果沒有則 向 更大的 order + 1 繼續查詢
continue;
// 透過結構體中的成員獲取結構體指標 核心經典操作
// 這裡是使用 struct page 的成員的指標 area->free_list.next
// free_list.next 是 記憶體管理區 zone的空閒區域的第一個 page 連結串列
// 和 lru 這個成員的名字來獲取這個結構體
page = list_entry(area->free_list.next, struct page, lru);
// 從 連結串列中刪除這個頁
list_del(&page->lru);
// 移除頁的 private 的 order 標誌
rmv_page_order(page);
// 值為 order 的空閒區塊的值 減 1
area->nr_free--;
// 記憶體管理區的空閒 頁表 - 2^order 個
zone->free_pages -= 1UL << order;
// 分配
return expand(zone, page, order, current_order, area);
}
return NULL;
}
static inline struct page *
expand(struct zone *zone, struct page *page,
int low, int high, struct free_area *area)
{
unsigned long size = 1 << high;
// 如果找到的 order 比 提交的申請的 order大,則需要將夥伴拆分
// 把後後面 2^h - 2^l 個頁框迴圈分配給free_area連結串列中下標在 h 到 k之間的元素
// (2^h - 2^l) - 2^(h*-1) 個人總結公式 h* 為初始值為 h 的變數
while (high > low) {
// 將指標向前移動 struct free_are free_area[MAX_ORDER];
// 第一次的area指向的order = high 的free_area
// 相當於area指向了 order - 1 的free_area
area--;
high--;
size >>= 1; // 剩餘的大小
// BUG_ON(bad_range(zone, &page[size]));
list_add(&page[size].lru, &area->free_list);
area->nr_free++;
set_page_order(&page[size], high);
}
return page;
}
list_entry函式展開
/**
* list_entry - get the struct for this entry
* @ptr: the &struct list_head pointer.
* @type: the type of the struct this is embedded in.
* @member: the name of the list_struct within the struct.
*/
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
/**
* @ptr: the pointer to the member.
* @type: the type of the container struct this is embedded in.
* @member: the name of the member within the struct.
*/
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
list_del 展開
static inline void list_del(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
// 將被解開的節點的前後指標指向核心中的"空",未被引用的標記
entry->next = LIST_POISON1;
entry->prev = LIST_POISON2;
}
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
// 將當前節點的前後節點互相指引,這樣就是刪除了當前節點
next->prev = prev;
prev->next = next;
}
釋放塊
__free_pages_bulk函式按照夥伴系統的策略釋放頁框。它使用3個基本輸入引數。
page
被釋放的塊中第一個頁面描述符的地址
zone
管理區描述符
order
被釋放的塊的大小
static inline void __free_pages_bulk (struct page *page, struct page *base,
struct zone *zone, unsigned int order)
{
unsigned long page_idx;
struct page *coalesced;
int order_size = 1 << order;
if (unlikely(order))
destroy_compound_page(page, order);
page_idx = page - base;
BUG_ON(page_idx & (order_size - 1));
BUG_ON(bad_range(zone, page));
zone->free_pages += order_size;
while (order < MAX_ORDER-1) {
struct free_area *area;
struct page *buddy;
int buddy_idx;
buddy_idx = (page_idx ^ (1 << order));
buddy = base + buddy_idx;
if (bad_range(zone, buddy))
break;
if (!page_is_buddy(buddy, order))
break;
/* Move the buddy up one level. */
list_del(&buddy->lru);
area = zone->free_area + order;
area->nr_free--;
rmv_page_order(buddy);
page_idx &= buddy_idx;
order++;
}
coalesced = base + page_idx;
set_page_order(coalesced, order);
list_add(&coalesced->lru, &zone->free_area[order].free_list);
zone->free_area[order].nr_free++;
}
``
```c
int order_size = 1 << order;
zone->free_pages += order_size;
order_size為計算出的當前頁塊的數目
然後加入 zone->free_pages 頁數目計數
然後是while迴圈,迴圈最多進行 MAX_ORDER-1 - order 次
為什麼是 MAX_ORDER-1 呢,因為 order最大下標為 MAX_order - 1
struct page *buddy;
int buddy_idx;
buddy_idx = (page_idx ^ (1 << order));
buddy = base + buddy_idx;
在while迴圈中尋找當前頁框塊的夥伴頁框塊 buddy_idx
, 夥伴塊的下標可以使用位運算簡單運算
(page_idx ^ (1 << order)) 將頁框塊的下標的第 order 位取反,相當於page_index + (1 << order) 或者 page_index - (1 << order)
一旦知道了夥伴塊的下標,就可以根據基址找到夥伴塊的下標
if (bad_range(zone, buddy))
break;
if (!page_is_buddy(buddy, order))
break;
/* Move the buddy up one level. */
list_del(&buddy->lru);
area = zone->free_area + order;
area->nr_free--;
rmv_page_order(buddy);
page_idx &= buddy_idx;
order++;
接下來是檢查該夥伴塊是否合法,如果該夥伴塊合法,則將這個夥伴塊從當前free_area中刪除,相當於將當前塊和夥伴塊合併
然後想上一級order + 1 free_area繼續尋找
static inline int page_is_buddy(struct page *page, int order)
{
if (PagePrivate(page) &&
(page_order(page) == order) &&
!PageReserved(page) &&
page_count(page) == 0)
return 1;
return 0;
}
檢查一個夥伴塊是否合法,首先檢查page的Private欄位是否合法,然後檢查Private欄位的值是否為oroder再然後檢查頁框的屬性是否為可移動記憶體,最後他的 page的_count欄位必須為-1 page_count在頁空閒時返回 0。
以上條件均滿足後,就可以釋放夥伴塊,繼續向上嘗試合併更大的塊
coalesced = base + page_idx;
set_page_order(coalesced, order);
list_add(&coalesced->lru, &zone->free_area[order].free_list);
zone->free_area[order].nr_free++;
最終跳出while迴圈就開始將塊合併進入對應order的free_area中