嵌入式Linux驅動筆記(二十九)------內存管理之夥伴算法(Buddy)分析

你好!這裏是風箏的博客,

歡迎和我一起交流。


我們知道,在一個通用操作系統裏,頻繁申請內存釋放內存都會出現一個非常著名的內存管理問題:內存碎片。
學過操作系統的都知道,有很多行之有效的方法(比如:記錄現存的空閒連續頁框塊的情況,以儘量避免爲滿足小塊的請求而分割大的空閒塊;小內存單獨分配,大內存系統自動分配)可以很大程度上避免出現內存碎片,其中夥伴算法被證明是非常行之有效的一套內存管理方法,因此也被相當多的操作系統所採用。

夥伴算法的原理也比較簡單,可以學習一下:
夥伴算法將內存分成若干個塊,以Linux爲例,把所有的空閒頁框分組爲 11 條鏈表,每一塊鏈表分別包含大小爲2^0 個,2^1 個,2^2 個,2^3 個…到2^10 個連續的頁框。在第11條鏈表中,2^10 個連續的頁框的最大請求對應着 4MB 大小的連續RAM 塊。每一塊的第一個頁框的物理地址是該塊大小的整數倍。例如,大小爲 16個頁框的塊,其起始地址是 16 * 2^12 (2^12 = 4096,這是一個常規頁的大小)的倍數。每個鏈表中元素的個數在系統初始化時決定,在執行過程中,動態變化。
如圖所示:
Buddy
假設要申請一個256個頁框的塊(2^8),先從256個頁框的鏈表(第9條鏈表)中查找空閒塊,如果沒有,就去下一個更大當頁塊,即512個頁框的鏈表中找,找到了則將頁框塊分爲2個256個頁框的塊,一個分配給應用,另外一個移到256個頁框的鏈表中。如果512個頁框的鏈表中仍沒有空閒塊,繼續向1024個頁框的鏈表查找,如果仍然沒有,則返回錯誤。頁框塊在釋放時,會主動將兩個連續的頁框塊合併爲一個較大的頁框塊。

簡而言之,就是在分配內存時,首先從空閒的內存中搜索比申請的內存大的最小的內存塊。如果這樣的內存塊存在,則將這塊內存標記爲“已用”,同時將該內存分配給應用程序。如果這樣的內存不存在,則操作系統將尋找更大塊的空閒內存,然後將這塊內存平分成兩部分,一部分返回給程序使用,另一部分作爲空閒的內存塊等待下一次被分配。

所以夥伴算法一直在對頁框做拆開合併拆開合併的動作。Buddy算法厲害就厲害在運用了世界上任何正整數都可以由2^n的和組成。這也是Buddy算法管理空閒頁表的本質。

假設要釋放一個256個頁框的塊,算法就把其插入到256個頁框的鏈表中,然後檢查與該內存相鄰的內存,如果存在同樣大小爲256個頁框的並且空閒的內存,而且由同一個大塊分裂而來(互爲夥伴),就將這兩塊內存合併成512個頁框,然後插入到512個頁框的鏈表中,如果不存在,就沒有後面的合併操作。然後再進一步檢查,如果合併後的512個頁框的內存存在大小爲512個頁框的相鄰且空閒的內存,則將兩者合併,然後插入到1024個頁框的鏈表中。

何謂“夥伴”?
由同一大塊分裂出來的小塊就稱之“互爲夥伴”。
例如:假設p爲大小爲pow(2,k)的空閒塊的初始地址,且p MOD pow(2,k+1)=0,則初始地址爲p和p+pow(2,k)的兩個空閒塊互爲夥伴。在夥伴系統中回收空閒塊時,只當其夥伴爲空閒塊時才歸併成大塊。也就是說,若有兩個空閒塊,即使大小相同且地址相鄰,但不是由同一大塊分裂出來的,也不歸併在一起。

簡而言之,就是當程序釋放內存時,操作系統首先將該內存回收,然後檢查與該內存相鄰的內存是否是同樣大小並且同樣處於空閒的狀態,同時由同一個大塊分裂而來(互爲夥伴),如果是,則需在相應子表中找到其夥伴並刪除,然後將這兩塊內存合併,然後程序遞歸進行同樣的檢查,試圖再次合併相鄰的塊,以再次試圖形成更大的塊,最後插入到相應的子表中去。

這樣能以最合適的分配方法將內存分配出去,使得系統可以避免外部碎片的產生(缺點就是產生了內部碎片)

內部碎片的產生:因爲所有的內存分配必須起始於可被 4、8 或 16 整除(視處理器體系結構而定)的地址或者因爲MMU的分頁機制的限制,決定內存分配算法僅能把預定大小的內存塊分配給客戶。
假設當請求一個65 字節的內存塊時,因爲沒有適合大小的內存,所以它會獲得128字節等稍大一點的字節,因此由所需大小四捨五入而產生的多餘空間就叫內部碎片。

假設系統中有 1MB 大小的內存需要動態管理,按照夥伴算法的要求:需要將這1M大小的內存進行劃分。這裏,我們將這1M的內存分爲 64K、64K、128K、256K、和512K 共五個部分,如下圖t0時刻所示:
simple
t1時刻,如果有一個程序A想要申請一塊45K大小的內存,則系統會將第一塊64K的內存塊分配給該程序(產生內部碎片爲代價)
t2時刻,程序B向系統申請一塊68K大小的內存,系統會將128K內存分配給該程序
t3時刻,程序C要申請一塊大小爲35K的內存。系統將空閒的64K內存分配給該程序
t4時刻,程序D需要一塊大小爲90K的內存。當程序提出申請時,系統本該分配給程序D一塊128K大小的內存,但此時內存中已經沒有空閒的128K內存塊了,於是根據夥伴算法的原理,系統會將256K大小的內存塊平分,將其中一塊分配給程序D,另一塊作爲空閒內存塊保留,等待以後使用
t5時刻,程序C釋放了它申請的64K內存。在內存釋放的同時,系統還負責檢查與之相鄰並且同樣大小的內存是否也空閒,由於此時程序A並沒有釋放它的內存,所以系統只會將程序C的64K內存回收
t6時刻,程序A也釋放掉由它申請的64K內存,系統隨機發現與之相鄰且大小相同的一段內存塊恰好也處於空閒狀態。於是,將兩者合併成128K內存
t7時刻,程序B釋放掉它的128k,系統也將這塊內存與相鄰的128K內存合併成256K的空閒內存
t8時刻,程序D也釋放掉它的內存,經過三次合併後,系統得到了一塊1024K的完整內存

最後,我們可以簡單追一下Linux4.9的源碼,Linux也是採用了夥伴算法:

Linux內核管理物理內存是通過分頁機制實現的,它將整個內存劃分成無數個4k大小的頁,從而分配和回收內存的基本單位便是內存頁了。利用分頁管理有助於靈活分配內存地址,因爲分配時不必要求必須有大塊的連續內存[3],系統可以東一頁、西一頁的湊出所需要的內存供進程使用。雖然如此,但是實際上系統使用內存時還是傾向於分配連續的內存塊,因爲分配連續內存時,頁表不需要更改,因此能降低TLB的刷新率(頻繁刷新會在很大程度上降低訪問速度)。

鑑於上述需求,內核分配物理頁面時爲了儘量減少不連續情況,採用了“夥伴”關係來管理空閒頁面。

夥伴系統完成了初始化(將內存的按照 order 添加到 zonelist 後),就可以利用夥伴系統進行分配內存了,對於物理頁的分配,夥伴系統分配的單位都是 2^order 次冪。
這些分配的宏函數都位於:include/linux/gfp.h 中

對於 alloc_pages(mask, order):分配2^order頁並返回一個struct page的實例,表示分配的內存塊的起始頁

#define alloc_pages(gfp_mask, order) \
		alloc_pages_node(numa_node_id(), gfp_mask, order)
alloc_page(mask):

#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)
get_zeroed_page(mask):

unsigned long get_zeroed_page(gfp_t gfp_mask)
{
        return __get_free_pages(gfp_mask | __GFP_ZERO, 0);
}
EXPORT_SYMBOL(get_zeroed_page);
__get_free_pages(mask, order):

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
	struct page *page;

	/*
	 * __get_free_pages() returns a 32-bit address, which cannot represent
	 * a highmem page
	 */
	VM_BUG_ON((gfp_mask & __GFP_HIGHMEM) != 0);

	page = alloc_pages(gfp_mask, order);
	if (!page)
		return 0;
	return (unsigned long) page_address(page);
}
EXPORT_SYMBOL(__get_free_pages);
__get_free_page(mask):

#define __get_free_page(gfp_mask) \
		__get_free_pages((gfp_mask), 0)
__get_dma_pages(gfp_mask, order) 

#define __get_dma_pages(gfp_mask, order) \
		__get_free_pages((gfp_mask) | GFP_DMA, (order))

上述的所有接口調用,最終都集合到了 alloc_pages 接口:
alloc

alloc_pages 函數的參數比alloc_pages()多了一個nid,它用來指定節點id,如果nid小於0,則說明在當前節點上分配頁框。正確獲取到節點id後,接下來調用__alloc_pages()。
函數調用過程如下:

alloc_pages -> 
    alloc_pages_node -> 
        __alloc_pages ->
             __alloc_pages_nodemask

__alloc_pages_nodemask函數在Linux代碼裏當註釋爲:This is the ‘heart’ of the zoned buddy allocator.
可想而知是多麼的核心了!

/*
 * This is the 'heart' of the zoned buddy allocator.
 */
struct page *
__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
			struct zonelist *zonelist, nodemask_t *nodemask)
{
	struct page *page;
	unsigned int alloc_flags = ALLOC_WMARK_LOW;
	gfp_t alloc_mask = gfp_mask; /* The gfp_t that was actually used for allocation */
	struct alloc_context ac = {
		.high_zoneidx = gfp_zone(gfp_mask),/*根據gfp_mask確定分配頁所處的zone*/
		.zonelist = zonelist,
		.nodemask = nodemask,
		.migratetype = gfpflags_to_migratetype(gfp_mask),/*根據gfp_mask得到遷移類分配頁的型*/
	};

	if (cpusets_enabled()) {
		alloc_mask |= __GFP_HARDWALL;
		alloc_flags |= ALLOC_CPUSET;
		if (!ac.nodemask)
			ac.nodemask = &cpuset_current_mems_allowed;
	}

	gfp_mask &= gfp_allowed_mask;

	lockdep_trace_alloc(gfp_mask);

	might_sleep_if(gfp_mask & __GFP_DIRECT_RECLAIM);//設置了__GFP_DIRECT_RECLAIM,則調用該函數的進程可能休眠

	if (should_fail_alloc_page(gfp_mask, order))
		return NULL;
	if (unlikely(!zonelist->_zonerefs->zone))
		return NULL;

	if (IS_ENABLED(CONFIG_CMA) && ac.migratetype == MIGRATE_MOVABLE)
		alloc_flags |= ALLOC_CMA;

	/* Dirty zone balancing only done in the fast path */
	ac.spread_dirty_pages = (gfp_mask & __GFP_WRITE);

	/*從zonelist中找到zone_idx與high_zoneidx相同的zone,也就是之前認定的zone*/
	ac.preferred_zoneref = first_zones_zonelist(ac.zonelist,
					ac.high_zoneidx, ac.nodemask);
	if (!ac.preferred_zoneref->zone) {
		page = NULL;
		goto no_zone;
	}

	/* First allocation attempt */
	page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac);//開始分配內存
	if (likely(page))
		goto out;

no_zone://沒有zone當情況
	alloc_mask = memalloc_noio_flags(gfp_mask);
	ac.spread_dirty_pages = false;
	if (unlikely(ac.nodemask != nodemask))
		ac.nodemask = nodemask;
	//如果上面沒有分配到空間,調用下面函數慢速分配,允許等待和回收
	page = __alloc_pages_slowpath(alloc_mask, order, &ac);

out:
	if (memcg_kmem_enabled() && (gfp_mask & __GFP_ACCOUNT) && page &&
	    unlikely(memcg_kmem_charge(page, gfp_mask, order) != 0)) {
		__free_pages(page, order);
		page = NULL;
	}

	if (kmemcheck_enabled && page)
		kmemcheck_pagealloc_alloc(page, order, gfp_mask);

	trace_mm_page_alloc(page, order, alloc_mask, ac.migratetype);

	return page;
}

可以看出,重要的還是get_page_from_freelist函數,從free的page裏分配出內存來使用,如果分配失敗,則纔會調用__alloc_pages_slowpath分配。
__alloc_pages_slowpath函數會調用wakeup_all_kswapd,該函數喚醒負責換出頁的內核守護進程。交換守護進程通過縮減內核緩存和頁面回收來獲得更多的空閒內存,縮減內核緩存和頁面回涉及到頁面寫回或者換出很少使用的頁面。這兩種措施都是由守護進程發起的。喚醒守護進程後,內核開始重新嘗試從內存zones中查找合適的內存塊,會再次在__alloc_pages_slowpath函數裏面調用get_page_from_freelist函數!

static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
						const struct alloc_context *ac)
{
	struct zoneref *z = ac->preferred_zoneref;
	struct zone *zone;
	struct pglist_data *last_pgdat_dirty_limit = NULL;

	/*從認定的zonelist開始遍歷,直到找到一個擁有足夠空間的zone,
	 *	  例如,如果high_zoneidx對應的ZONE_HIGHMEM,則遍歷順序爲HIGHMEM-->NORMAL-->DMA,
	 *		  如果high_zoneidx對應ZONE_NORMAL,則遍歷順序爲NORMAL-->DMA*/
	for_next_zone_zonelist_nodemask(zone, z, ac->zonelist, ac->high_zoneidx,//從zonelist給定的ac->high_zoneidx開始查找,返回的是zone
								ac->nodemask) {
		struct page *page;
		unsigned long mark;

		if (cpusets_enabled() &&
			(alloc_flags & ALLOC_CPUSET) &&
			!__cpuset_zone_allowed(zone, gfp_mask))
				continue;
		if (ac->spread_dirty_pages) {
			if (last_pgdat_dirty_limit == zone->zone_pgdat)
				continue;

			if (!node_dirty_ok(zone->zone_pgdat)) {
				last_pgdat_dirty_limit = zone->zone_pgdat;
				continue;
			}
		}

		/*通過alloc_flags來確定是使用何種水位,pages_min?pages_low?pages_high?
		  選擇了一種水位,就要求分配後的空閒不低於該水位才能進行分配*/
		mark = zone->watermark[alloc_flags & ALLOC_WMARK_MASK];//從flags取出水位
		if (!zone_watermark_fast(zone, order, mark,//快速檢查該zone是否值得進一步去查找空閒內存
				       ac_classzone_idx(ac), alloc_flags)) {
			int ret;

			/* Checked here to keep the fast path fast */
			BUILD_BUG_ON(ALLOC_NO_WATERMARKS < NR_WMARK);
			if (alloc_flags & ALLOC_NO_WATERMARKS)
				goto try_this_zone;

			if (node_reclaim_mode == 0 ||
			    !zone_allows_reclaim(ac->preferred_zoneref->zone, zone))//如果不允許回收
				continue;

			ret = node_reclaim(zone->zone_pgdat, gfp_mask, order);//通過zone_reclaim進行一些頁面回收
			switch (ret) {
			case NODE_RECLAIM_NOSCAN:
				/* did not scan */
				continue;
			case NODE_RECLAIM_FULL:
				/* scanned but unreclaimable */
				continue;
			default:
				/* did we reclaim enough */
				if (zone_watermark_ok(zone, order, mark,
						ac_classzone_idx(ac), alloc_flags))//此時水位OK
					goto try_this_zone;//包括水位各種條件都滿足之後,可以在此zone進行頁面分配工作

				continue;
			}
		}

try_this_zone:
		page = buffered_rmqueue(ac->preferred_zoneref->zone, zone, order,//從此zone分配內存
				gfp_mask, alloc_flags, ac->migratetype);
		if (page) {
			prep_new_page(page, order, gfp_mask, alloc_flags);

			if (unlikely(order && (alloc_flags & ALLOC_HARDER)))
				reserve_highatomic_pageblock(page, zone, order);

			return page;
		}
	}

	return NULL;
}

該函數主要是遍歷各個內存管理區列表zonelist以嘗試頁面申請。其中for_each_zone_zonelist_nodemask()則是用於遍歷zonelist的,每個內存管理區嘗試申請前,在正常情況下需要進行一系列的驗證,保證當前zone有足夠的可用頁面供分配。那麼什麼是非正常情況呢?即使攜帶ALLOC_NO_WATERMARKS標識的,所以這裏就分爲兩種情況。這裏涉及到一個watermark,俗稱分配水位,水位有三種

#define ALLOC_WMARK_MIN WMARK_MIN
#define ALLOC_WMARK_LOW WMARK_LOW
#define ALLOC_WMARK_HIGH WMARK_HIGH

在分配之前一般會指定滿足那個水位才允許分配,或者不管水位直接分配。這就對應ALLOC_NO_WATERMARKS標識。
在zone結構中,有vm_stat字段,是一個數組,記錄各個狀態的頁面的數量,其中就包含空閒頁面,對應NR_FREE_PAGES,攜帶watermark標識的分配,需要驗證空閒頁面是否大於對應的水位,只有在大於水位了才允許分配,否則需要根據情況對頁面進行回收reclaim,如果無法回收或者回收後仍然不滿足條件,則直接返回了。在一些急迫的事務中,可以指定ALLOC_NO_WATERMARKS,這樣會不會對水位進行驗證,直接調用buffered_rmqueue分配頁面。

更多關於watermark可以參考:Linux內存管理 (1)物理內存初始化

當watermark滿足時,調用buffered_rmqueue函數在當前滿足條件的zone進行內存分配。
buffered_rmqueue並不直接從夥伴系統分配,爲了加速分配流程,每個CPU也會維護頁框高速緩存,通過per_cpu_pageset管理:

struct per_cpu_pageset {
    struct per_cpu_pages pcp;
#ifdef CONFIG_NUMA
    s8 expire;
#endif
#ifdef CONFIG_SMP
    s8 stat_threshold;
    s8 vm_stat_diff[NR_VM_ZONE_STAT_ITEMS];
#endif
};

結構體中,per_cpu_pages 維護了各種性質的頁面鏈表,性質基本是根據可移動性來決定的:

struct per_cpu_pages {
	int count;		/* number of pages in the list */
	int high;		/* high watermark, emptying needed */
	int batch;		/* chunk size for buddy add/remove */

	/* Lists of pages, one per migrate type stored on the pcp-lists */
	struct list_head lists[MIGRATE_PCPTYPES];/*鏈表數組,每個遷移類型維護一個數組*/
};

結構體註釋寫的很清楚了:
count代表鏈表中所有頁面的數量;
high和清空相關;
batch是當緩存不足時,每次從夥伴系統申請多少頁面填充進來。

核心分配邏輯如下:

static inline
struct page *buffered_rmqueue(struct zone *preferred_zone,
			struct zone *zone, unsigned int order,
			gfp_t gfp_flags, unsigned int alloc_flags,
			int migratetype)
{
	unsigned long flags;
	struct page *page;
	bool cold = ((gfp_flags & __GFP_COLD) != 0);

	if (likely(order == 0)) {//如果只申請一頁,那麼就從當前cpu的高速緩存內存中申請
		struct per_cpu_pages *pcp;
		struct list_head *list;
		/* 這裏需要關中斷,因爲內存回收過程可能發送核間中斷,強制每個核從每CPU
		   緩存中釋放頁面。而且中斷處理函數也會分配單頁。 */
		local_irq_save(flags);
		do {
			pcp = &this_cpu_ptr(zone->pageset)->pcp;//獲取zoon的當前處理器的高速緩存內存描述結構指針
			list = &pcp->lists[migratetype];
			if (list_empty(list)) {/*如果鏈表爲空,則表示沒有可分配的頁,需要從夥伴系統中分配2^batch個頁給list*/
				pcp->count += rmqueue_bulk(zone, 0,//調用此函數從夥伴系統中分配batch空閒內存到高速緩存
						pcp->batch, list,
						migratetype, cold);
				if (unlikely(list_empty(list)))
					goto failed;
			}

			/* 如果需要的是冷頁,即分配的頁面不需要考慮硬件緩存(注意不是每CPU頁面緩存)
			   ,則取出鏈表的最後一個節點返回給上層*/
			if (cold)
				page = list_last_entry(list, struct page, lru);
			else
				/* 如果使需要熱頁,即要考慮硬件緩存,則取出鏈表的第一個頁面,這個頁面是最近剛釋放到每CPU
				 *			緩存的,緩存熱度更高 */
				page = list_first_entry(list, struct page, lru);

			list_del(&page->lru);//由於被分配出去了,所以高速緩存內存中不再包含這頁內存,所以從鏈表裏刪除這一項
			pcp->count--;//相應的當前頁數也要減少

		} while (check_new_pcp(page));
	} else {/*當order爲大於1時,不從pcp中分配,直接考慮從夥伴系統中分配*/
		/*
		 * We most definitely don't want callers attempting to
		 * allocate greater than order-1 page units with __GFP_NOFAIL.
		 */
		WARN_ON_ONCE((gfp_flags & __GFP_NOFAIL) && (order > 1));
		spin_lock_irqsave(&zone->lock, flags);

		do {
			page = NULL;
			if (alloc_flags & ALLOC_HARDER) {//通知夥伴系統放寬檢查限制
			/*從指定order開始從小到達遍歷,優先從指定的遷移類型鏈表中分配頁面*/
				page = __rmqueue_smallest(zone, order, MIGRATE_HIGHATOMIC);
				if (page)
					trace_mm_page_alloc_zone_locked(page, order, migratetype);
			}
			if (!page)
			/*從管理區的夥伴系統中選擇合適的內存塊進行分配*/
				page = __rmqueue(zone, order, migratetype);
		} while (page && check_new_pages(page, order));//這裏進行安全性檢查,並進行一些善後工作。如果頁面標誌破壞,返回的頁面出現了問題,則返回試圖分配其他頁面
		spin_unlock(&zone->lock);
		if (!page)
			goto failed;
			 /* 已經分配了1 << order個頁面,這裏進行管理區空閒頁面統計計數*/
		__mod_zone_freepage_state(zone, -(1 << order),
					  get_pcppage_migratetype(page));
	}

	__count_zid_vm_events(PGALLOC, page_zonenum(page), 1 << order);
	zone_statistics(preferred_zone, zone, gfp_flags);
	local_irq_restore(flags);

	VM_BUG_ON_PAGE(bad_range(zone, page), page);
	return page;

failed:
	local_irq_restore(flags);
	return NULL;
}

當只請求一個頁面時,每個CPU都維護着一個遷移頁面鏈表,在指定的mingratetype遷移頁面鏈表查找是否有空閒頁面,就分配一個頁面,同時更新遷移鏈表中的計數,最後請求冷/熱頁面。

當某個物理內存頁面在CPU_Cache理,CPU訪問該頁的數據可以快速直接從Cache裏面讀取,此時該頁面則稱爲“熱(Hot)頁面”,反之,頁面不在CPU_Cache中,則稱爲“冷(Cold)頁面”。
在多CPU系統中,每個CPU都有自己的Cache,因此冷/熱頁面按CPU來管理,每個cpu都維持着一個冷/熱頁面內存池。

若申請多個頁面時,從夥伴系統中進行內存分配。核心函數爲:__rmqueue,在zone分配2^order個物理地址連續當頁面。

static struct page *__rmqueue(struct zone *zone, unsigned int order,
				int migratetype)
{
	struct page *page;

#ifdef CONFIG_CMA
	if (migratetype == MIGRATE_MOVABLE)
		page = __rmqueue_cma_prev(zone, order, migratetype);
	else
#endif
		page = __rmqueue_smallest(zone, order, migratetype);//開始分配
	if (unlikely(!page))//如果申請不到page
		page = __rmqueue_fallback(zone, order, migratetype);//從fallback鏈表中分配指定的order和migrate頁面
	trace_mm_page_alloc_zone_locked(page, order, migratetype);
	return page;
}

在numa系統中,內核總是首先嚐試從進程所在的CPU節點上分配內存,這樣內存性能更好。但是這種分配策略並不能保證每次都能成功。
對於不能從本節點分配內存的情況,每個節點都提供一個fallback鏈表。該鏈表包含其他節點及區域,可以作爲內存分配的替代選擇。

static inline
struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
						int migratetype)
{
	unsigned int current_order;
	struct free_area *area;
	struct page *page;

	/* Find a page of the appropriate size in the preferred list */
	for (current_order = order; current_order < MAX_ORDER; ++current_order) {//由指定的夥伴管理算法鏈表order階開始
		area = &(zone->free_area[current_order]);
		page = list_first_entry_or_null(&area->free_list[migratetype],
							struct page, lru);
		if (!page)//說明這裏沒有空閒區域可以分配
			continue;
		list_del(&page->lru);//將空閒頁面塊通過list_del()從鏈表中摘除出來
		rmv_page_order(page);/*移除page中order的變量*/
		area->nr_free--;/*空閒塊數減一*/
		expand(zone, page, order, current_order, area, migratetype);//將其對等拆分開
		set_pcppage_migratetype(page, migratetype);
		return page;
	}
	return NULL;
}

函數很簡單,實現了夥伴算法當核心功能:
首先for()循環其由指定的夥伴管理算法鏈表order階開始,如果該階的鏈表不爲空,則直接通過list_del()從該鏈表中獲取空閒頁面以滿足申請需要;
如果該階的鏈表爲空,則往更高一階的鏈表查找,直到找到鏈表不爲空的一階,直到找到了最高階仍爲空鏈表,則申請失敗;
若在找到鏈表不爲空的一階後,將空閒頁面塊通過list_del()從鏈表中摘除出來,然後通過expand()將其對等拆分切割,並將拆分出來的一半空閒部分插入更低order的鏈表中,直到拆分至恰好滿足申請需要的order階,最後將得到的滿足要求的頁面返回回去。
至此,頁面已經分配到了。

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;
	while (high > low) {//low表示所需塊大小,high表示實際大小
		area--;//先把一般的內存頁恢復,對應的free_area[n]也要減少,這樣纔可以訪問到其free_list鏈表
		high--;//面說的current_order的值,我們也要相應減少
		size >>= 1;//內存塊減半切割
		VM_BUG_ON_PAGE(bad_range(zone, &page[size]), &page[size]);//看看size這麼大內存區域的首頁是否在該內存zoon的範圍下,不是則系統崩潰

		if (set_page_guard(zone, &page[size], high, migratetype))
			continue;

		list_add(&page[size].lru, &area->free_list[migratetype]);//如果無誤,我們就把這一段空間恢復爲空閒區域中
		area->nr_free++;/*空閒塊加一*/
		set_page_order(&page[size], high);/*設置相關order*/
	}
}

low對應所屬頁面塊大小的order,high對應當前空閒zone的order,噹噹前分配到的頁面塊大於所需的內存塊,那就將內存塊對半切割,對應參數也自減一,然後標記爲保護頁。
如果可以友好釋放,那就將該頁面插入低一級的order鏈表,否則繼續切割內存塊。

如果__rmqueue_smallest不能分配到page當話,則會調用__rmqueue_fallback函數分配。
其主要是向其他遷移類型中獲取內存。較正常的夥伴算法不同,不是在本節點zone區域分配,而是在fallback鏈表中。
其向遷移類型的內存申請內存頁面時,是從最高階開始查找的,主要是從大塊內存中申請可以避免更少的碎片。如果嘗試完所有的手段仍無法獲得內存頁面,則會從MIGRATE_RESERVE列表中獲取。


參考:【Linux 內核】內存管理(二)夥伴算法
Linux物理內存頁面分配
linux夥伴系統接口alloc_page分析1裏面還分析了__alloc_pages_slowpath函數

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