Linux內存管理第六章 -- Physical Page Allocation(Buddy Allocator)

Linux內存管理第六章 – Physical Page Allocation

本章將描述在Linux中物理內存如何分配和管理。其主要算法是Binary Buddy Allocator。Buddy Allocator相比較其他分配器要快很多。

這是一種將普通pow-of-two分配器與空閒buffer塊相結合的分配方案。其背後的基本概念實際上相當簡單。memory被分成若干個page組成的大塊,其中每個大塊中page的個數是2的x次冪。如果一個大塊中可用的memory size小於期望分配的size,那麼一個更大的塊將會對半切分,切分後的兩個子塊相互爲buddies。其中一個子塊用於分配,另一個是空閒的。這些塊在必要時連續減半,直到達到所需大小的塊可用爲止。當一個block被釋放後,會檢查它的buddy是否也是空閒的,如果是,那麼這對buddies將會合並。

本章將從描述Linux是如何記住哪些塊是free的開始。然後再來討論分配和釋放pages的方法。再後續的小節會將到影響allocator分配行爲的flags,最後來討論下碎片問題以及allocator如何處理碎片問題。

Managing Free Blocks

如上所述,buddy allocator維護了一組2的x此冪個page的free page block。其中指數x被稱之爲order。一個struct free_area的數組中保存了每個order所屬於的free page 鏈表。
其示意圖如下:
buddy
因此,數組中的第0個元素將指一個free page block的鏈表,其中每個block由20 free page組成,第1個元素中的每個block由21 free pages組成,直到2^MAX_ORDER -1^ pages.當前MAX_ORDER被定義爲10. 這就消除了當小塊能夠滿足分配需求的時候而切分大塊的機會。page blocks通過page->list連成一個線性鏈表被維護起來。

struct zone {
		.............
	/*
	 * free areas of different sizes
	 */
	struct free_area	free_area[MAX_ORDER];
	..............
}
struct free_area {
	struct list_head	free_list;
	unsigned long		*map;
};
  • free_list: free page block鏈表的表頭
  • map:代表一對buddies的狀態的bitmap

Linux爲了節省memory僅僅使用一個bit而不是兩個bit來表示每隊buddies的狀態。每當一個buddy被分配或者被釋放,代表該對buddies的bit位將被反轉,因此一個bit爲0時,表示兩個buddy都空閒或者都在使用中,bit爲1時,表示兩個buddy中只有一個在使用中。爲了能正確反轉bit,定義了宏函數MARK_USED(),其定義如下:

#define MARK_USED(index, order, area) \
	__change_bit((index) >> (1+(order)), (area)->map)

其中index表示一個page在全局數組mem_map中的索引。將index右移1+order位後,所得的值就是map中代表該page所屬的buddies的bit。

Buddy Alocator初始化源碼分析

從前面一片博客boot memory中可以知道,當boot memory retire的時候,會把pages釋放給buddy allocator。其實這個釋放的過程就是buddy allocator的free_area數組中各個order中的free_area[order]->free_list的添加page的過程。
下面來分析下核心算法代碼:free_all_bootmem_core() --> __free_pages() or __free_page() --> … -->__free_pages_bulk()
buddy
結合上圖,假設調用__free_pages_bulk 來釋放page2,則此時的order =0

static inline void __free_pages_bulk (struct page *page, struct page *base,
		struct zone *zone, struct free_area *area, unsigned int order)
{
	unsigned long page_idx, index, mask;

	if (order)
		destroy_compound_page(page, order);
	mask = (~0UL) << order;//如果order爲0則mask =1111 1111,order = 1 則mask = 1111 1110 ......
	page_idx = page - base;//傳入page在zone->zone_mem_map中的偏移位置,假設爲2
	if (page_idx & ~mask) //檢查傳入的page 是否與傳入的order匹配
		BUG();            //如果page_idx = 2,order = 0,則mask = 1111 1111,~mask = 0,page_idx & ~mask = 0,不會報錯
	index = page_idx >> (1 + order);//index = 2 >> (1 + 0) = 1  0010 >> 1 = 0001

	zone->free_pages += 1 << order;//free_pages 加上1,因爲只釋放page2
	while (order < MAX_ORDER-1) {
		struct page *buddy1, *buddy2;

		BUG_ON(area >= zone->free_area + MAX_ORDER);
		if (!__test_and_change_bit(index, area->map)) //此時bit 1應該爲1,翻轉後則爲0,所以不會break
			/* the buddy page is still allocated.*/   //*** 當再次循環時,order =1, index = 0,
			                                          //*** 此時因爲buddyA:page0 ~ page1在使用,所以會跳出循環
			break; 
		/* Move the buddy up one level. */
		buddy1 = base + (page_idx ^ (1 << order));//2 ^ (1 << 0) = 0010 ^ 0001 = 0011.buddy1 = page3   
		buddy2 = base + page_idx;                 //buddy2 = page2
		BUG_ON(bad_range(zone, buddy1));
		BUG_ON(bad_range(zone, buddy2));
		list_del(&buddy1->lru);//因爲page2和page3要被合併到order1中,所以要將page3從free_area[0]->free_list中刪除
		mask <<= 1;// 1111 1111 << 1 = 1111 1110
		order++;// 0 + 1 = 1
		area++;//area = free_area[1]
		index >>= 1;//index = 1 >> 1 = 0,
		            //也就是order1中buddyA:page0 ~ page1,buddyB:page2 ~ page3 在free_area[1]->map中的bit index是0
		page_idx &= mask;// 0000 0010 & 1111 1110 = 2
	}
	list_add(&(base + page_idx)->lru, &area->free_list);//跳出循環後,將page2加入到free_area[1]->free_list中
}

Allocating Pages

Linux提供了相當多的API來進行分配物理頁框。這些API都帶一個gfp_mask的參數,這些參數將決定了buddy allocator的行爲。這些gfp_mask的具體含義在後續章節中會詳細描述。
所有的這些分配API最終都會調用到核心函數__alloc_pages().但這些API存在的意義是可以提前選擇正確的node和正確的zone。不同的使用者要求從不同的zone中獲取memory,比如相當多的device driver要從ZONE_DMA中獲取memory,而disk buffers需要從ZONE_NORMAL中獲取memory,但調用者不必關心當前正在使用的是哪一個node。下面來看看這些API的說明:

struct page * alloc_page(unsigned int gfp_mask)
分配單個page,並返回struct page的地址
struct page * alloc_pages(unsigned int gfp_mask, unsigned int order)
分配2order個page,並返回大頭struct page的地址
unsigned long get_free_page(unsigned int gfp_mask)
分配單個page,並初始化爲0,然後再返回對應的虛擬地址
unsigned long __get_free_page(unsigned int gfp_mask)
分配單個page並返回虛擬地址
unsigned long __get_free_pages(unsigned int gfp_mask, unsigned int order)
分配2order個page並返回虛擬地址
struct page * __get_dma_pages(unsigned int gfp_mask, unsigned int order)
從DMA zone中分配2order個page並返回struct page的地址

在分配時總是需要指定order,如果是0則代表申請一個page。如果當指定的order中找不到一個空閒的block,那麼更高一級的order中的block將被切分爲兩個buddies。其中一個被擁擠分配,而另外一個被放置到low order的free list中。下圖中展示了當一個24 block被切分後的buddies如何被加入到低級別order的free list中,直到拿到與申請size相比比較合適的block爲止。
buddies
當一個block被釋放後,它的buddy將會被檢查。如果兩個buddy都是free的,則它們要被合併到高階的block中,並放置在高階的free list中。如果buddy不是free的,被釋放的block將被加入到當前order的free list中。在操作這些鏈表的過程中,中斷時被禁止的從而來阻止這種case:中斷處理函數正在處理list,而某個進程此時拿到的狀態還沒有同步過來。這個是通過使用中斷安全的自選鎖實現的。
第二個需要做的決定是選擇哪一個node。Linux使用使用node-local分配策略,即當前CPU中正在運行的進程需要分配page,當前進程在的代碼在哪個node中,動態獲取page就在哪個node中分配。因此_alloc_pages()是很重要的一個函數因爲該函數會根據內核被編譯成UMA還是NUMA的不同而不同。
無論是使用上述哪一個API來進行分配,__alloc_pages()是buddy allocator的核心。該函數從來不會被直接調用,該函數會檢查被選中的zone並檢查zone中是否又合適的空閒內存塊使用。如果被指定的Zone不合適,分配器會轉移到其他zone中嘗試分配。分配器轉移zone的順序是在內核啓動階段build_zonelist()函數決定的,但通常的一個轉移順序是ZONE_HIGHMEM->ZONE_NORMAL->ZONE_DMA。如果zone中空閒page的數量達到page_low水位線,系統將喚醒kswapd守護進程開始在zones中釋放一些pages,如果內存很緊張的情況下,調用者會直接來釋放pages就像kswapd做的事情一樣。
一旦被分配的zone被確定後,函數rmqueue()將會被調用,用來分配pages或者如果沒有合適的size,就將高階的塊進行切分。
2222

Buddy Allocator分配過程源碼分析

bd
依據上圖狀態1,在通過調用alloc_pages(),其中order = 2

static struct page *__rmqueue(struct zone *zone, unsigned int order)
{
	struct free_area * area;
	unsigned int current_order;
	struct page *page;
	unsigned int index;

	for (current_order = order; current_order < MAX_ORDER; ++current_order) {
		area = zone->free_area + current_order;
		if (list_empty(&area->free_list)) //由於在狀態1時,order2中無空閒的block,此時會跳到order3中去找空閒的blocks
			continue;

		page = list_entry(area->free_list.next, struct page, lru);//從zone->free_area[3]->free_list中取出page8
		list_del(&page->lru);//從zone->free_area[3]->free_list刪除page8
		index = page - zone->zone_mem_map;//index = 8
		if (current_order != MAX_ORDER-1)
			MARK_USED(index, current_order, area);//將從zone->free_area[3]->map中bit 0翻轉,此時應該爲0,表示
			                                      // buddy1:page0 ~page7,buddy2:page8 ~ page15,兩個buddy都在使用
		zone->free_pages -= 1UL << order;//將free_pages 減去8
		return expand(zone, page, index, order, current_order, area);//傳入的page = page8,index = 8,order=2, current_order = 3,
		                                                             //area = zone->free_area[3]
	}

	return NULL;
}

下面再來看expand()函數:

static inline struct page *
expand(struct zone *zone, struct page *page,
	 unsigned long index, int low, int high, struct free_area *area)
{
	unsigned long size = 1 << high; // 1 << 3 = 8
    // low = 2, high = 3
	while (high > low) { //條件成立
		area--;//進入到order2
		high--;//high 變爲2
		size >>= 1;// 8 >> 1 = 4
		BUG_ON(bad_range(zone, &page[size]));
		list_add(&page[size].lru, &area->free_list);//page = page8, 所以page[4]就是page12,
		                                            //將page12加入到zone->free_area[2]-》free_list
		MARK_USED(index + size, high, area);//將buddy:page12 ~ page15在zone->free_area[2]->map中的bit 1翻轉
	}
	return page;//返回page8
}

Free Pages

釋放pages的API要簡單很多,但其必須要記住要釋放的block所在的order。這也是buddy allocator的一個缺點:調用者必須記住原始分配時的page的個數。其具體的API如下:

void __free_pages(struct page *page, unsigned int order)
從page開始釋放2order個page
void __free_page(struct page *page)
釋放order 0中的單個page
void free_page(void *addr)
釋放一個虛擬地址對應的page

Buddy Alocator釋放page源碼分析

上述這些函數最終都會調用到__free_pages_bulk(),其原理參見本章小節:Buddy Alocator初始化源碼分析

Get Free Page (GFP) Flags

一個貫穿整個VM的永久的概念是GFP標誌。這些標誌決定了allocator和kswapd的分配和釋放page的行爲。舉個例子:一箇中斷處理函數可能不允許被中斷因此中斷處理函數中分配內存就不需要__GFP_WAIT標誌集,這些標誌表示當前調用者可以進入睡眠。有三組GFP標誌,全部定義在<linux/mm.h>中。

  • 第一組標誌是zone modifiers,這些標誌表示調用者必須儘量從指定的zone分配memory.此時有人會問怎麼沒有ZONE_NORMAL的zone modifier?因爲這些標誌是一個數組中的偏移量。0默認就代表從ZONE_NORMAL分配內存。
Flag Description
__GFP_DMA 如果可能,儘量從ZONE_DMA分配內存
__GFP_HIGHMEM 如果可能,儘量從ZONE_HIGHMEM分配內存
GFP_DMA __GFP_DMA的別名
  • 第二組標誌是action modifier,這些標誌可以改變VM和調用進程的行爲。這些標誌位屬於low level標誌位,由於太過原始從而不容易使用。
Flag Description
__GFP_WAIT 表示調用者不是高優先級所以可以進入睡眠或者重新調度
__GFP_HIGH 被高優先級進程使用或者內核進程使用
__GFP_IO 表示調用者可以操作low level IO
__GFP_HIGHIO 決定了IO可以在被映射到高端內存的page上
__GFP_FS 表示調用者可以調用文件系統層,當調用者與文件系統相關則可以使用,舉例來說buffer cache,可以用這個標記來避免自己調用自己
__GFP_NOFAIL 如果使用該flag表示此次分配不允許分配器分配失敗,分配器應該無限地嘗試繼續分配
__GFP_REPEAT 當分配失敗後應該重複進行分配。它和__GFP_NOFAIL的區別是執行再次分配的決定比__GFP_NOFAIL稍微遲一些
__GFP_NORETRY 作用和__GFP_NOFAIL完全相反,如果失敗了就立即返回
  • 第三組high level flag。由於low level flag不好用,很難知道在某個場景使用哪些正確的low level flag組合。因此定義了一些由low level flag的組合而成的high level flag。
Flag Low Level Flag Combination
GFP_ATOMIC __GFP_HIGH
GFP_NOIO __GFP_HIGH | __GFP_WAIT
GFP_NOHIGHIO __GFP_HIGH | __GFP_WAIT | _GFP IO
GFP_NOFS __GFP_HIGH | __GFP_WAIT | __GFP_IO | __GFP_HIGHIO
GFP_KERNEL __GFP_HIGH | __GFP_WAIT | __GFP_IO | __GFP_HIGHIO | __GFP_FS
GFP_NFS __GFP_HIGH | __GFP_WAIT | __GFP_IO | __GFP_HIGHIO | __GFP_FS
GFP_USER __GFP_WAIT | __GFP_IO |_GFP HIGHIO | __GFP_FS
GFP_HIGHUSER __GFP_WAIT | __GFP_IO | __GFP_HIGHIO | __GFP_FS | __GFP_HIGHMEM
GFP_KSWAPD __GFP_WAIT| __GFP_IO |_GFP HIGHIO |__GFP_FS
Flag Description
GFP_AUOMIC 調用者無論何時都不能進入睡眠並且必須盡所有可能爲其服務。任何中斷處理函數中申請memory必須使用這個flag來避免睡眠或者操作IO。許多子系統在初始化的時候會用到這個flag如:buffer_init(),inode_init()
GFP_NOIO 當調用者已經在一個正在進行的IO相關操作中時,再分配memory就需要使用該flag。舉例:當一個loop back device正在嘗試從一個buffer頭部獲取一個page,它使用這個flag將會保證不會執行一些導致更多IO的操作。事實上,該flag盡用於loop back device中避免死鎖
GFP_NOHIGHIO 當在high memory中爲IO創建一個bounce buffer,該flag僅僅被用在alloc_bounce_page()中
GFP_NOFS 該flag僅僅被buffer cache和文件系統使用來保證他們自己不會遞歸調用他們自己
GFP_KERNEL 這是一個最常用的flag。它表示調用者可以自由的幹它想幹的事情。嚴格來講該flag與GFP_USER不同的地方在於該flag可以使用緊急池中的page,但在kernel2.4中已經沒有緊急池
GFP_USER 具有歷史意義的flag。在kernel2.2x中,以此分配被給定爲LOW,MEDIUM,HIGH三個優先級。如果內存緊張,使用GFP_USER的調用者優先級是LOW,此時內存申請會失敗同時其他優先級的申請還可以繼續進行。但在kernel2.4中已經不再重要了,它和GFP_KERNEL沒什麼區別
GFP_HIGHUSER 該標誌表示分配器應該儘可能從ONE_HIGHMEM中分配內存。當代表用戶空間的進程分配內存時使用這個flag
GFP_NFS 該flag已經失效
GFP_KSWAPD 該flag具有歷史意義,但現在已經可GFP_KERNEL沒有什麼區別了
  • Process Flags:一個進程在進程描述符task_struct中設置的一些flag也可能會影響buddy allocator的行爲。這些flag定義在<linux/sched.h>中。下面來介紹下回影響VM的幾個flag:
Flag Description
PF_MEMALLOC 該標誌表示當前進程自己就是一個memory allocator。kswapd進程就設置了這個flag即將被OOM killer殺掉的進程將會設置這個flag,它告訴buddy allocator忽略zone的水位線儘可能分配內存
PF_MEMDIE OOM killer給要殺死的進程設置該flag,其含義和PF_MEMALLOC差不多告訴buddy allocator儘量分配內存因爲該進程馬上要死了
PF_FREE_PAGES 當buddy allocator調用try_to_free_pages()時表示這些free pages應該預留給當前調用的進程而不是返回給free lists中

Avoiding Fragmentation

任何分配器都必須要解決的一個重要問題是內部碎片和外部碎片。

  • 外部碎片:是指內存中有很多個不連續的小塊,當需要分配連續的大塊的時候,沒有可用的大塊。一般是指整個page的不連續。
  • 內部碎片:內部碎片被定義爲浪費的空間,其中必須分配一個大塊來服務一個小請求。

在Linux中,外部隨便不是一個很嚴重的問題因爲大塊請求連續的物理內存的概率很小,Linux中經常使用vmalloc()來服務大塊請求,返回的是不連續的物理page。
而內部碎片對於binary buddy system來說是一個很嚴重的問題。因爲buddy allocator每次分配必須是2order個page同時分配,這樣就會造成一些浪費。Linux爲了解決這個問題引入了slab allocator將pages切割成細小的內存塊用於分配。組合使用buddy allocator和slab allocator,內核可以確保由於內部碎片而浪費的內存量保持在最低限度。

Per-CPU Page Lists

在kernel2.6中,pages通過buffered_rmqueue()函數從struct per_cpu_pageset中分配。如果low水位(per_cpu_pageset->low)沒有到達,page將從此pageset中分配而無需獲取spinlock。一旦low水位到達,大量的page將從buddy allocator中轉移到per-cpu list中此次分配過程是需要獲取spinlock的。然後再將per-cpu中的一個page返回給調用者。
儘管high order分配機率很小,但它還是要獲取中斷安全的spinlock因此在切分和合並page的時候無延時。而0 order的分配的切割將被延時到per-cpu list的低水位到達時才執行,而0order的合併將延時到高水位到達時才執行。
加入per-cpu list的效果是很明顯的。因爲獲取自旋鎖來保護buddy lists的次數降低了。而在Linux中大塊memory申請又很少所以此次優化是針對常用的case。該改動在多核機器上效果明顯而在單核機器上效果不是很明顯。
per-cpu也存在一些問題但這些問題都不認爲是很嚴重的問題:

  • 第一個問題,如果per-cpu list中的pages可以被合併長連續page大塊,那麼high order allocation可能會失敗,因爲buddy allocator中沒有更多的大塊內存了。
  • 第二個問題,當內存很緊張的時候且當前CPU的per-cpu list空了而其他CPU list卻是滿的,當前CPU中的0 order allocation也會fail,因爲當前沒有機制從別的CPU per-cpu list中回收內存。
  • 第三個潛在的問題,新釋放page的buddy在別的CPU list中,這也會導致high order allocation失敗。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章