深入理解 Linux 物理記憶體分配全鏈路實現

来源:https://www.cnblogs.com/binlovetech/archive/2023/01/02/17019710.html
-Advertisement-
Play Games

前文回顧 在上篇文章 《深入理解 Linux 物理記憶體管理》中,筆者詳細的為大家介紹了 Linux 內核如何對物理記憶體進行管理以及相關的一些內核數據結構。 在介紹物理記憶體管理之前,筆者先從 CPU 的角度開始,介紹了三種 Linux 物理記憶體模型:FLATMEM 平坦記憶體模型,DISCONTIGME ...


前文回顧

在上篇文章 《深入理解 Linux 物理記憶體管理》中,筆者詳細的為大家介紹了 Linux 內核如何對物理記憶體進行管理以及相關的一些內核數據結構。

在介紹物理記憶體管理之前,筆者先從 CPU 的角度開始,介紹了三種 Linux 物理記憶體模型:FLATMEM 平坦記憶體模型,DISCONTIGMEM 非連續記憶體模型,SPARSEMEM 稀疏記憶體模型。

image

image

image

隨後筆者又帶大家站在一個新的視角上,把物理記憶體看做成一個整體,從 CPU 訪問物理記憶體以及 CPU 與物理記憶體的相對位置變化的角度介紹了兩種物理記憶體架構:一致性記憶體訪問 UMA 架構,非一致性記憶體訪問 NUMA 架構。

image

image

在 NUMA 架構下,只有 DISCONTIGMEM 非連續記憶體模型和 SPARSEMEM 稀疏記憶體模型是可用的。而 UMA 架構下,前面介紹的三種記憶體模型可以配置使用。

無論是 NUMA 架構還是 UMA 架構在內核中都是使用相同的數據結構來組織管理的,在內核的記憶體管理模塊中會把 UMA 架構當做只有一個 NUMA 節點的偽 NUMA 架構。

image

這樣一來這兩種架構模式就在內核中被統一管理起來,我們基於這個事實,深入剖析了內核針對 NUMA 架構下用於物理記憶體管理的相關數據結構:struct pglist_data (NUMA 節點),struct zone(物理記憶體區域),struct page(物理頁)。

image

上圖展示的是在 NUMA 架構下,NUMA 節點與物理記憶體區域 zone 以及物理記憶體頁 page 之間的層次關係。

物理記憶體被劃分成了一個一個的記憶體節點(NUMA 節點),在每個 NUMA 節點內部又將其所管理的物理記憶體按照功能不同劃分成了不同的記憶體區域 zone ,每個記憶體區域 zone 管理一片用於具體功能的物理記憶體頁 page,而內核會為每一個記憶體區域分配一個伙伴系統用於管理該記憶體區域下物理記憶體頁 page 的分配和釋放。

物理記憶體在內核中管理的層級關係為:None -> Zone -> page

在上篇文章的最後,筆者又花了大量的篇幅來為大家介紹了 struct page 結構,我們瞭解了內核如何通過 struct page 結構來描述物理記憶體頁,這個結構是內核中最為複雜的一個結構體,因為它是物理記憶體管理的最小單位,被頻繁應用在內核中的各種複雜機制下。

通過以上內容的介紹,筆者覺得大家已經在架構層面上對 Linux 物理記憶體管理有了一個較為深刻的認識,現在物理記憶體管理的架構我們已經建立起來了,那麼內核如何根據這個架構層次來分配物理記憶體呢?

為了給大家梳理清楚內核分配物理記憶體的過程及其涉及到的各個重要模塊,於是就有了本文的內容~~

image

1. 內核物理記憶體分配介面

image

在為大家介紹物理記憶體分配之前,筆者先來介紹下內核中用於物理記憶體分配的幾個核心介面,這幾個物理記憶體分配介面全部是基於伙伴系統的,伙伴系統有一個特點就是它所分配的物理記憶體頁全部都是物理上連續的,並且只能分配 2 的整數冪個頁,這裡的整數冪在內核中稱之為分配階。

下麵要介紹的這些物理記憶體分配介面均需要指定這個分配階,意思就是從伙伴系統申請多少個物理記憶體頁,假設我們指定分配階為 order,那麼就會從伙伴系統中申請 2 的 order 次冪個物理記憶體頁。

內核中提供了一個 alloc_pages 函數用於分配 2 的 order 次冪個物理記憶體頁,參數中的 unsigned int order 表示向底層伙伴系統指定的分配階,參數 gfp_t gfp 是內核中定義的一個用於規範物理記憶體分配行為的修飾符,這裡我們先不展開,後面的小節中筆者會詳細為大家介紹。

struct page *alloc_pages(gfp_t gfp, unsigned int order);

alloc_pages 函數用於向底層伙伴系統申請 2 的 order 次冪個物理記憶體頁組成的記憶體塊,該函數返回值是一個 struct page 類型的指針用於指向申請的記憶體塊中第一個物理記憶體頁。

alloc_pages 函數用於分配多個連續的物理記憶體頁,在內核的某些記憶體分配場景中有時候並不需要分配這麼多的連續記憶體頁,而是只需要分配一個物理記憶體頁即可,於是內核又提供了 alloc_page 巨集,用於這種單記憶體頁分配的場景,我們可以看到其底層還是依賴了 alloc_pages 函數,只不過 order 指定為 0。

#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)

當系統中空閑的物理記憶體無法滿足記憶體分配時,就會導致記憶體分配失敗,alloc_pages,alloc_page 就會返回空指針 NULL 。

vmalloc 分配機制底層就是用的 alloc_page

在物理記憶體分配成功的情況下, alloc_pages,alloc_page 函數返回的都是指向其申請的物理記憶體塊第一個物理記憶體頁 struct page 指針。

大家可以直接理解成返回的是一塊物理記憶體,而 CPU 可以直接訪問的卻是虛擬記憶體,所以內核又提供了一個函數 __get_free_pages ,該函數直接返回物理記憶體頁的虛擬記憶體地址。用戶可以直接使用。

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);

__get_free_pages 函數在使用方式上和 alloc_pages 是一樣的,函數參數的含義也是一樣,只不過一個是返回物理記憶體頁的虛擬記憶體地址,一個是直接返回物理記憶體頁。

事實上 __get_free_pages 函數的底層也是基於 alloc_pages 實現的,只不過多了一層虛擬地址轉換的工作。

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
	struct page *page;
    // 不能在高端記憶體中分配物理頁,因為無法直接映射獲取虛擬記憶體地址
	page = alloc_pages(gfp_mask & ~__GFP_HIGHMEM, order);
	if (!page)
		return 0;
    // 將直接映射區中的物理記憶體頁轉換為虛擬記憶體地址
	return (unsigned long) page_address(page);
}

page_address 函數用於將給定的物理記憶體頁 page 轉換為它的虛擬記憶體地址,不過這裡只適用於內核虛擬記憶體空間中的直接映射區,因為在直接映射區中虛擬記憶體地址到物理記憶體地址是直接映射的,虛擬記憶體地址減去一個固定的偏移就可以直接得到物理記憶體地址。

如果物理記憶體頁處於高端記憶體中,則不能這樣直接進行轉換,在通過 alloc_pages 函數獲取物理記憶體頁 page 之後,需要調用 kmap 映射將 page 映射到內核虛擬地址空間中。

image

忘記這塊內容的同學,可以在回看下筆者之前的文章 《深入理解虛擬記憶體管理》中的 “ 7.1.4 永久映射區 ” 小節。

同 alloc_page 函數一樣,內核也提供了 __get_free_page 用於只分配單個物理記憶體頁的場景,底層還是依賴於 __get_free_pages 函數,參數 order 指定為 0 。

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

無論是 alloc_pages 也好還是 __get_free_pages 也好,它們申請到的記憶體頁中包含的數據在一開始都不是空白的,而是內核隨機產生的一些垃圾信息,但其實這些信息可能並不都是完全隨機的,很有可能隨機的包含一些敏感的信息。

這些敏感的信息可能會被一些黑客所利用,並對電腦系統產生一些危害行為,所以從使用安全的角度考慮,內核又提供了一個函數 get_zeroed_page,顧名思義,這個函數會將從伙伴系統中申請到記憶體頁全部初始化填充為 0 ,這在分配物理記憶體頁給用戶空間使用的時候非常有用。

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

get_zeroed_page 函數底層也依賴於 __get_free_pages,指定的分配階 order 也是 0,表示從伙伴系統中只申請一個物理記憶體頁並初始化填充 0 。

除此之外,內核還提供了一個 __get_dma_pages 函數,專門用於從 DMA 記憶體區域分配適用於 DMA 的物理記憶體頁。其底層也是依賴於 __get_free_pages 函數。

unsigned long __get_dma_pages(gfp_t gfp_mask, unsigned int order);

這些底層依賴於 __get_free_pages 的物理記憶體分配函數,在遇到記憶體分配失敗的情況下都會返回 0 。

以上介紹的物理記憶體分配函數,分配的均是在物理上連續的記憶體頁。

當然了,有記憶體的分配就會有記憶體的釋放,所以內核還提供了兩個用於釋放物理記憶體頁的函數:

void __free_pages(struct page *page, unsigned int order);
void free_pages(unsigned long addr, unsigned int order);
  • __free_pages : 同 alloc_pages 函數對應,用於釋放一個或者 2 的 order 次冪個記憶體頁,釋放的物理記憶體區域起始地址由該區域中的第一個 page 實例指針表示,也就是參數里的 struct page *page 指針。

  • free_pages:同 __get_free_pages 函數對應,與 __free_pages 函數的區別是在釋放物理記憶體時,使用了虛擬記憶體地址而不是 page 指針。

在釋放記憶體時需要非常謹慎小心,我們只能釋放屬於你自己的記憶體頁,傳遞了錯誤的 struct page 指針或者錯誤的虛擬記憶體地址,或者傳遞錯了 order 值,都可能會導致系統的崩潰。在內核空間中,內核是完全信賴自己的,這點和用戶空間不同。

另外內核也提供了 __free_page 和 free_page 兩個巨集,專門用於釋放單個物理記憶體頁。

#define __free_page(page) __free_pages((page), 0)
#define free_page(addr) free_pages((addr), 0)

到這裡,關於內核中對於物理記憶體分配和釋放的介面,筆者就為大家交代完了,但是大家可能會有一個疑問,就是我們在介紹 alloc_pages 和 __get_free_pages 函數的時候,它們的參數中都有 gfp_t gfp_mask,之前筆者簡單的提過這個 gfp_mask 掩碼:它是內核中定義的一個用於規範物理記憶體分配行為的掩碼。

那麼這個掩碼究竟規範了哪些物理記憶體的分配行為 ?並對物理記憶體的分配有哪些影響呢 ?大家跟著筆者的節奏繼續往下看~~~

2.規範物理記憶體分配行為的掩碼 gfp_mask

筆者在 《深入理解 Linux 物理記憶體管理》一文中的 “ 4.3 NUMA 節點物理記憶體區域的劃分 ” 小節中曾經為大家詳細的介紹了 NUMA 節點中物理記憶體區域 zone 的劃分。

筆者在文章中提到,由於實際的電腦體繫結構受到硬體方面的制約,間接限制了頁框的使用方式。於是內核會根據不同的物理記憶體區域的功能不同,將 NUMA 節點內的物理記憶體劃分為:ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_HIGHMEM 這幾個物理記憶體區域。

ZONE_MOVABLE 區域是內核從邏輯上的劃分,該區域中的物理記憶體頁面來自於上述幾個記憶體區域,目的是避免記憶體碎片和支持記憶體熱插拔

image

當我們調用上小節中介紹的那幾個物理記憶體分配介面時,比如:alloc_pages 和 __get_free_pages。就會遇到一個問題,就是我們申請的這些物理記憶體到底來自於哪個物理記憶體區域 zone,假如我們想要從指定的物理記憶體區域中申請記憶體,我們該如何告訴內核呢 ?

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

這時,這些物理記憶體分配介面中的 gfp_t 參數就派上用場了,首碼 gfp 是 get free page 的縮寫,意思是在獲取空閑物理記憶體頁的時候需要指定的分配掩碼 gfp_mask。

gfp_mask 中的低 4 位用來表示應該從哪個物理記憶體區域 zone 中獲取記憶體頁 page。

image

gfp_mask 掩碼中這些區域修飾符 zone modifiers 定義在內核 /include/linux/gfp.h 文件中:

#define ___GFP_DMA		    0x01u
#define ___GFP_HIGHMEM		0x02u
#define ___GFP_DMA32		0x04u
#define ___GFP_MOVABLE		0x08u

大家這裡可能會感到好奇,為什麼沒有定義 ___GFP_NORMAL 的掩碼呢?

這是因為內核對物理記憶體的分配主要是落在 ZONE_NORMAL 區域中,如果我們不指定物理記憶體的分配區域,那麼內核會預設從 ZONE_NORMAL 區域中分配記憶體,如果 ZONE_NORMAL 區域中的空閑記憶體不夠,內核則會降級到 ZONE_DMA 區域中分配。

關於物理記憶體分配的區域降級策略,筆者在前面的文章《深入理解 Linux 物理記憶體管理》的 “ 5.1 物理記憶體區域中的預留記憶體 ” 小節中已經詳細地為大家介紹過了,但是之前的介紹只是停留在理論層面,那麼這個物理記憶體區域降級策略是在哪裡實現的呢?接下來的內容筆者就為大家揭曉~~~

內核在 /include/linux/gfp.h 文件中定義了一個叫做 gfp_zone 的函數,這個函數用於將我們在物理記憶體分配介面中指定的 gfp_mask 掩碼轉換為物理記憶體區域,返回的這個物理記憶體區域是記憶體分配的最高級記憶體區域,如果這個最高級記憶體區域不足以滿足記憶體分配的需求,則按照 ZONE_HIGHMEM -> ZONE_NORMAL -> ZONE_DMA 的順序依次降級。

static inline enum zone_type gfp_zone(gfp_t flags)
{
	enum zone_type z;
	int bit = (__force int) (flags & GFP_ZONEMASK);

	z = (GFP_ZONE_TABLE >> (bit * GFP_ZONES_SHIFT)) &
					 ((1 << GFP_ZONES_SHIFT) - 1);
	VM_BUG_ON((GFP_ZONE_BAD >> bit) & 1);
	return z;
}

上面的這個 gfp_zone 函數是在內核 5.19 版本中的實現,在高版本的實現中用大量的移位操作替換了低版本中的實現,目的是為了提高程式的性能,但是帶來的卻是可讀性的大幅下降。

筆者寫到這裡覺得給大家分析清楚每一步移位操作的實現對大家理解這個函數的主幹邏輯並沒有什麼實質意義上的幫助,並且和本文主題偏離太遠,所以我們退回到低版本 2.6.24 中的實現,在這一版中直擊 gfp_zone 函數原本的面貌。

static inline enum zone_type gfp_zone(gfp_t flags)
{
	int base = 0;

#ifdef CONFIG_NUMA
	if (flags & __GFP_THISNODE)
		base = MAX_NR_ZONES;
#endif

#ifdef CONFIG_ZONE_DMA
	if (flags & __GFP_DMA)
		return base + ZONE_DMA;
#endif
#ifdef CONFIG_ZONE_DMA32
	if (flags & __GFP_DMA32)
		return base + ZONE_DMA32;
#endif
	if ((flags & (__GFP_HIGHMEM | __GFP_MOVABLE)) ==
			(__GFP_HIGHMEM | __GFP_MOVABLE))
		return base + ZONE_MOVABLE;
#ifdef CONFIG_HIGHMEM
	if (flags & __GFP_HIGHMEM)
		return base + ZONE_HIGHMEM;
#endif
    // 預設從 normal 區域中分配記憶體
	return base + ZONE_NORMAL;
}

我們看到在內核 2.6.24 版本中的 gfp_zone 函數實現邏輯就非常的清晰了,核心邏輯主要如下:

  • 只要掩碼 flags 中設置了 __GFP_DMA,則不管 __GFP_HIGHMEM 有沒有設置,記憶體分配都只會在 ZONE_DMA 區域中分配。

  • 如果掩碼只設置了 ZONE_HIGHMEM,則在物理記憶體分配時,優先在 ZONE_HIGHMEM 區域中進行分配,如果容量不夠則降級到 ZONE_NORMAL 中,如果還是不夠則進一步降級至 ZONE_DMA 中分配。

  • 如果掩碼既沒有設置 ZONE_HIGHMEM 也沒有設置 __GFP_DMA,則走到最後的分支,預設優先從 ZONE_NORMAL 區域中進行記憶體分配,如果容量不夠則降級至 ZONE_DMA 區域中分配。

  • 單獨設置 __GFP_MOVABLE 其實並不會影響內核的分配策略,我們如果想要讓內核在 ZONE_MOVABLE 區域中分配記憶體需要同時指定 __GFP_MOVABLE 和 __GFP_HIGHMEM 。

ZONE_MOVABLE 只是內核定義的一個虛擬記憶體區域,目的是避免記憶體碎片和支持記憶體熱插拔。上述介紹的 ZONE_HIGHMEM,ZONE_NORMAL,ZONE_DMA 才是真正的物理記憶體區域,ZONE_MOVABLE 虛擬記憶體區域中的物理記憶體來自於上述三個物理記憶體區域。

在 32 位系統中 ZONE_MOVABLE 虛擬記憶體區域中的物理記憶體頁來自於 ZONE_HIGHMEM。

在64 位系統中 ZONE_MOVABLE 虛擬記憶體區域中的物理記憶體頁來自於 ZONE_NORMAL 或者 ZONE_DMA 區域。

下麵是不同的 gfp_t 掩碼設置方式與其對應的記憶體區域降級策略彙總列表:

gfp_t 掩碼 記憶體區域降級策略
什麼都沒有設置 ZONE_NORMAL -> ZONE_DMA
__GFP_DMA ZONE_DMA
__GFP_DMA & __GFP_HIGHMEM ZONE_DMA
__GFP_HIGHMEM ZONE_HIGHMEM -> ZONE_NORMAL -> ZONE_DMA

除了上述介紹 gfp_t 掩碼中的這四個物理記憶體區域修飾符之外,內核還定義了一些規範記憶體分配行為的修飾符,這些行為修飾符並不會限制內核從哪個物理記憶體區域中分配記憶體,而是會限制物理記憶體分配的行為,那麼具體會限制哪些記憶體分配的行為呢?讓我們接著往下看~~~

這些記憶體分配行為修飾符同樣也是定義在 /include/linux/gfp.h 文件中:

#define ___GFP_RECLAIMABLE	0x10u
#define ___GFP_HIGH		0x20u
#define ___GFP_IO		0x40u
#define ___GFP_FS		0x80u
#define ___GFP_ZERO		0x100u
#define ___GFP_ATOMIC		0x200u
#define ___GFP_DIRECT_RECLAIM	0x400u
#define ___GFP_KSWAPD_RECLAIM	0x800u
#define ___GFP_NOWARN		0x2000u
#define ___GFP_RETRY_MAYFAIL	0x4000u
#define ___GFP_NOFAIL		0x8000u
#define ___GFP_NORETRY		0x10000u
#define ___GFP_HARDWALL		0x100000u
#define ___GFP_THISNODE		0x200000u
#define ___GFP_MEMALLOC		0x20000u
#define ___GFP_NOMEMALLOC	0x80000u
  • ___GFP_RECLAIMABLE 用於指定分配的頁面是可以回收的,___GFP_MOVABLE 則是用於指定分配的頁面是可以移動的,這兩個標誌會影響底層的伙伴系統從哪個區域中去獲取空閑記憶體頁,這塊內容我們會在後面講解伙伴系統的時候詳細介紹。

  • ___GFP_HIGH 表示該記憶體分配請求是高優先順序的,內核急切的需要記憶體,如果記憶體分配失敗則會給系統帶來非常嚴重的後果,設置該標誌通常記憶體是不允許分配失敗的,如果空閑記憶體不足,則會從緊急預留記憶體中分配。

關於物理記憶體區域中的緊急預留記憶體相關內容,筆者在之前文章 《深入理解 Linux 物理記憶體管理》一文中的 “ 5.1 物理記憶體區域中的預留記憶體 ” 小節中已經詳細介紹過了。

  • ___GFP_IO 表示內核在分配物理記憶體的時候可以發起磁碟 IO 操作。什麼意思呢?比如當內核在進行記憶體分配的時候,發現物理記憶體不足,這時需要將不經常使用的記憶體頁置換到 SWAP 分區或者 SWAP 文件中,這時就涉及到了 IO 操作,如果設置了該標誌,表示允許內核將不常用的記憶體頁置換出去。

  • ___GFP_FS 允許內核執行底層文件系統操作,在與 VFS 虛擬文件系統層相關聯的內核子系統中必須禁用該標誌,否則可能會引起文件系統操作的迴圈遞歸調用,因為在設置 ___GFP_FS 標誌分配記憶體的情況下,可能會引起更多的文件系統操作,而這些文件系統的操作可能又會進一步產生記憶體分配行為,這樣一直遞歸持續下去。

  • ___GFP_ZERO 在內核分配記憶體成功之後,將記憶體頁初始化填充位元組 0 。

  • ___GFP_ATOMIC 該標誌的設置表示記憶體在分配物理記憶體的時候不允許睡眠必須是原子性地進行記憶體分配。比如在中斷處理程式中,就不能睡眠,因為中斷程式不能被重新調度。同時也不能在持有自旋鎖的進程上下文中睡眠,因為可能導致死鎖。綜上所述這個標誌只能用在不能被重新安全調度的進程上下文中

  • ___GFP_DIRECT_RECLAIM 表示內核在進行記憶體分配的時候,可以進行直接記憶體回收。當剩餘記憶體容量低於水位線 _watermark[WMARK_MIN] 時,說明此時的記憶體容量已經非常危險了,如果進程在這時請求記憶體分配,內核就會進行直接記憶體回收,直到記憶體水位線恢復到 _watermark[WMARK_HIGH] 之上。

image

  • ___GFP_KSWAPD_RECLAIM 表示內核在分配記憶體的時候,如果剩餘記憶體容量在 _watermark[WMARK_MIN] 與 _watermark[WMARK_LOW] 之間時,內核就會喚醒 kswapd 進程開始非同步記憶體回收,直到剩餘記憶體高於 _watermark[WMARK_HIGH] 為止。

  • ___GFP_NOWARN 表示當內核分配記憶體失敗時,抑制內核的分配失敗錯誤報告。

  • ___GFP_RETRY_MAYFAIL 在內核分配記憶體失敗的時候,允許重試,但重試仍然可能失敗,重試若幹次後停止。與其對應的是 ___GFP_NORETRY 標誌表示分配記憶體失敗時不允許重試。

  • ___GFP_NOFAIL 在內核分配失敗時一直重試直到成功為止。

  • ___GFP_HARDWALL 該標誌限制了內核分配記憶體的行為只能在當前進程分配到的 CPU 所關聯的 NUMA 節點上進行分配,當進程可以運行的 CPU 受限時,該標誌才會有意義,如果進程允許在所有 CPU 上運行則該標誌沒有意義。

  • ___GFP_THISNODE 該標誌限制了內核分配記憶體的行為只能在當前 NUMA 節點或者在指定 NUMA 節點中分配記憶體,如果記憶體分配失敗不允許從其他備用 NUMA 節點中分配記憶體。

  • ___GFP_MEMALLOC 允許內核在分配記憶體時可以從所有記憶體區域中獲取記憶體,包括從緊急預留記憶體中獲取。但使用該標示時需要保證進程在獲得記憶體之後會很快的釋放掉記憶體不會過長時間的占用,尤其要警惕避免過多的消耗緊急預留記憶體區域中的記憶體。

  • ___GFP_NOMEMALLOC 標誌用於明確禁止內核從緊急預留記憶體中獲取記憶體。___GFP_NOMEMALLOC 標識的優先順序要高於 ___GFP_MEMALLOC

好了到現在為止,我們已經知道了 gfp_t 掩碼中包含的記憶體區域修飾符以及記憶體分配行為修飾符,是不是感覺頭有點大了,事實上確實很讓人頭大,因為內核在不同場景下會使用不同的組合,這麼多的修飾符總是以組合的形式出現,如果我們每次使用的時候都需要單獨指定,那就會非常繁雜也很容易出錯。

於是內核將各種標準情形下用到的 gfp_t 掩碼組合,提前為大家定義了一些標準的分組,方便大家直接使用。

#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)
#define GFP_NOIO	(__GFP_RECLAIM)
#define GFP_NOFS	(__GFP_RECLAIM | __GFP_IO)
#define GFP_USER	(__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
#define GFP_DMA		__GFP_DMA
#define GFP_DMA32	__GFP_DMA32
#define GFP_HIGHUSER	(GFP_USER | __GFP_HIGHMEM)
  • GFP_ATOMIC 是掩碼 __GFP_HIGH,__GFP_ATOMIC,__GFP_KSWAPD_RECLAIM 的組合,表示記憶體分配行為必須是原子的,是高優先順序的。在任何情況下都不允許睡眠,如果空閑記憶體不夠,則會從緊急預留記憶體中分配。該標誌適用於中斷程式,以及持有自旋鎖的進程上下文中。

  • GFP_KERNEL 是內核中最常用的標誌,該標誌設置之後內核的分配記憶體行為可能會阻塞睡眠,可以允許內核置換出一些不活躍的記憶體頁到磁碟中。適用於可以重新安全調度的進程上下文中。

  • GFP_NOIO 和 GFP_NOFS 分別禁止內核在分配記憶體時進行磁碟 IO 和 文件系統 IO 操作。

  • GFP_USER 用於映射到用戶空間的記憶體分配,通常這些記憶體可以被內核或者硬體直接訪問,比如硬體設備會將 Buffer 直接映射到用戶空間中

  • GFP_DMA 和 GFP_DMA32 表示需要從 ZONE_DMA 和 ZONE_DMA32 記憶體區域中獲取適用於 DMA 的記憶體頁。

  • GFP_HIGHUSER 用於給用戶空間分配高端記憶體,因為在用戶虛擬記憶體空間中,都是通過頁表來訪問非直接映射的高端記憶體區域,所以用戶空間一般使用的是高端記憶體區域 ZONE_HIGHMEM。

現在我們算是真正理解了,在本小節開始時,介紹的那幾個記憶體分配介面函數中關於記憶體分配掩碼 gfp_mask 的所有內容,其中包括用於限制內核從哪個記憶體區域中分配記憶體,內核在分配記憶體過程中的行為,以及內核在各種標準分配場景下預先定義的掩碼組合。

這時我們在回過頭來看內核中關於物理記憶體分配的這些介面函數是不是感覺瞭如指掌了:

struct page *alloc_pages(gfp_t gfp, unsigned int order)
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
unsigned long get_zeroed_page(gfp_t gfp_mask)
unsigned long __get_dma_pages(gfp_t gfp_mask, unsigned int order)

好了,現在我們已經清楚了這些記憶體分配介面的使用,那麼這些介面又是如何實現的呢 ?讓我們再一次深入到內核源碼中去探索內核到底是如何分配物理記憶體的~~

3. 物理記憶體分配內核源碼實現

本文基於內核 5.19 版本討論

在介紹 Linux 內核關於記憶體分配的源碼實現之前,我們需要先找到記憶體分配的入口函數在哪裡,在上小節中為大家介紹的眾多記憶體分配介面的依賴層級關係如下圖所示:

image

我們看到記憶體分配的任務最終會落在 alloc_pages 這個介面函數中,在 alloc_pages 中會調用 alloc_pages_node 進而調用 __alloc_pages_node 函數,最終通過 __alloc_pages 函數正式進入內核記憶體分配的世界~~

__alloc_pages 函數為 Linux 內核記憶體分配的核心入口函數

static inline struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)
{
	return alloc_pages_node(numa_node_id(), gfp_mask, order);
}
static inline struct page *
__alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order)
{
    // 校驗指定的 NUMA 節點 ID 是否合法,不要越界
    VM_BUG_ON(nid < 0 || nid >= MAX_NUMNODES);
    // 指定節點必須是有效線上的
    VM_WARN_ON((gfp_mask & __GFP_THISNODE) && !node_online(nid));

    return __alloc_pages(gfp_mask, order, nid, NULL);
}

__alloc_pages_node 函數參數中的 nid 就是我們在上篇文章 《深入理解 Linux 物理記憶體管理》的 “ 4.1 內核如何統一組織 NUMA 節點 ” 小節介紹的 NUMA 節點 id。

image

內核使用了一個大小為 MAX_NUMNODES 的全局數組 node_data[] 來管理所有的 NUMA 節點,數組的下標即為 NUMA 節點 Id 。

#ifdef CONFIG_NUMA
extern struct pglist_data *node_data[];
#define NODE_DATA(nid)		(node_data[(nid)])

這裡指定 nid 是為了告訴內核應該在哪個 NUMA 節點上分配記憶體,我們看到在
alloc_pages 函數中通過 numa_node_id() 獲取運行當前進程的 CPU 所在的 NUMA 節點。並通過 !node_online(nid) 確保指定的 NUMA 節點是有效線上的。

關於 NUMA 節點的狀態信息,大家可回看上篇文章的 《4.5 NUMA 節點的狀態 node_states》小節。

image

3.1 記憶體分配行為標識掩碼 ALLOC_*

在我們進入 __alloc_pages 函數之前,筆者先來為大家介紹幾個影響內核分配記憶體行為的標識,這些重要標識定義在內核文件 /mm/internal.h 中:

#define ALLOC_WMARK_MIN     WMARK_MIN
#define ALLOC_WMARK_LOW     WMARK_LOW
#define ALLOC_WMARK_HIGH    WMARK_HIGH
#define ALLOC_NO_WATERMARKS 0x04 /* don't check watermarks at all */

#define ALLOC_HARDER         0x10 /* try to alloc harder */
#define ALLOC_HIGH       0x20 /* __GFP_HIGH set */
#define ALLOC_CPUSET         0x40 /* check for correct cpuset */

#define ALLOC_KSWAPD        0x800 /* allow waking of kswapd, __GFP_KSWAPD_RECLAIM set */

我們先來看前四個標識記憶體水位線的常量含義,這四個記憶體水位線標識表示內核在分配記憶體時必須考慮記憶體的水位線,在不同的水位線下記憶體的分配行為也會有所不同。

筆者在上篇文章 《深入理解 Linux 物理記憶體管理》的 “ 5.2 物理記憶體區域中的水位線 ” 小節中曾詳細地介紹了各個水位線的含義以及在不同水位線下記憶體分配的不同表現。

上篇文章中我們提到,內核會為 NUMA 節點中的每個物理記憶體區域 zone 定製三條用於指示記憶體容量的水位線,它們分別是:WMARK_MIN(頁最小閾值), WMARK_LOW (頁低閾值),WMARK_HIGH(頁高閾值)。

這三個水位線定義在 /include/linux/mmzone.h 文件中:

enum zone_watermarks {
	WMARK_MIN,
	WMARK_LOW,
	WMARK_HIGH,
	NR_WMARK
};

三條水位線對應的 watermark 具體數值存儲在每個物理記憶體區域 struct zone 結構中的 _watermark[NR_WMARK] 數組中。

struct zone {
    // 物理記憶體區域中的水位線
    unsigned long _watermark[NR_WMARK];
}

物理記憶體區域中不同水位線的含義以及記憶體分配在不同水位線下的行為如下圖所示:

image

  • 當該物理記憶體區域的剩餘記憶體容量高於 _watermark[WMARK_HIGH] 時,說明此時該物理記憶體區域中的記憶體容量非常充足,記憶體分配完全沒有壓力。

  • 當剩餘記憶體容量在 _watermark[WMARK_LOW] 與_watermark[WMARK_HIGH] 之間時,說明此時記憶體有一定的消耗但是還可以接受,能夠繼續滿足進程的記憶體分配需求。

  • 當剩餘記憶體容量在 _watermark[WMARK_MIN] 與 _watermark[WMARK_LOW] 之間時,說明此時記憶體容量已經有點危險了,記憶體分配面臨一定的壓力,但是還可以滿足進程此時的記憶體分配要求,當給進程分配完記憶體之後,就會喚醒 kswapd 進程開始記憶體回收,直到剩餘記憶體高於 _watermark[WMARK_HIGH] 為止。

在這種情況下,進程的記憶體分配會觸發記憶體回收,但請求進程本身不會被阻塞,由內核的 kswapd 進程非同步回收記憶體。

  • 當剩餘記憶體容量低於 _watermark[WMARK_MIN] 時,說明此時的記憶體容量已經非常危險了,如果進程在這時請求記憶體分配,內核就會進行直接記憶體回收,這時記憶體回收的任務將會由請求進程同步完成。

註意:上面提到的物理記憶體區域 zone 的剩餘記憶體是需要刨去 lowmem_reserve 預留記憶體大小(用於緊急記憶體分配)。也就是說 zone 里被伙伴系統所管理的記憶體並不包含 lowmem_reserve 預留記憶體。

好了,在我們重新回顧了記憶體分配行為在這三條水位線:_watermark[WMARK_HIGH],_watermark[WMARK_LOW],watermark[WMARK_MIN] 下的不同表現之後,我們在回過來看本小節開始處提到的那幾個 ALLOC* 記憶體分配標識。

ALLOC_NO_WATERMARKS 表示在記憶體分配過程中完全不會考慮上述三個水位線的影響。

ALLOC_WMARK_HIGH 表示在記憶體分配的時候,當前物理記憶體區域 zone 中剩餘記憶體頁的數量至少要達到 _watermark[WMARK_HIGH] 水位線,才能進行記憶體的分配。

ALLOC_WMARK_LOW 和 ALLOC_WMARK_MIN 要表達的記憶體分配語義也是一樣,當前物理記憶體區域 zone 中剩餘記憶體頁的數量至少要達到水位線 _watermark[WMARK_LOW] 或者 _watermark[WMARK_MIN],才能進行記憶體的分配。

ALLOC_HARDER 表示在記憶體分配的時候,會放寬記憶體分配規則的限制,所謂的放寬規則就是降低 _watermark[WMARK_MIN] 水位線,努力使記憶體分配最大可能成功。

當我們在 gfp_t 掩碼中設置了 ___GFP_HIGH 時,ALLOC_HIGH 標識才起作用,該標識表示當前記憶體分配請求是高優先順序的,內核急切的需要記憶體,如果記憶體分配失敗則會給系統帶來非常嚴重的後果,設置該標誌通常記憶體是不允許分配失敗的,如果空閑記憶體不足,則會從緊急預留記憶體中分配。

ALLOC_CPUSET 表示記憶體只能在當前進程所允許運行的 CPU 所關聯的 NUMA 節點中進行分配。比如使用 cgroup 限制進程只能在某些特定的 CPU 上運行,那麼進程所發起的記憶體分配請求,只能在這些特定 CPU 所在的 NUMA 節點中進行。

ALLOC_KSWAPD 表示允許喚醒 NUMA 節點中的 KSWAPD 進程,非同步進行記憶體回收。

內核會為每個 NUMA 節點分配一個 kswapd 進程用於回收不經常使用的頁面。

typedef struct pglist_data {
        .........
    // 頁面回收進程
    struct task_struct *kswapd;
        ..........
} pg_data_t;

3.2 記憶體分配的心臟 __alloc_pages

好了,在為大家介紹完這些影響記憶體分配行為的相關標識掩碼:GFP_*ALLOC_* 之後,下麵就該來介紹本文的主題——物理記憶體分配的核心函數 __alloc_pages ,從下麵內核源碼的註釋中我們可以看出,這個函數正是伙伴系統的核心心臟,它是內核記憶體分配的核心入口函數,整個記憶體分配的完整過程全部封裝在這裡。

該函數的邏輯比較複雜,因為在記憶體分配過程中需要涉及處理各種 GFP_*ALLOC_* 標識,然後根據上述各種標識的含義來決定記憶體分配該如何進行。所以大家需要多點耐心,一步一步跟著筆者的思路往下走~~~

/*
 * This is the 'heart' of the zoned buddy allocator.
 */
struct page *__alloc_pages(gfp_t gfp, unsigned int order, int preferred_nid,
                            nodemask_t *nodemask)
{
    // 用於指向分配成功的記憶體
    struct page *page;
    // 記憶體區域中的剩餘記憶體需要在 WMARK_LOW 水位線之上才能進行記憶體分配,否則失敗(初次嘗試快速記憶體分配)
    unsigned int alloc_flags = ALLOC_WMARK_LOW;
    // 之前小節中介紹的記憶體分配掩碼集合
    gfp_t alloc_gfp; 
    // 用於在不同記憶體分配輔助函數中傳遞參數
    struct alloc_context ac = { };

    // 檢查用於向伙伴系統申請記憶體容量的分配階 order 的合法性
    // 內核定義最大分配階 MAX_ORDER -1 = 10,也就是說一次最多只能從伙伴系統中申請 1024 個記憶體頁。
    if (WARN_ON_ONCE_GFP(order >= MAX_ORDER, gfp))
        return NULL;
    // 表示在記憶體分配期間進程可以休眠阻塞
    gfp &= gfp_allowed_mask;

    alloc_gfp = gfp;
    // 初始化 alloc_context,併為接下來的快速記憶體分配設置相關 gfp
    if (!prepare_alloc_pages(gfp, order, preferred_nid, nodemask, &ac,
            &alloc_gfp, &alloc_flags))
        // 提前判斷本次記憶體分配是否能夠成功,如果不能則儘早失敗
        return NULL;

    // 避免記憶體碎片化的相關分配標識設置,可暫時忽略
    alloc_flags |= alloc_flags_nofragment(ac.preferred_zoneref->zone, gfp);

    // 記憶體分配快速路徑:第一次嘗試從底層伙伴系統分配記憶體,註意此時是在 WMARK_LOW 水位線之上分配記憶體
    page = get_page_from_freelist(alloc_gfp, order, alloc_flags, &ac);
    if (likely(page))
        // 如果記憶體分配成功則直接返回
        goto out;
    // 流程走到這裡表示記憶體分配在快速路徑下失敗
    // 這裡需要恢復最初的記憶體分配標識設置,後續會嘗試更加激進的記憶體分配策略
    alloc_gfp = gfp;
    // 恢復最初的 node mask 因為它可能在第一次記憶體分配的過程中被改變
    // 本函數中 nodemask 起初被設置為 null
    ac.nodemask = nodemask;

    // 在第一次快速記憶體分配失敗之後,說明記憶體已經不足了,內核需要做更多的工作
    // 比如通過 kswap 回收記憶體,或者直接記憶體回收等方式獲取更多的空閑記憶體以滿足記憶體分配的需求
    // 所以下麵的過程稱之為慢速分配路徑
    page = __alloc_pages_slowpath(alloc_gfp, order, &ac);

out:
    // 記憶體分配成功,直接返回 page。否則返回 NULL
    return page;
}

__alloc_pages 函數中的記憶體分配整體邏輯如下:

  • 首先內核會嘗試在記憶體水位線 WMARK_LOW 之上快速的進行一次記憶體分配。這一點我們從開始的 unsigned int alloc_flags = ALLOC_WMARK_LOW 語句中可以看得出來。

image

  • 校驗本次記憶體分配指定伙伴系統的分配階 order 的有效性,伙伴系統在內核中的最大分配階定義在 /include/linux/mmzone.h 文件中,最大分配階 MAX_ORDER -1 = 10,也就是說一次最多只能從伙伴系統中申請 1024 個記憶體頁,對應 4M 大小的連續物理記憶體。
/* Free memory management - zoned buddy allocator.  */
#ifndef CONFIG_FORCE_MAX_ZONEORDER
#define MAX_ORDER 11
  • 調用 prepare_alloc_pages 初始化 alloc_context ,用於在不同記憶體分配輔助函數中傳遞記憶體分配參數。為接下來即將進行的快速記憶體分配做準備。
struct alloc_context {
    // 運行進程 CPU 所在 NUMA  節點以及其所有備用 NUMA 節點中允許記憶體分配的記憶體區域
    struct zonelist *zonelist;
    // NUMA  節點狀態掩碼
    nodemask_t *nodemask;
    // 記憶體分配優先順序最高的記憶體區域 zone
    struct zoneref *preferred_zoneref;
    // 物理記憶體頁的遷移類型分為:不可遷移,可回收,可遷移類型,防止記憶體碎片
    int migratetype;

    // 記憶體分配最高優先順序的記憶體區域 zone
    enum zone_type highest_zoneidx;
    // 是否允許當前 NUMA 節點中的臟頁均衡擴散遷移至其他 NUMA 節點
    bool spread_dirty_pages;
};
  • 調用 get_page_from_freelist 方法首次嘗試在伙伴系統中進行記憶體分配,這次記憶體分配比較快速,只是快速的掃描一下各個記憶體區域中是否有足夠的空閑記憶體能夠滿足本次記憶體分配,如果有則立馬從伙伴系統中申請,如果沒有立即返回, page 設置為 null,進行後續慢速記憶體分配處理。

這裡需要註意的是:首次嘗試的快速記憶體分配是在 WMARK_LOW 水位線之上進行的。

  • 當快速記憶體分配失敗之後,情況就會變得非常複雜,內核將不得不做更多的工作,比如開啟 kswapd 進程非同步記憶體回收,更極端的情況則需要進行直接記憶體回收,或者直接記憶體整理以獲取更多的空閑連續記憶體。這一切的複雜邏輯全部封裝在 __alloc_pages_slowpath 函數中。

alloc_pages_slowpath 函數複雜在於需要結合前邊小節中介紹的 GFP*,ALLOC* 這些記憶體分配標識,根據不同的標識進入不同的記憶體分配邏輯分支,涉及到的情況比較繁雜。這裡大家只需要簡單瞭解,後面筆者會詳細介紹~~~

以上介紹的 __alloc_pages 函數記憶體分配邏輯以及與對應的記憶體水位線之間的關係如下圖所示:

image

總體流程介紹完之後,我們接著來看一下以上記憶體分配過程涉及到的三個重要記憶體分配輔助函數:prepare_alloc_pages,__alloc_pages_slowpath,get_page_from_freelist 。

3.3 prepare_alloc_pages

prepare_alloc_pages 初始化 alloc_context ,用於在不同記憶體分配輔助函數中傳遞記憶體分配參數,為接下來即將進行的快速記憶體分配做準備。

static inline bool prepare_alloc_pages(gfp_t gfp_mask, unsigned int order,
        int preferred_nid, nodemask_t *nodemask,
        struct alloc_context *ac, gfp_t *alloc_gfp,
        unsigned int *alloc_flags)
{
    // 根據 gfp_mask 掩碼中的記憶體區域修飾符獲取記憶體分配最高優先順序的記憶體區域 zone
    ac->highest_zoneidx = gfp_zone(gfp_mask);
    // 從 NUMA 節點的備用節點鏈表中一次性獲取允許進行記憶體分配的所有記憶體區域
    ac->zonelist = node_zonelist(preferred_nid, gfp_mask);
    ac->nodemask = nodemask;
    // 從 gfp_mask 掩碼中獲取頁面遷移屬性,遷移屬性分為:不可遷移,可回收,可遷移。這裡只需要簡單知道,後面在相關章節會細講
    ac->migratetype = gfp_migratetype(gfp_mask);

   // 如果使用 cgroup 將進程綁定限制在了某些 CPU 上,那麼記憶體分配只能在
   // 這些綁定的 CPU 相關聯的 NUMA 節點中進行
    if (cpusets_enabled()) {
        *alloc_gfp |= __GFP_HARDWALL;
        if (in_task() && !ac->nodemask)
            ac->nodemask = &cpuset_current_mems_allowed;
        else
            *alloc_flags |= ALLOC_CPUSET;
    }
      
    // 如果設置了允許直接記憶體回收,那麼記憶體分配進程則可能會導致休眠被重新調度 
    might_sleep_if(gfp_mask & __GFP_DIRECT_RECLAIM);
    // 提前判斷本次記憶體分配是否能夠成功,如果不能則儘早失敗
    if (should_fail_alloc_page(gfp_mask, order))
        return false;
    // 獲取最高優先順序的記憶體區域 zone
    // 後續記憶體分配則首先會在該記憶體區域中進行分配
    ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
                    ac->highest_zoneidx, ac->nodemask);

    return true;
}

prepare_alloc_pages 主要的任務就是在快速記憶體分配開始之前,做一些準備初始化的工作,其中最核心的就是從指定 NUMA 節點中,根據 gfp_mask 掩碼中的記憶體區域修飾符獲取可以進行記憶體分配的所有記憶體區域 zone (包括其他備用 NUMA 節點中包含的記憶體區域)。

之前筆者已經在 《深入理解 Linux 物理記憶體管理》一文中的 “ 4.3 NUMA 節點物理記憶體區域的劃分 ” 小節為大家已經詳細介紹了 NUMA 節點的數據結構 struct pglist_data。

struct pglist_data 結構中不僅包含了本 NUMA 節點中的所有記憶體區域,還包括了其他備用 NUMA 節點中的物理記憶體區域,當本節點中記憶體不足的情況下,內核會從備用 NUMA 節點中的記憶體區域進行跨節點記憶體分配。

typedef struct pglist_data {
    // NUMA 節點中的物理記憶體區域個數
    int nr_zones; 
    // NUMA 節點中的物理記憶體區域
    struct zone node_zones[MAX_NR_ZONES];
    // NUMA 節點的備用列表,其中包含了所有 NUMA 節點中的所有物理記憶體區域 zone,按照訪問距離由近到遠順序依次排列
    struct zonelist node_zonelists[MAX_ZONELISTS];
} pg_data_t;

我們可以根據 nid 和 gfp_mask 掩碼中的物理記憶體區域描述符利用 node_zonelist 函數一次性獲取允許進行記憶體分配的所有記憶體區域(所有 NUMA 節點)。

static inline struct zonelist *node_zonelist(int nid, gfp_t flags)
{
	return NODE_DATA(nid)->node_zonelists + gfp_zonelist(flags);
}

4. 記憶體慢速分配入口 alloc_pages_slowpath

正如前邊小節我們提到的那樣,alloc_pages_slowpath 函數非常的複雜,其中包含了記憶體分配的各種異常情況的處理,並且會根據前邊介紹的 GFP_,ALLOC_ 等各種記憶體分配策略掩碼進行不同分支的處理,這樣就變得非常的龐大而繁雜。

alloc_pages_slowpath 函數包含了整個記憶體分配的核心流程,本身非常的繁雜龐大,為了能夠給大家清晰的梳理清楚這些複雜的記憶體分配流程,所以筆者決定還是以 總 - 分 - 總 的結構來給大家呈現。

下麵這段偽代碼是筆者提取出來的 alloc_pages_slowpath 函數的主幹框架,其中包含的一些核心分支以及核心步驟筆者都通過註釋的形式為大家標註出來了,這裡我先從總體上大概瀏覽下 alloc_pages_slowpath 主要分為哪幾個邏輯處理模塊,它們分別處理了哪些事情。

還是那句話,這裡大家只需要總體把握,不需要掌握每個細節,關於細節的部分,筆者後面會帶大家逐個擊破!!!

static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
                        struct alloc_context *ac)
{
        ......... 初始化慢速記憶體分配路徑下的相關參數 .......

retry_cpuset:

        ......... 調整記憶體分配策略 alloc_flags 採用更加激進方式獲取記憶體 ......
        ......... 此時記憶體分配主要是在進程所允許運行的 CPU 相關聯的 NUMA 節點上 ......
        ......... 記憶體水位線下調至 WMARK_MIN ...........
        ......... 喚醒所有 kswapd 進程進行非同步記憶體回收  ...........
        ......... 觸發直接記憶體整理 direct_compact 來獲取更多的連續空閑記憶體 ......

retry:

        ......... 進一步調整記憶體分配策略 alloc_flags 使用更加激進的非常手段進行記憶體分配 ...........
        ......... 在記憶體分配時忽略記憶體水位線 ...........
        ......... 觸發直接記憶體回收 direct_reclaim ...........
        ......... 再次觸發直接記憶體整理 direct_compact ...........
        ......... 最後的殺手鐧觸發 OOM 機制  ...........

nopage:
        ......... 經過以上激進的記憶體分配手段仍然無法滿足記憶體分配就會來到這裡 ......
        ......... 如果設置了 __GFP_NOFAIL 不允許記憶體分配失敗,則不停重試上述記憶體分配過程 ......

fail:
        ......... 記憶體分配失敗,輸出告警信息 ........

      warn_alloc(gfp_mask, ac->nodemask,
            "page allocation failure: order:%u", order);
got_pg:
        ......... 記憶體分配成功,返回新申請的記憶體塊 ........

      return page;
}

4.1 初始化記憶體分配慢速路徑下的相關參數

static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
                        struct alloc_context *ac)
{
    // 在慢速記憶體分配路徑中可能會導致內核進行直接記憶體回收
    // 這裡設置 __GFP_DIRECT_RECLAIM 表示允許內核進行直接記憶體回收
    bool can_direct_reclaim = gfp_mask & __GFP_DIRECT_RECLAIM;
    // 本次記憶體分配是否是針對大量記憶體頁的分配,內核定義 PAGE_ALLOC_COSTLY_ORDER = 3
    // 也就是說記憶體請求記憶體頁的數量大於 2 ^ 3 = 8 個記憶體頁時,costly_order = true,後續會影響是否進行 OOM
    const bool costly_order = order > PAGE_ALLOC_COSTLY_ORDER;
    // 用於指向成功申請的記憶體
    struct page *page = NULL;
    // 記憶體分配標識,後續會根據不同標識進入到不同的記憶體分配邏輯處理分支
    unsigned int alloc_flags;
    // 後續用於記錄直接記憶體回收了多少記憶體頁
    unsigned long did_some_progress;
    // 關於記憶體整理相關參數
    enum compact_priority compact_priority;
    enum compact_result compact_result;
    int compaction_retries;
    // 記錄重試的次數,超過一定的次數(16次)則記憶體分配失敗
    int no_progress_loops;
    // 臨時保存調整後的記憶體分配策略
    int reserve_flags;

    // 流程現在來到了慢速記憶體分配這裡,說明快速分配路徑已經失敗了
    // 內核需要對 gfp_mask 分配行為掩碼做一些修改,修改為一些更可能導致記憶體分配成功的標識
    // 因為接下來的直接記憶體回收非常耗時可能會導致進程阻塞睡眠,不適用原子 __GFP_ATOMIC 記憶體分配的上下文。
    if (WARN_ON_ONCE((gfp_mask & (__GFP_ATOMIC|__GFP_DIRECT_RECLAIM)) ==
                (__GFP_ATOMIC|__GFP_DIRECT_RECLAIM)))
        gfp_mask &= ~__GFP_ATOMIC;

retry_cpuset:

retry:

nopage:

fail:

got_pg:

}

在內核進入慢速記憶體分配路徑之前,首先會在這裡初始化後續記憶體分配需要的參數,由於筆者已經在各個欄位上標註了豐富的註釋,所以這裡筆者只對那些難以理解的核心參數為大家進行相關細節的鋪墊,這裡大家對這些參數有個大概印象即可,後續在使用到的時候,筆者還會再次提起~~~

首先我們看 costly_order 參數,order 表示底層伙伴系統的分配階,內核只能向伙伴系統申請 2 的 order 次冪個記憶體頁,costly 從字面意思上來說表示有一定代價和消耗的,costly_order 連起來就表示在內核中 order 分配階達到多少,在內核看來就是代價比較大的記憶體分配行為。

這個臨界值就是 PAGE_ALLOC_COSTLY_ORDER 定義在 /include/linux/mmzone.h 文件中:

#define PAGE_ALLOC_COSTLY_ORDER 3

也就是說在內核看來,當請求記憶體頁的數量大於 2 ^ 3 = 8 個記憶體頁時,costly_order = true,內核就認為本次記憶體分配是一次成本比較大的行為。後續會根據這個參數 costly_order 來決定是否觸發 OOM 。

    const bool costly_order = order > PAGE_ALLOC_COSTLY_ORDER;

當記憶體嚴重不足的時候,內核會開啟直接記憶體回收 direct_reclaim ,參數 did_some_progress 表示經過一次直接記憶體回收之後,內核回收了多少個記憶體頁。這個參數後續會影響是否需要進行記憶體分配重試。

no_progress_loops 用於記錄記憶體分配重試的次數,如果記憶體分配重試的次數超過最大限制 MAX_RECLAIM_RETRIES,則停止重試,開啟 OOM。

MAX_RECLAIM_RETRIES 定義在 /mm/internal.h 文件中:

#define MAX_RECLAIM_RETRIES 16

compact_* 相關的參數用於直接記憶體整理 direct_compact,內核通常會在直接記憶體回收 direct_reclaim 之前進行一次 direct_compact,如果經過 direct_compact 整理之後有了足夠多的空間記憶體就不需要進行 direct_reclaim 了。

那麼這個 direct_compact 到底是乾什麼的呢?它在慢速記憶體分配過程起了什麼作用?

隨著系統的長時間運行通常會伴隨著不同大小的物理記憶體頁的分配和釋放,這種不規則的分配釋放,隨著系統的長時間運行就會導致記憶體碎片,記憶體碎片會使得系統在明明有足夠記憶體的情況下,依然無法為進程分配合適的記憶體。

image

如上圖所示,假如現在系統一共有 16 個物理記憶體頁,當前系統只是分配了 3 個物理頁,那麼在當前系統中還剩餘 13 個物理記憶體頁的情況下,如果內核想要分配 8 個連續的物理頁由於記憶體碎片的存在則會分配失敗。(只能分配最多 4 個連續的物理頁)

內核中請求分配的物理頁面數只能是 2 的次冪!!

為瞭解決記憶體碎片化的問題,內核將記憶體頁面分為了:可移動的,可回收的,不可移動的三種類型。

可移動的頁面聚集在一起,可回收的的頁面聚集在一起,不可移動的的頁面聚集也在一起。從而作為去碎片化的基礎, 然後進行成塊回收。

在回收時把可回收的一起回收,把可移動的一起移動,從而能空出大量連續物理頁面。direct_compact 會掃描記憶體區域 zone 里的頁面,把已分配的頁記錄下來,然後把所有已分配的頁移動到 zone 的一端,這樣就會把一個已經充滿碎片的 zone 整理成一段完全未分配的區間和一段已經分配的區間,從而騰出大塊連續的物理頁面供內核分配。

image

4.2 retry_cpuset

在介紹完了記憶體分配在慢速路徑下所需要的相關參數之後,下麵就正式來到了 alloc_pages_slowpath 的記憶體分配邏輯:

static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
                        struct alloc_context *ac)
{
        ......... 初始化慢速記憶體分配路徑下的相關參數 .......

retry_cpuset:

    // 在之前的快速記憶體分配路徑下設置的相關分配策略比較保守,不是很激進,用於在 WMARK_LOW 水位線之上進行快速記憶體分配
    // 走到這裡表示快速記憶體分配失敗,此時空閑記憶體嚴重不足了
    // 所以在慢速記憶體分配路徑下需要重新設置更加激進的記憶體分配策略,採用更大的代價來分配記憶體
    alloc_flags = gfp_to_alloc_flags(gfp_mask);

    // 重新按照新的設置按照記憶體區域優先順序計算 zonelist 的迭代起點(最高優先順序的 zone)
    // fast path 和 slow path 的設置不同所以這裡需要重新計算
    ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
                    ac->highest_zoneidx, ac->nodemask);
    // 如果沒有合適的記憶體分配區域,則跳轉到 nopage , 記憶體分配失敗
    if (!ac->preferred_zoneref->zone)
        goto nopage;
    // 喚醒所有的 kswapd 進程非同步回收記憶體
    if (alloc_flags & ALLOC_KSWAPD)
        wake_all_kswapds(order, gfp_mask, ac);

    // 此時所有的 kswapd 進程已經被喚醒,正在非同步進行記憶體回收
    // 之前我們已經在 gfp_to_alloc_flags 方法中重新調整了 alloc_flags
    // 換成了一套更加激進的記憶體分配策略,註意此時是在 WMARK_MIN 水位線之上進行記憶體分配
    // 調整後的 alloc_flags 很可能會立即成功,因此這裡先嘗試一下
    page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
    if (page)
        // 記憶體分配成功,跳轉到 got_pg 直接返回 page
        goto got_pg;

    // 對於分配大記憶體來說 costly_order = true (超過 8 個記憶體頁),需要首先進行記憶體整理,這樣內核可以避免直接記憶體回收從而獲取更多的連續空閑記憶體頁
    // 對於需要分配不可移動的高階記憶體的情況,也需要先進行記憶體整理,防止永久記憶體碎片
    if (can_direct_reclaim &&
            (costly_order ||
               (order > 0 && ac->migratetype != MIGRATE_MOVABLE))
            && !gfp_pfmemalloc_allowed(gfp_mask)) {
        // 進行直接記憶體整理,獲取更多的連續空閑記憶體防止記憶體碎片
        page = __alloc_pages_direct_compact(gfp_mask, order,
                        alloc_flags, ac,
                        INIT_COMPACT_PRIORITY,
                        &compact_result);
        if (page)
            goto got_pg;

        if (costly_order && (gfp_mask & __GFP_NORETRY)) {
            // 流程走到這裡表示經過記憶體整理之後依然沒有足夠的記憶體供分配
            // 但是設置了 NORETRY 標識不允許重試,那麼就直接失敗,跳轉到 nopage
            if (compact_result == COMPACT_SKIPPED ||
                compact_result == COMPACT_DEFERRED)
                goto nopage;
            // 同步記憶體整理開銷太大,後續開啟非同步記憶體整理
            compact_priority = INIT_COMPACT_PRIORITY;
        }
    }

retry:

nopage:

fail:

got_pg:
    return page;
}

流程走到這裡,說明內核在 《3.2 記憶體分配的心臟 __alloc_pages》小節中介紹的快速路徑下嘗試的記憶體分配已經失敗了,所以才會走到慢速分配路徑這裡來。

之前我們介紹到快速分配路徑是在 WMARK_LOW 水位線之上進行記憶體分配,與其相配套的記憶體分配策略比較保守,目的是快速的在各個記憶體區域 zone 之間搜索可供分配的空閑記憶體。

image

快速分配路徑下的失敗意味著此時系統中的空閑記憶體已經不足了,所以在慢速分配路徑下內核需要改變記憶體分配策略,採用更加

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • RequestMappingHandlerAdapter是Spring Web MVC中針對@Controller和@RequestMapping體系的處理器適配器,本文對RequestMappingHandlerAdapter的組成、初始化以及同步請求處理流程進行詳細梳理和總結。 ...
  • 家居網購項目實現012 以下皆為部分代碼,詳見 https://github.com/liyuelian/furniture_mall.git 29.功能27-Ajax檢驗註冊名 29.1需求分析/圖解 用戶註冊時,後端通過驗證,提示用戶當前輸入的用戶名是否可用。 29.2思路分析 29.3代碼實現 ...
  • JZ76 刪除鏈表中重覆的結點 題目 在一個排序的鏈表中,存在重覆的結點,請刪除該鏈表中重覆的結點,重覆的結點不保留,返回鏈表頭指針。 例如,鏈表 1->2->3->3->4->4->5 處理後為 1->2->5 方法1 哈希表進行刪除 思路 演算法實現 LinkedHashMap實現順序插入,不過查 ...
  • C++中有左值和右值的概念。其實,左值和右值的區分也同樣適用於類對象,本文中將左值的類對象稱為左值對象,將右值的類對象稱為右值對象。 1. C++11:引用限定符 預設情況下,對於類中用 public 修飾的成員函數,既可以被左值對象調用,也可以被右值對象調用。舉個例子: #include <ios ...
  • 目前項目當中存有 .NET Framework 和 .NET Core 兩種類型的項目,但是都需要進行容器化將其分別部署在 Windows 集群和 Linux 集群當中。在 WCF 進行容器化的時候,遇到了以下幾個問題: 1. 某些服務使用到了 WSHttpBinding 保護服務安全,要在容器里... ...
  • 今天在寫一個通訊錄實現程式的時候,遇到個讓我突然卡殼的問題,不知道怎麼進行兩個結構體之間的成員互換......結構體成員有“姓名”,“性別”,“年齡”,“地址”,“電話”,目的就是實現一個通過年齡進行sort排序的功能,作為一個努力學習的編程小白來說,有太多的東西需要學習了..........代碼如 ...
  • 大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家介紹的是i.MXRT下FlexSPI driver實現Flash編程時對於中斷支持問題。 前段時間有客戶在官方社區反映 i.MXRT1170 下使用官方 SDK 里 FlexSPI 驅動去擦寫 Flash 時不能很好地支持全局中斷。客戶項目里用 ...
  • 合宙ESP32C3 + VSCode + OpenOCD調試經歷 環境 Windows10 VSCode + ESP-IDF 合宙ESP32C3(無串口晶元版本) 理論 想要直接使用內置JTAG,USB要求連接GPIO18和GPIO19 合宙ESP32C3有串口晶元版本USB經過CH343連接的串口 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...