前言
Linux核心中是如何分配出頁面的,如果我們站在CPU的角度去看這個問題,CPU能分配出來的頁面是以物理頁面為單位的。也就是我們計算機中常講的分頁機制。本文就看下Linux核心是如何管理,釋放和分配這些物理頁面的。
夥伴演算法
夥伴系統的定義
大家都知道,Linux核心的頁面分配器的基本演算法是基於夥伴系統的,夥伴系統通俗的講就是以2^order
分配記憶體的。這些記憶體塊我們就稱為夥伴。
何為夥伴
-
兩個塊大小相同
-
兩個塊地址連續
-
兩個塊必須是同一個大塊分離出來的
下面我們舉個例子理解夥伴分配演算法。假設我們要管理一大塊連續的記憶體,有64個頁面,假設現在來了一個請求,要分配8個頁面。總不能把64個頁面全部給他使用吧。
首先把64個頁面一切為二,每部分32個頁面。
把32個頁面給請求者還是很大,這個時候會繼續拆分為16個。
最後會將16個頁面繼續拆分為8個,將其返回給請求者,這就完成了第一個請求。
這個時候,第二個請求者也來了,同樣的請求8個頁面,這個時候系統就會把另外8個頁面返回給請求者。
假設現在有第三個請求者過來了,它請求4個頁面。這個時候之前的8個頁面都被分配走了,這個時候就要從16個頁面的記憶體塊切割了,切割後變為每份8個頁面。最後將8個頁面的記憶體塊一分為二後返回給呼叫者。
假設前面分配的8個頁面都已經用完了,這個時候可以把兩個8個頁面合併為16個頁面。
以上例子就是夥伴系統的簡單的例子,大家可以透過這個例子通俗易懂的理解夥伴系統。
另外一個例子將要去說明三個條件中的第三個條件:兩個塊必須要是從同一個大塊中分離出來的,這兩個塊才能稱之為夥伴,才能去合併為一個大塊。
我們以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分割出來的。
假設,page0正在使用,page1 和 page2都是空閒的。那page1 和 page 2 可以合併成一個大的記憶體塊嗎?
我們從上下級的關係來看,page 1,page 2 並不屬於一個大記憶體塊切割而來的,不屬於夥伴關係。
如果我們把page 1 page 2,page4 page 5 合併了,看下結果會是什麼樣子。
page0和page3 就會變成大記憶體塊中孤零零的空洞了。page 0 和 page3 就無法再和其他塊合併了。這樣就形成了外碎片化。因此,核心的夥伴系統是極力避免這種清空發生的。
夥伴系統在核心中的實現
下面我們看下核心中是怎麼實現夥伴系統的。
上面這張圖是核心中早期夥伴系統的實現
核心中把記憶體以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_mem
在zone_ref
中排在zone_normal
的前面。而且,ZONE_NORMAL是線性對映的,線性對映的記憶體會優先給核心態使用。
頁面分配的時候從那個遷移型別中分配出記憶體呢?
函式static inline int gfp_migratetype(const gfp_t gfp_flags)
可以根據掩碼型別轉換出遷移型別,從那個遷移型別分配頁面。比如GFP_KERNEL是從UNMOVABLE型別分配頁面的。
ZONE水位
頁面分配器是基於ZONE的機制來實現的,怎麼去管理這些空閒頁面呢?Linux核心中定義了三個警戒線,WATERMARK_MIN
,WATERMARK_LOW
,WATERMARK_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