1. 程式人生 > >記憶體管理六 夥伴系統管理記憶體

記憶體管理六 夥伴系統管理記憶體

一、夥伴系統概序
1、夥伴演算法的原理
  為了便於頁面的維護,核心將多個頁面組成記憶體塊,每個記憶體塊都有 2^order個頁(page)。order相同的記憶體塊被組織到一個空閒連結串列中。夥伴系統基於2的方冪來申請釋放記憶體頁。
  當申請記憶體頁時,夥伴系統首先檢查與申請大小相同的記憶體塊連結串列中,檢看是否有空閒頁,如果有就將其分配出去,並將其從連結串列中刪除,否則就檢查上一級,即大小為申請大小的2倍的記憶體塊空閒連結串列,如果該連結串列有空閒記憶體,就將其分配出去,同時將剩餘的一部分(即未分配出去的一半)加入到下一級空閒連結串列中;如果這一級仍沒有空閒記憶體;就檢查它的上一級,依次類推,直到分配成功或者徹底失敗,在成功時還要按照夥伴系統的要求,將未分配的記憶體塊進行劃分並加入到相應的空閒記憶體塊連結串列。
  在釋放記憶體頁時,會檢查其夥伴是否也是空閒的,如果是就將它和它的夥伴合併為更大的空閒記憶體塊,該檢查會遞迴進行,直到發現夥伴正在被使用或者已經合併成了最大的記憶體塊。

2、夥伴系統相關的結構
  核心中每個節點node,分為不同的zone來管理記憶體,每一個zone中的記憶體,包含不同大小的記憶體塊(2^order大小):
在這裡插入圖片描述
  不同大小的記憶體塊被加入到不同的連結串列中,其組織結構如下:
在這裡插入圖片描述
  系統中的每個實體記憶體頁(頁幀)都對應一個struct page資料結構,每個節點都包含了多個zone,每個zone都有struct zone表示,其中儲存了用於夥伴系統的資料結構。zone中的:

struct zone {
	unsigned long watermark[NR_WMARK];
		/* free areas of different sizes */
	struct free_area	free_area[MAX_ORDER]; //用於管理該zone的夥伴系統資訊
}

夥伴系統將基於這些資訊管理該zone的實體記憶體。該陣列中每個陣列項用於管理一個空閒記憶體頁塊連結串列,同一個連結串列中的記憶體頁塊的大小相同,並且大小為2^order。MAX_ORDER定義了支援的最大的記憶體頁塊大小(11)。
struct free_area的定義如下:

struct free_area {
	struct list_head	free_list[MIGRATE_TYPES];
	unsigned long		nr_free;
};

(1)free_list:是用於連線空閒頁的連結串列. 頁連結串列包含大小相同的連續記憶體區
(2)nr_free:nr_free表示記憶體頁塊的數目,對於0階的表示以1頁為單位計算,對於1階的以2頁為單位計算,n階的以2的n次方為單位計算。
  zone->free_area[MAX_ORDER]陣列中階作為各個元素的索引, 用於指定對應連結串列中的連續記憶體區包含多少個頁幀。
   a、陣列中第0個元素的order為0, 它的free_list連結串列域指向具有包含區為單頁(2^0=1)的記憶體頁面連結串列
   b、陣列中第1個元素的free_list域管理的記憶體區為兩頁(2^1=2)的連續頁面
   c、第3個管理的記憶體區為4頁, 依次類推.
   d、直到2^MAXORDER−1個頁面大小的塊(1024個頁面,也就是4MB)。
可以用下圖清晰的表明其之間的關係:
在這裡插入圖片描述


  夥伴系統和當前狀態的資訊可以在/proc/buddyinfo中獲取(從左到右,order依次增加):
在這裡插入圖片描述

二、記憶體碎片
1、碎片概念:
  在系統長時間執行後,實體記憶體會出現很多碎片,如圖左圖所示,左圖中,雖然還有很多記憶體頁可供使用,但連續的頁只有一頁,對於使用者程式來說可能不成問題,因為使用者程式通過頁表對映,看到都是連續的虛擬記憶體,但核心部分時候需要連續的實體記憶體,會導致系統在申請大塊的記憶體的記憶體時無法申請到,相對於申請大塊記憶體,右圖的狀態會表現的更好,因為存在大片連續的頁可供分配:
在這裡插入圖片描述
2、避免記憶體碎片:
  在核心2.6.24開發期間,防止碎片的方法最終加入核心。核心根據頁的可移動性將其劃分為3種不同的型別:
   - 不可移動的頁:在記憶體中有固定位置,不能移動。分配給核心核心的頁大多是此種類型;
   - 可回收的頁:不能移動,但是可以刪除,其內容可以從某些源重新生成;
   - 可移動的頁:可以隨意移動,屬於使用者程序的頁屬於這種型別,因為它們是通過頁表對映的;
   由於頁無法移動, 導致在原本幾乎全空的記憶體區中無法進行連續分配. 根據頁的可移動性, 將其分配到不同的列表中, 即可防止這種情形。例如, 不可移動的頁不能位於可移動記憶體區的中間, 否則就無法從該記憶體區分配較大的連續記憶體塊。核心的另一種方法確實將記憶體分割槽, 分別用於可移動頁和不可移動頁的分配。與此思想相關的結構體型別如下:

enum {
	MIGRATE_UNMOVABLE,
	MIGRATE_MOVABLE,
	MIGRATE_RECLAIMABLE,
	MIGRATE_PCPTYPES,
	MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,
	MIGRATE_TYPES
};

對夥伴系統資料結構的主要調整, 是將空閒列表分解為MIGRATE_TYPE個列表,資料結構如下,每一個free_list中都對應有不同型別的頁面:

struct free_area {
	struct list_head	free_list[MIGRATE_TYPES];
	unsigned long		nr_free;
};

在這裡插入圖片描述
3、pageblock相關:
  記憶體管理中有一個pageblock的概念,一個pageblock的大小是(MAX_ORDER-1)個頁面,每個pageblock有一個MIGRATE_TYPES型別,zone的資料結構中有個pageblock_flags的指標,指向用於存放pageblock的MIGRATE_TYPES型別的記憶體空間,每個pageblock用4個位元位來存放MIGRATE_TYPES的型別:

struct zone {
	/*
	 * Flags for a pageblock_nr_pages block. See pageblock-flags.h.
	 * In SPARSEMEM, this map is stored in struct mem_section
	 */
	unsigned long		*pageblock_flags;
}

核心中有兩個函式來管理這些遷移型別:set_pageblock_migratetype和get_pageblock_migratetype:
- set_pageblock_migratetype:用於設定一個以指定的頁為起始地址的記憶體區的遷移型別;
- get_pageblock_migratetype:可用於從struct page中獲取頁的遷移型別。
核心初始化是所有頁面都被標記位可遷移型別(MIGRATE_MOVABLE),在啟動期間分配可移動記憶體區的情況較少, 那麼分配器有很高的機率分配長度最大的記憶體區, 並將其從可移動列表轉換到不可移動列表,那麼在核心分配不可移動的記憶體區時,則必須”盜取”MIGRATE_MOVABLE型別的頁面。

//./kernel-4.4/mm/page_alloc.c
void __meminit memmap_init_zone(unsigned long size, int nid, unsigned long zone,
		unsigned long start_pfn, enum memmap_context context)
{
	z = &pgdat->node_zones[zone];
	for (pfn = start_pfn; pfn < end_pfn; pfn++) {
			if (!(pfn & (pageblock_nr_pages - 1))) {
			struct page *page = pfn_to_page(pfn);

			__init_single_page(page, pfn, zone, nid);
			set_pageblock_migratetype(page, MIGRATE_MOVABLE);
		} 
		......
}

三、夥伴系統分配記憶體
  記憶體中分配物理頁面的介面函式是:alloc_pages(),是夥伴系統分配記憶體的核心函式,用於分配一個或者多個連續的物理頁面,分配頁面的大小隻能是2^order個頁面。alloc_page()函式的定義如下,包含分配掩碼gfp_mask和分配階數order的引數。

//kernel-4.4/include/linux/gfp.h
#define alloc_pages(gfp_mask, order) \
		alloc_pages_node(numa_node_id(), gfp_mask, order)

1、分配掩碼gfp_mask介紹:
  gfp_mask是非常重要的引數,也定義與gfp.h問中,包含以下幾種型別:
- 區域描敘符zone modifiers:指定總哪個zone中分配頁面,核心優先從ZONE_NORMAL開始分配;
- 行為描敘符action modifiers:表示核心應該如何分配所需的記憶體,在某些特定的情況下,只能使用某些特定的方法分配
記憶體,例如,中斷處理程式就要求核心在分配記憶體時不能睡眠(因為中斷處理程式不能被重新排程);
- 型別描敘符:組合了行為區域描敘符和行為描敘符,將這些可能用到的組合歸納為不同型別;
(1)區域描敘符

#define __GFP_DMA	((__force gfp_t)___GFP_DMA)                             /*從ZONE_DMA中分配記憶體*/               
#define __GFP_HIGHMEM	((__force gfp_t)___GFP_HIGHMEM)				        /*從ZONE_HIGHMEM活ZONE_NORMAL中分配記憶體*/
#define __GFP_DMA32	((__force gfp_t)___GFP_DMA32)                           /*從ZONE_DMA32中分配記憶體*/
#define __GFP_MOVABLE	((__force gfp_t)___GFP_MOVABLE)                     /* 從__GFP_MOVABLE中分配記憶體 */
#define GFP_ZONEMASK    (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE)

(2)行為描敘符:

#define __GFP_RECLAIMABLE ((__force gfp_t)___GFP_RECLAIMABLE)   		/* 頁是可回收的 */
#define __GFP_HIGH	((__force gfp_t)___GFP_HIGH)                        /*如果請求非常重要, 則設定__GFP_HIGH,即核心急切地需要記憶體時。*/       
#define __GFP_IO	((__force gfp_t)___GFP_IO)               		    /*__GFP_IO說明在查詢空閒記憶體期間核心可以進行I/O操作*/    
#define __GFP_FS	((__force gfp_t)___GFP_FS)						    /*分配器可以啟動檔案系統I/O*/
#define __GFP_COLD	((__force gfp_t)___GFP_COLD)						/*如果需要分配不在CPU快取記憶體中的“冷”頁時*/
#define __GFP_NOWARN	((__force gfp_t)___GFP_NOWARN)					/*在分配失敗時禁止核心故障警告。在極少數場合該標誌有用*/
#define __GFP_REPEAT	((__force gfp_t)___GFP_REPEAT)					/*在分配失敗後自動重試,但在嘗試若干次之後會停止*/
#define __GFP_NOFAIL	((__force gfp_t)___GFP_NOFAIL)					/*在分配失敗後一直重試,直至成功*/
#define __GFP_NORETRY	((__force gfp_t)___GFP_NORETRY)					/*在分配失敗後不重試,因此可能分配失敗*/
#define __GFP_MEMALLOC	((__force gfp_t)___GFP_MEMALLOC)				/*使用緊急分配連結串列*/
#define __GFP_COMP	((__force gfp_t)___GFP_COMP)						/* 增加複合頁元資料 */
#define __GFP_ZERO	((__force gfp_t)___GFP_ZERO)						/*在分配成功時,將返回填充位元組0的頁*/

(3)型別描敘符

#define __GFP_NOMEMALLOC ((__force gfp_t)___GFP_NOMEMALLOC)			/*不使用緊急分配連結串列*/
#define __GFP_HARDWALL   ((__force gfp_t)___GFP_HARDWALL)			/* 只能在當前程序可執行的cpu關聯的記憶體節點上分配記憶體*/
#define __GFP_THISNODE	((__force gfp_t)___GFP_THISNODE)			/* 只能在當前節點上分配記憶體 */
#define __GFP_ATOMIC	((__force gfp_t)___GFP_ATOMIC)				/* 用於原子分配,在任何情況下都不能中斷  */
#define __GFP_NOACCOUNT	((__force gfp_t)___GFP_NOACCOUNT)			
#define __GFP_NOTRACK	((__force gfp_t)___GFP_NOTRACK)			    /* 不對分配的記憶體進行跟蹤 */
#define __GFP_DIRECT_RECLAIM	((__force gfp_t)___GFP_DIRECT_RECLAIM) /* Caller can reclaim */
#define __GFP_OTHER_NODE ((__force gfp_t)___GFP_OTHER_NODE)				 /* On behalf of other node *
#define __GFP_WRITE	((__force gfp_t)___GFP_WRITE)
#define __GFP_KSWAPD_RECLAIM	((__force gfp_t)___GFP_KSWAPD_RECLAIM) /* kswapd can wake */
#define __GFP_NOTRACK_FALSE_POSITIVE (__GFP_NOTRACK)
#define __GFP_RECLAIM ((__force gfp_t)(___GFP_DIRECT_RECLAIM|___GFP_KSWAPD_RECLAIM))  /*可以被回收*/

(4)掩碼組:

#define GFP_ATOMIC	(__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM)
		/*用於原子分配,在任何情況下都不能中斷,這個標誌用在中斷處理程式, 下半部,持有自旋鎖以及其他不能睡眠的地方*/
#define GFP_KERNEL	(__GFP_RECLAIM | __GFP_IO | __GFP_FS)
		/*常規分配方式,可能會阻塞,為了獲取呼叫者所需的記憶體,核心會盡力而為,這個標誌應該是首選標誌*/
#define GFP_NOWAIT	(__GFP_KSWAPD_RECLAIM)
		/*與GFP_ATOMIC類似,不同之處在於,呼叫不會退給緊急記憶體池, 這就增加了記憶體分配失敗的可能性*/
#define GFP_NOIO	(__GFP_RECLAIM)					/*這種分配可以阻塞, 但不會啟動磁碟I/O */
#define GFP_NOFS	(__GFP_RECLAIM | __GFP_IO)		/*這種分配在必要時可以阻塞,可能啟動磁碟,但是不會啟動檔案系統操作*/
#define GFP_TEMPORARY	(__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_RECLAIMABLE)
#define GFP_USER	(__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
		/*常規的分配方式, 可能會阻塞. 這個標誌用於為使用者空間程序分配記憶體時使用*/
#define GFP_DMA		__GFP_DMA						/*用於分配適用於DMA的記憶體*/
#define GFP_DMA32	__GFP_DMA32						/*是GFP_USER的一個擴充套件,用於使用者空間它允許分配無法直接對映的高階記憶體*/
#define GFP_HIGHUSER	(GFP_USER | __GFP_HIGHMEM)
#define GFP_HIGHUSER_MOVABLE	(GFP_HIGHUSER | __GFP_MOVABLE)
#define GFP_TRANSHUGE	((GFP_HIGHUSER_MOVABLE | __GFP_COMP | \
			 __GFP_NOMEMALLOC | __GFP_NORETRY | __GFP_NOWARN) & \
			 ~__GFP_KSWAPD_RECLAIM)

/* Convert GFP flags to their corresponding migrate type */
#define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE)	/*分配將從虛擬記憶體域ZONE_MOVABLE進行*/

(5)應用:
  在編寫的程式碼中,使用GFP_KERNEL,和GFP_ATOMIC比較多,當然各個型別標誌也均有其應用場景:
  程序上下文, 可以睡眠:GFP_KERNEL
  程序上下文, 不可以睡眠:GFP_KERNEL,在睡眠之前或之後以GFP_KERNEL執行記憶體分配
  中斷處理程式/軟中斷/tasklet:GFP_ATMOIC
  需要用於DMA的記憶體, 可以睡眠:GFP_DMA GFP_KERNEL
  需要用於DMA的記憶體, 不可以睡眠:GFP_DMA GFP_ATOMIC

2、alloc_pages介紹:
  buddy系統分配記憶體有很多介面,如:alloc_page、get_zeroed_page、__get_free_page、__get_dma_page最後都會呼叫到alloc_pages去分配記憶體,只是在alloc_pages做了相關的修飾。下面看alloc_pages的呼叫,__alloc_pages_nodemask是夥伴系統的心臟函式,如下:

//alloc_pages->alloc_pages_node->__alloc_pages_node->__alloc_pages
static inline struct page *
__alloc_pages(gfp_t gfp_mask, unsigned int order,
		struct zonelist *zonelist)
{
	return __alloc_pages_nodemask(gfp_mask, order, zonelist, NULL);
}

__alloc_pages_nodemask函式主要完成以下工作:
  (1)找到指定的分配管理區,指定分配的型別;
  (2)嘗試開始分配記憶體:get_page_from_freelist,如果成功就返回分配到的頁;
  (3)在各個區域都找不到可以滿足分配的記憶體了,那麼說明管理區的記憶體已經確實不夠了,於是開始啟用一條慢速的途徑來分配,包括嘗試去喚醒kswapd去回收一些不經常使用的頁等等;

struct page *
__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
			struct zonelist *zonelist, nodemask_t *nodemask)
{
	/*根據gfp_mask確定分配頁所處的管理區*/
	enum zone_type high_zoneidx = gfp_zone(gfp_mask);
	struct zone *preferred_zone;
	struct page *page;
	/*根據gfp_mask得到遷移類分配頁的型*/
	int migratetype = allocflags_to_migratetype(gfp_mask);

	gfp_mask &= gfp_allowed_mask;
	lockdep_trace_alloc(gfp_mask);
	might_sleep_if(gfp_mask & __GFP_WAIT);
	if (should_fail_alloc_page(gfp_mask, order))
		return NULL;
 
	/*
	 * Check the zones suitable for the gfp_mask contain at least one
	 * valid zone. It's possible to have an empty zonelist as a result
	 * of GFP_THISNODE and a memoryless node
	 */
	if (unlikely(!zonelist->_zonerefs->zone))
		return NULL;
 
	/* The preferred zone is used for statistics later */
	/*從zonelist中找到zone_idx與high_zoneidx相同的管理區,也就是之前認定的管理區*/
	first_zones_zonelist(zonelist, high_zoneidx, nodemask, &preferred_zone);
	if (!preferred_zone)
		return NULL;
 
	/* First allocation attempt */
	page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, nodemask, order,
			zonelist, high_zoneidx, ALLOC_WMARK_LOW|ALLOC_CPUSET,
			preferred_zone, migratetype);
	if (unlikely(!page))
		/*第一次分配失敗的話則會用通過一條低速路徑來進行第二次分配,包括喚醒頁換出守護程序等等*/
		page = __alloc_pages_slowpath(gfp_mask, order,
				zonelist, high_zoneidx, nodemask,
				preferred_zone, migratetype);
 
	trace_mm_page_alloc(page, order, gfp_mask, migratetype);
	return page;
}

get_page_from_freelist函式主要完成以下工作:
  (1)從指定的管理區開始按照zonelist中定義的順序來遍歷管理區;
  (2)如果該管理區的水位線正常,則呼叫buffered_rmqueue()在該管理區中分配;
  (3)如果管理區的水位線過低,則在NUMA架構下會申請頁面回收;

static struct page *
get_page_from_freelist(gfp_t gfp_mask, nodemask_t *nodemask, unsigned int order,
		struct zonelist *zonelist, int high_zoneidx, int alloc_flags,
		struct zone *preferred_zone, int migratetype)
{
	struct zoneref *z;
	struct page *page = NULL;
	int classzone_idx;
	struct zone *zone;
	nodemask_t *allowednodes = NULL;/* zonelist_cache approximation */
	int zlc_active = 0;		/* set if using zonelist_cache */
	int did_zlc_setup = 0;		/* just call zlc_setup() one time */
 
	/*獲取管理區的編號*/
	classzone_idx = zone_idx(preferred_zone);
zonelist_scan:
	/*
	 * Scan zonelist, looking for a zone with enough free.
	 * See also cpuset_zone_allowed() comment in kernel/cpuset.c.
	 */
    /*從認定的管理區開始遍歷,直到找到一個擁有足夠空間的管理區,
	  例如,如果high_zoneidx對應的ZONE_HIGHMEM,則遍歷順序為HIGHMEM-->NORMAL-->DMA,
	  如果high_zoneidx對應ZONE_NORMAL,則遍歷順序為NORMAL-->DMA*/
	for_each_zone_zonelist_nodemask(zone, z, zonelist,
						high_zoneidx, nodemask) {
		if (NUMA_BUILD && zlc_active &&
			!zlc_zone_worth_trying(zonelist, z, allowednodes))
				continue;
 
		/*檢查給定的記憶體域是否屬於該程序允許執行的CPU*/
		if ((alloc_flags & ALLOC_CPUSET) &&
			!cpuset_zone_allowed_softwall(zone, gfp_mask))
				goto try_next_zone;
 
		BUILD_BUG_ON(ALLOC_NO_WATERMARKS < NR_WMARK);
		if (!(alloc_flags & ALLOC_NO_WATERMARKS)) {
			unsigned long mark;
			int ret;
			
            /*通過alloc_flags來確定是使用何種水印,pages_min?pages_low?pages_high?
			  選擇了一種水印,就要求分配後的空閒不低於該水印才能進行分配*/
			mark = zone->watermark[alloc_flags & ALLOC_WMARK_MASK];
 
			/*如果管理區的水位線處於正常水平,則在該管理區進行分配*/
			if (zone_watermark_ok(zone, order, mark,
				    classzone_idx, alloc_flags))
				goto try_this_zone;
 
			if (zone_reclaim_mode == 0)
				goto this_zone_full;
 
			/*下面這部分都是針對NUMA架構的申請頁面回收*/
			ret = zone_reclaim(zone, gfp_mask, order);
			switch (ret) {
			case ZONE_RECLAIM_NOSCAN:/*沒有進行回收*/
				/* did not scan */
				goto try_next_zone;
			case ZONE_RECLAIM_FULL:  /*沒有找到可回收的頁面*/
				/* scanned but unreclaimable */
				goto this_zone_full;
			default:
				/* did we reclaim enough */
				if (!zone_watermark_ok(zone, order, mark,
						classzone_idx, alloc_flags))
					goto this_zone_full;
			}
		}
 
try_this_zone:/*分配2^order個頁*/
		page = buffered_rmqueue(preferred_zone, zone, order,
						gfp_mask, migratetype);
		if (page)
			break;
this_zone_full:
		if (NUMA_BUILD)
			zlc_mark_zone_full(zonelist, z);
try_next_zone:
		if (NUMA_BUILD && !did_zlc_setup && nr_online_nodes > 1) {
			/*
			 * we do zlc_setup after the first zone is tried but only
			 * if there are multiple nodes make it worthwhile
			 */
			allowednodes = zlc_setup(zonelist, alloc_flags);
			zlc_active = 1;
			did_zlc_setup = 1;
		}
	}
 
	if (unlikely(NUMA_BUILD && page == NULL && zlc_active)) {
		/* Disable zlc cache for second zonelist scan */
		zlc_active = 0;
		goto zonelist_scan;
	}
	return page;
}
static inline
struct page *buffered_rmqueue(struct zone *preferred_zone,
			struct zone *zone, int order, gfp_t gfp_flags,
			int migratetype)
{
	unsigned long flags;
	struct page *page;
	int cold = !!(gfp_flags & __GFP_COLD);
	int cpu;
 
again:
	cpu  = get_cpu();
	if (likely(order == 0)) {/*order為0,即要求分配一個頁*/
		struct per_cpu_pages *pcp;
		struct list_head *list;
 
		pcp = &zone_pcp(zone, cpu)->pcp;/*獲取本地CPU對應的pcp*/
		list = &pcp->lists[migratetype];/*獲取和遷移型別對應的連結串列*/
		local_irq_save(flags);
 
		/*如果連結串列為空,則表示沒有可分配的頁,需要從夥伴系統中分配2^batch個頁給list*/
		if (list_empty(list)) {
			pcp->count += rmqueue_bulk(zone, 0,
					pcp->batch, list,
					migratetype, cold);
			if (unlikely(list_empty(list)))
				goto failed;
		}
 
		if (cold)/*如果是需要冷頁,則從連結串列的尾部獲取*/
			page = list_entry(list->prev, struct page, lru);
		else     /*如果是需要熱頁,則從連結串列的頭部獲取*/
			page = list_entry(list->next, struct page, lru);
        
		list_del(&page->lru);
		pcp->count--;
	} else {
		spin_lock_irqsave(&zone->lock, flags);
		/*從管理區的夥伴系統中選擇合適的記憶體塊進行分配*/
		page = __rmqueue(zone, order, migratetype);
		spin_unlock(&zone->lock);
		if (!page)
			goto failed;
		__mod_zone_page_state(zone, NR_FREE_PAGES, -(1 << order));
	}
 
	__count_zone_vm_events(PGALLOC, zone, 1 << order);
	zone_statistics(preferred_zone, zone);
	local_irq_restore(flags);
	put_cpu();
 
	VM_BUG_ON(bad_range(zone, page));
	if (prep_new_page(page, order, gfp_flags))
		goto again;
	return page;
}

__rmqueue-> __rmqueue_smallest ->expand,expand實現“切蛋糕”的功能,因為可能摘下來的記憶體比需要的記憶體大,需要把切完之後剩下的記憶體重新放到夥伴系統中:

static inline void expand(struct zone *zone, struct page *page,
	int low, int high, struct free_area *area,
	int migratetype)
{
	unsigned long size = 1 << high;/*order為high的頁塊對應的頁框數*/
 
	/*申請的order為low,實際分配的塊對應的order為high
	  如果high大於low則要將大塊進行拆分,並且將拆分後的夥伴塊新增到下一級order的塊連結串列中去*/
	while (high > low) {
		area--;/*area減1得到下一級order對應的area*/
		high--;/*high減1表明進行了一次拆分*/
		size >>= 1;/*拆分一次size就要除以2*/
		VM_BUG_ON(bad_range(zone, &page[size]));
 
		/*通過size來定位拆分後的夥伴塊的起始頁框描述符,
		並將其作為第一個塊新增到下一級order的塊連結串列中*/
		list_add(&page[size].lru, &area->free_list[migratetype]);
		area->nr_free++;/*該order區域的塊數加1*/
		set_page_order(&page[size], high);/*設定private域為high*/
	}
}

四、夥伴系統釋放記憶體
  夥伴系統釋放記憶體的介面為__free_pages,這些介面最終都會呼叫到__free_one_page來釋放記憶體:

void __free_pages(struct page *page, unsigned int order)
{
	if (put_page_testzero(page)) {
		if (order == 0)
			free_hot_cold_page(page, false);//如果釋放的是單頁,則呼叫free_hot_cold_page
		else
			__free_pages_ok(page, order);
	}
}
/*
page:指向釋放的頁框塊的首個page
zone:page所屬zone區
order:當前page的order
頁面回收是分配的逆過程,首先查詢當前要釋放的記憶體塊同order是否能跟臨近的夥伴合併,
如果能合併的話,再進一步查詢order+1夥伴,確認能否合併為一個order+2的記憶體塊,重複
這樣的過程,直到不能繼續合併;
如果不能合併的話,直接將要釋放的pages加入到對應order的夥伴系統中;
注意查詢夥伴系統的條件,比如某個order下四個記憶體區  A -- B -- C -- D 
AB,CD稱之為夥伴,BC不能稱為夥伴,因此查詢夥伴index演算法為
page_idx ^ (1 << order);
__find_buddy_index 函式查詢對應的夥伴
page_is_buddy 判斷兩個記憶體塊是否為夥伴,0 is not buddy, 1 is buddy
*/
static inline void __free_one_page(struct page *page,
		struct zone *zone, unsigned int order,
		int migratetype)
{
	//max_order=MAX_ORDER
	max_order = min_t(unsigned int, MAX_ORDER, pageblock_order + 1);
 
	VM_BUG_ON(!zone_is_initialized(zone));
 
	if (unlikely(PageCompound(page)))
		if (unlikely(destroy_compound_page(page, order)))
			return;
 
	VM_BUG_ON(migratetype == -1);
	if (likely(!is_migrate_isolate(migratetype)))
		__mod_zone_freepage_state(zone, 1 << order, migratetype);
		
	//計算page index
	page_idx = page_to_pfn(page) & ((1 << MAX_ORDER) - 1);
 
	VM_BUG_ON(page_idx & ((1 << order) - 1));
	VM_BUG_ON(bad_range(zone, page));
 
continue_merging:
	/* 
	首先從同order查詢,能合併的話,進一步往高一階order查詢
	*/
	while (order < max_order - 1) {
		//查詢當前要釋放的page的夥伴idx,先從同階找buddy
		buddy_idx = __find_buddy_index(page_idx, order);
		buddy = page + (buddy_idx - page_idx);
		if (!page_is_buddy(page, buddy, order))//page,buddy不能合併,跳轉到done_merging
			goto done_merging;
		//當前order能合併,如果有開CONFIG_DEBUG_PAGEALLOC,進入if,否則進入else
		if (page_is_guard(buddy)) {
			clear_page_guard_flag(buddy);
			set_page_private(page, 0);
			__mod_zone_freepage_state(zone, 1 << order,
						  migratetype);
		} else {
		//先將找到的buddy從夥伴系統刪除,
			list_del(&buddy->lru);
			zone->free_area[order].nr_free--;
			if (is_migrate_cma(migratetype))
				zone->free_area[order].nr_free_cma--;
			rmv_page_order(buddy);
		}
		//接著將buddy跟page進行合併,計算合併後的page index
		combined_idx = buddy_idx & page_idx;
		page = page + (combined_idx - page_idx);
		page_idx = combined_idx;
		order++;
		//最後order+1後重復這個過程
	}
	if (max_order < MAX_ORDER) {
		if (unlikely(has_isolate_pageblock(zone))) {
			int buddy_mt;
 
			buddy_idx = __find_buddy_index(page_idx, order);
			buddy = page + (buddy_idx - page_idx);
			buddy_mt = get_pageblock_migratetype(buddy);
 
			if (migratetype != buddy_mt
					&& (is_migrate_isolate(migratetype) ||
					is_migrate_isolate(buddy_mt)))
				goto done_merging;
		}
		max_order++;
		goto continue_merging;
	}
 
done_merging:
	//重新設定這塊記憶體order
	set_page_order(page, order);
 
	 //page跟buddy能合併的情況下,檢查能否跟更高一階order合併
	if ((order < MAX_ORDER-2) && pfn_valid_within(page_to_pfn(buddy))) {
		struct page *higher_page, *higher_buddy;
		combined_idx = buddy_idx & page_idx;//合併後的首個page idx
		higher_page = page + (combined_idx - page_idx); //合併後的page
		buddy_idx = __find_buddy_index(combined_idx, order + 1);
		higher_buddy = higher_page + (buddy_idx - combined_idx);
		if (page_is_buddy(higher_page, higher_buddy, order + 1)) {
		//這個是將page,buddy加入order free_list尾部,這樣跟order+1可以link起來
			list_add_tail(&page->lru,
				&zone->free_area[order].free_list[migratetype]);
			goto out;
		}
	}
	//將page插入到對應order free_list頭部
	list_add(&page->lru, &zone->free_area[order].free_list[migratetype]);
out:
	zone->free_area[order].nr_free++;
	if (is_migrate_cma(migratetype))
		zone->free_area[order].nr_free_cma++;
}

作者:frank_zyp
您的支援是對博主最大的鼓勵,感謝您的認真閱讀。
本文無所謂版權,歡迎轉載。