linux 頁框管理(二) 夥伴系統演算法

kuraxii發表於2024-04-14

夥伴系統演算法

為什麼使用夥伴系統演算法?

在系統的使用過程中,頻繁的請求和釋放頁框,勢必會導致已分配的頁框之間分散存在了許多小塊的空閒頁框。由次帶來的問題是,即使有足夠的空葉匡可以滿足請求,但要分配一個大塊的連續頁框就可能無法滿足。

本質上說,避免外碎片由兩種方法

  • 利用分頁單元把一組非連續空閒頁框對映到連續的線性地址上
  • 開發一種適當的技術來記錄現存的空閒頁框的分配情況,以儘量避免為滿足對小塊的請求二分割大的空閒塊。

基於以下原因,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_mapsize 欄位指定
  • 包含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中

相關文章