【記憶體管理】頁面分配機制

学习,积累,成长發表於2024-06-23

前言

Linux核心中是如何分配出頁面的,如果我們站在CPU的角度去看這個問題,CPU能分配出來的頁面是以物理頁面為單位的。也就是我們計算機中常講的分頁機制。本文就看下Linux核心是如何管理,釋放和分配這些物理頁面的。

夥伴演算法

夥伴系統的定義

大家都知道,Linux核心的頁面分配器的基本演算法是基於夥伴系統的,夥伴系統通俗的講就是以2^order 分配記憶體的。這些記憶體塊我們就稱為夥伴。

何為夥伴

  • 兩個塊大小相同

  • 兩個塊地址連續

  • 兩個塊必須是同一個大塊分離出來的

下面我們舉個例子理解夥伴分配演算法。假設我們要管理一大塊連續的記憶體,有64個頁面,假設現在來了一個請求,要分配8個頁面。總不能把64個頁面全部給他使用吧。

截圖_20240623133911

首先把64個頁面一切為二,每部分32個頁面。

截圖_20240623134103

把32個頁面給請求者還是很大,這個時候會繼續拆分為16個。

截圖_20240623134157

最後會將16個頁面繼續拆分為8個,將其返回給請求者,這就完成了第一個請求。

截圖_20240623134617

這個時候,第二個請求者也來了,同樣的請求8個頁面,這個時候系統就會把另外8個頁面返回給請求者。

截圖_20240623134828

假設現在有第三個請求者過來了,它請求4個頁面。這個時候之前的8個頁面都被分配走了,這個時候就要從16個頁面的記憶體塊切割了,切割後變為每份8個頁面。最後將8個頁面的記憶體塊一分為二後返回給呼叫者。

截圖_20240623134934

截圖_20240623135122

假設前面分配的8個頁面都已經用完了,這個時候可以把兩個8個頁面合併為16個頁面。

截圖_20240623135232

以上例子就是夥伴系統的簡單的例子,大家可以透過這個例子通俗易懂的理解夥伴系統。

另外一個例子將要去說明三個條件中的第三個條件:兩個塊必須要是從同一個大塊中分離出來的,這兩個塊才能稱之為夥伴,才能去合併為一個大塊。

我們以8個頁面的一個大塊為例子來說明,如圖A0所示。將A0一分為二分,分別為 B0,B1。

B0:4頁

B1:4頁

再將B0,B1繼續切分:

C0:2頁

C1:2頁

C2:2頁

C3:2頁

最後可以將C0,C1,C2,C3切分為1個頁面大小的記憶體塊。

我們從C列來看,C0,C1稱之為夥伴關係,C2,C3為夥伴關係。

同理,page0 和 page1也為夥伴關係,因為他們都是從C0分割出來的。

截圖_20240623140813

假設,page0正在使用,page1 和 page2都是空閒的。那page1 和 page 2 可以合併成一個大的記憶體塊嗎?

我們從上下級的關係來看,page 1,page 2 並不屬於一個大記憶體塊切割而來的,不屬於夥伴關係。

如果我們把page 1 page 2,page4 page 5 合併了,看下結果會是什麼樣子。

截圖_20240623141028

page0和page3 就會變成大記憶體塊中孤零零的空洞了。page 0 和 page3 就無法再和其他塊合併了。這樣就形成了外碎片化。因此,核心的夥伴系統是極力避免這種清空發生的。

夥伴系統在核心中的實現

下面我們看下核心中是怎麼實現夥伴系統的。

截圖_20240623143810

上面這張圖是核心中早期夥伴系統的實現

核心中把記憶體以2^order 為單位分為多個連結串列。order範圍為[0,MAX_ORDER-1],MAX_ORDER一般為11。因此,Linux核心中可以分配的最大的記憶體塊為2^10= 4M,術語叫做page block。

核心中有一個叫free_area的資料結構,這個資料結構為連結串列的陣列。陣列的大小為MAX_ORDER。陣列的每個成員為一個連結串列。分別表示對應order的空閒連結串列。以上就是早期的夥伴系統的頁面分配器的實現。

現在的夥伴系統中的頁面分配器的實現,為了解決記憶體碎片化的問題,在Linux核心2.6.4中引入了遷移型別的 演算法緩解記憶體碎片化的問題。

我們看這張圖,現在的頁面分配器中,每個free_area陣列成員中都增加了一個遷移型別。也就是說在每個order連結串列中多增加了一個連結串列。例如,order = 0 的連結串列中,新增了MOVABLE 連結串列,UNMOVABLE 連結串列,RECLAIMABLE連結串列。隨著核心的發展,遷移型別越來越多,但常用的就那三個。

遷移型別

在Linux核心2.6.4核心中引入了反碎片化的概念,反碎片化就是根據遷移型別來實現的。我們知道遷移型別 是根據page block來劃分的。我們看下常用的遷移型別。

  • MIGRATE_UNMOVABLE:在記憶體中有固定位置,不能隨意移動,比如核心分配的記憶體。那為什麼核心分配的不能遷移呢?因此要遷移頁面,首先要把物理頁面的對映關係斷開,在新的地方分配物理頁面,重新建立對映關係。在斷開對映關係的途中,如果核心繼續訪問這個頁面,會導致oop錯誤或者系統crash。因為核心是敏感區,核心必須保證它使用的記憶體是安全的。這一點和使用者程序不一樣。如果是使用者程序使用的記憶體,我們將其斷開後,使用者程序再去訪問,就會產生缺頁中斷,重新去尋找可用實體記憶體然後建立對映關係。

  • MIGRATE_MOVABLE:可以隨意移動,使用者態app分配的記憶體,mlock,mmap分配的 匿名頁面。

  • MIGRATE_RECLAIMABLE:不能移動可以刪除回收,比如檔案對映。

記憶體碎片化的產生

夥伴系統的遷移演算法可以解決一些碎片化的問題,但在記憶體管理的方面,長期存在一個問題。從系統啟動,長期執行之後,經過大量的分配-釋放過程,還是會產生很多碎片,下面我們看下,這些碎片是怎麼產生的。

我們以8個page的記憶體塊為例,假設page3是被核心使用的,比如alloc_page(GFP_KERNRL),所以它屬於不可移動的頁面,它就像一個樁一樣,插入在一大塊記憶體的中間。

儘管其他的頁面都是空閒頁面,導致page0 ~ page 7 不能合併為一個大塊的記憶體。

下面我們看下,遷移型別是怎麼解決這類問題的。我們知道,遷移演算法是以page block為單位工作的,一個page block大小就是頁面分配器能分配的最大記憶體塊。也就是說,一個page block 中的頁面都是屬於一個遷移型別的。所以,就不會存在上面說的多個page中夾著一個不可遷移的型別的情況。

頁面分配和釋放常用的函式

頁面分配函式

alloc_pages是核心中常用的分配實體記憶體頁面的函式, 用於分配2^order個連續的物理頁。

static inline struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)
  • gfp_mask:gfp的全稱是get free page, 因此gfp_mask表示頁面分配的方法。gfp_mask的具體分類後面我們會詳細介紹。
  • order:頁面分配器使用夥伴系統按照順序請求頁面分配。所以只能以2的冪記憶體分配。例如,請求order=3的頁面分配,最終會分配2 ^ 3 = 8頁。arm64當前預設MAX_ORDER為11, 即最多一次性分配2 ^(MAX_ORDER-1)個頁。
  • 返回值:返回指向第一個page的struct page指標

__get_free_page() 是頁面分配器提供給呼叫者的最底層的記憶體分配函式。它分配連續的實體記憶體。__get_free_page() 函式本身是基於 buddy 實現的。在使用 buddy 實現的實體記憶體管理中最小分配粒度是以頁為單位的。

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
  • 返回值:返回第一個page對映後的虛擬地址。
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)

alloc_page 是宏定義,邏輯是呼叫 alloc_pages,傳遞給 order 引數的值為 0,表示需要分配的物理頁個數為 2 的 0 次方,即 1 個物理頁,需要使用者傳遞引數 GFP flags。

釋放函式

void free_pages(unsigned long addr, unsigned int order)

釋放2^order大小的頁塊,傳入引數是頁框首地址的虛擬地址

#define __free_page(page) __free_pages((page), 0)

釋放一個頁,傳入引數是指向該頁對應的虛擬地址

#define free_page(addr) free_pages((addr), 0)

釋放一個頁,傳入引數是頁框首地址的虛擬地址

gfp_mask標誌位

行為修飾符

標誌 描述
GFP_WAIT 分配器可以睡眠
GFP_HIGH 分配器可以訪問緊急的記憶體池
GFP_IO 不能直接移動,但可以刪除
GFP_FS 分配器可以啟動檔案系統IO
GFP_REPEAT 在分配失敗的時候重複嘗試
GFP_NOFAIL 分配失敗的時候重複進行分配,直到分配成功位置
GFP_NORETRY 分配失敗時不允許再嘗試

zone 修飾符

標誌 描述
GFP_DMA 從ZONE_DMA中分配記憶體(只存在與X86)
GFP_HIGHMEM 可以從ZONE_HIGHMEM或者ZONE_NOMAL中分配

水位修飾符

標誌 描述
GFP_ATOMIC 分配過程中不允許睡眠,通常用作中斷處理程式、下半部、持有自旋鎖等不能睡眠的地方
GFP_KERNEL 常規的記憶體分配方式,可以睡眠
GFP_USER 常用於使用者程序分配記憶體
GFP_HIGHUSER 需要從ZONE_HIGHMEM開始進行分配,也是常用於使用者程序分配記憶體
GFP_NOIO 分配可以阻塞,但不會啟動磁碟IO
GFP_NOFS 可以阻塞,可以啟動磁碟,但不會啟動檔案系統操作

GFP_MASK和zone 以及遷移型別的關係

GFP_MASK除了表示分配行為之外,還可以表示從那些ZONE來分配記憶體。還可以確定從那些遷移型別的page block 分配記憶體。

我們以ARM為例,由於ARM架構沒有ZONE_DMA的記憶體,因此只能從ZONE_HIGHMEM或者ZONE_NOMAL中分配.

在核心中有兩個資料結構來表示從那些地方開始分配記憶體。

struct zonelist {
	struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
};struct zonelist

zonelist是一個zone的連結串列。一次分配的請求是在zonelist上執行的。開始在連結串列的第一個zone上分配,如果失敗,則根據優先順序降序訪問其他zone。
zlcache_ptr 指向zonelist的快取。為了加速對zonelist的讀取操作 ,用_zonerefs 儲存zonelist中每個zone的index。

struct zoneref {
	struct zone *zone;	/* Pointer to actual zone */
	int zone_idx;		/* zone_idx(zoneref->zone) */
};

頁面分配器是基於ZONE來設計的,因此頁面的分配有必要確定那些zone可以用於本次頁面分配。系統會優先使用ZONE_HIGHMEM,然後才是ZONE_NORMAL 。

基於zone 的設計思想,在分配物理頁面的時候理應以zone_hignmem優先,因為hign_memzone_ref中排在zone_normal的前面。而且,ZONE_NORMAL是線性對映的,線性對映的記憶體會優先給核心態使用。

頁面分配的時候從那個遷移型別中分配出記憶體呢?

函式static inline int gfp_migratetype(const gfp_t gfp_flags)可以根據掩碼型別轉換出遷移型別,從那個遷移型別分配頁面。比如GFP_KERNEL是從UNMOVABLE型別分配頁面的。

ZONE水位

頁面分配器是基於ZONE的機制來實現的,怎麼去管理這些空閒頁面呢?Linux核心中定義了三個警戒線,WATERMARK_MINWATERMARK_LOWWATERMARK_HIGH。大家可以看下面這張圖,就是分配水位和警戒線的關係。

  • 最低水線(WMARK_MIN):當剩餘記憶體在min以下時,則系統記憶體壓力非常大。一般情況下min以下的記憶體是不會被分配的,min以下的記憶體預設是保留給特殊用途使用,屬於保留的頁框,用於原子的記憶體請求操作。
    比如:當我們在中斷上下文申請或者在不允許睡眠的地方申請記憶體時,可以採用標誌GFP_ATOMIC來分配記憶體,此時才會允許我們使用保留在min水位以下的記憶體。
  • 低水線(WMARK_LOW):空閒頁數小數低水線,說明該記憶體區域的記憶體輕微不足。預設情況下,該值為WMARK_MIN的125%
  • 高水線(WMARK_HIGH):如果記憶體區域的空閒頁數大於高水線,說明該記憶體區域水線充足。預設情況下,該值為WMARK_MAX的150%

在進行記憶體分配的時候,如果分配器(比如buddy allocator)發現當前空餘記憶體的值低於”low”但高於”min”,說明現在記憶體面臨一定的壓力,那麼在此次記憶體分配完成後,kswapd將被喚醒,以執行記憶體回收操作。在這種情況下,記憶體分配雖然會觸發記憶體回收,但不存在被記憶體回收所阻塞的問題,兩者的執行關係是非同步的

對於kswapd來說,要回收多少記憶體才算完成任務呢?只要把空餘記憶體的大小恢復到”high”對應的watermark值就可以了,當然,這取決於當前空餘記憶體和”high”值之間的差距,差距越大,需要回收的記憶體也就越多。”low”可以被認為是一個警戒水位線,而”high”則是一個安全的水位線。

如果記憶體分配器發現空餘記憶體的值低於了”min”,說明現在記憶體嚴重不足。這裡要分兩種情況來討論,一種是預設的操作,此時分配器將同步等待記憶體回收完成,再進行記憶體分配,也就是direct reclaim。還有一種特殊情況,如果記憶體分配的請求是帶了PF_MEMALLOC標誌位的,並且現在空餘記憶體的大小可以滿足本次記憶體分配的需求,那麼也將是先分配,再回收。

per-cpu頁面分配

核心會經常請求和釋放單個頁框,比如網路卡驅動。

  • 頁面分配器分配和釋放頁面的時候需要申請一把鎖:zone->lock

    • 為了提高單個頁框的申請和釋放效率,核心建立了per-cpu頁面告訴快取池
    • 其中存放了若干預先分配好的頁框
  • 當請求單個頁框時,直接從本地cpu的頁框告訴快取池中獲取頁框

    • 不必申請鎖
    • 不必進行復雜的頁框分配操作

    體現了預先建立快取池的優勢,而且是每個CPU有一個獨立的快取池

per-cpu資料結構

由於頁框頻繁的分配和釋放,核心在每個zone中放置了一些事先保留的頁框。這些頁框只能由來自本地CPU的請求使用。zone中有一個成員pageset欄位指向per-cpu的快取記憶體,快取記憶體由struct 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 :buddy allocator增加或刪除的頁框數
  • lists:頁框連結串列。

本文參考

https://www.cnblogs.com/dennis-wong/p/14729453.html

https://blog.csdn.net/yhb1047818384/article/details/112298996

https://blog.csdn.net/u010923083/article/details/115916169

https://blog.csdn.net/farmwang/article/details/66975128

相關文章