每cpu頁幀快取 (The Per-CPU Page Frame Cache)
核心經常請求和釋放單個頁框。在這樣的場景下,頁的分配效率比較低。為了提升系統效能,記憶體管理區引入了每cpu葉幀快取(The Per-CPU Page Frame Cache
)。每個 cpu
的快取記憶體會預先快取一些單個頁框,用於該cpu申請單個頁框。從而避免了頻繁的訪問全域性頁表
實際上,每個cpu對於每個記憶體管理區都有兩個快取,一個 hot cache
,他的儲存位置很有可能在 cpu
硬體快取記憶體中 中, 一個 cold cache
。
總的來說 hot cache
更有可以存放在更高速級別儲存介質中。
資料結構
struct zone{
/* ... */
struct per_cpu_pageset pageset[NR_CPUS];
/* ... */
};
struct per_cpu_pageset {
struct per_cpu_pages pcp[2]; /* 0: hot. 1: cold */
} ____cacheline_aligned_in_smp;
struct per_cpu_pages {
int count; /* number of pages in the list */
int low; /* low watermark, refill needed */
int high; /* high watermark, emptying needed */
int batch; /* chunk size for buddy add/remove */
struct list_head list; /* the list of pages */
};
每 cpu
頁幀快取 的主要資料結構 是 zone
描述符中的pageset
欄位結構體陣列中的 per_cpu_pages
陣列。
pageset
陣列的大小為 NR_CPUS
,頁管理區為每個 cpu
都分配了一個 per_cpu_pages
。
per_cpu_pages陣列大小為2。0
為 hot pages
,1
為 cold pages
per_cpu_pages欄位描述
---型別--- | ---名稱--- | ---描述--- |
---|---|---|
int | count | 該連結串列中物理頁的個 |
int | low | 當連結串列中的物理頁個數低於該數值,會從zone buddy系統申請頁框 |
int | high | 當連結串列中的物理頁個數超過該數值,會將部分頁返還給zone buddy系統 |
int | batch | 每次返還給buddy系統的物理頁的個數 |
struct list_head | list | 快取記憶體中的頁框描述符連結串列 |
總的來說,透過 low high batch三個欄位來控制快取中的頁框數目
透過每CPU頁幀快取分配頁框
從每cpu頁幀快取申請頁框,核心使用buffered_rmqueue
函式,該函式有三個引數zone
記憶體管理區,order
請求的頁塊大小的對數,gfp_flags
從 hot cache
申請還是從hot cache
申請
static struct page *
buffered_rmqueue(struct zone *zone, int order, int gfp_flags)
{
unsigned long flags;
struct page *page = NULL;
int cold = !!(gfp_flags & __GFP_COLD);
if (order == 0) {
struct per_cpu_pages *pcp;
pcp = &zone->pageset[get_cpu()].pcp[cold];
local_irq_save(flags);
if (pcp->count <= pcp->low)
pcp->count += rmqueue_bulk(zone, 0,
pcp->batch, &pcp->list);
if (pcp->count) {
page = list_entry(pcp->list.next, struct page, lru);
list_del(&page->lru);
pcp->count--;
}
local_irq_restore(flags);
put_cpu();
}
if (page == NULL) {
spin_lock_irqsave(&zone->lock, flags);
page = __rmqueue(zone, order);
spin_unlock_irqrestore(&zone->lock, flags);
}
if (page != NULL) {
mod_page_state_zone(zone, pgalloc, 1 << order);
prep_new_page(page, order);
if (gfp_flags & __GFP_ZERO)
prep_zero_page(page, order, gfp_flags);
if (order && (gfp_flags & __GFP_COMP))
prep_compound_page(page, order);
}
return page;
}
buffered_rmqueue函式在指定的記憶體管理區分配頁框。使用cpu快取記憶體來分配單個頁框(分配單個頁框只對order=0有效)。
-
計算申請的頁框是冷快取還是熱快取
int cold = !!(gfp_flags & __GFP_COLD);
-
首先判斷oeder是否為0, 為 0 就從cpu頁幀快取中分配,否則跳過
if (order == 0) { struct per_cpu_pages *pcp; pcp = &zone->pageset[get_cpu()].pcp[cold]; // 如果count 小於 low,從夥伴系統申請頁框 if (pcp->count <= pcp->low) pcp->count += rmqueue_bulk(zone, 0, pcp->batch, &pcp->list); // 快取還有頁框則從快取分配 if (pcp->count) { // 分配頁框連結串列中的第一個頁框 page = list_entry(pcp->list.next, struct page, lru); list_del(&page->lru); // 頁幀快取頁框數減1 pcp->count--; } put_cpu(); // put_cpu get_cpu 函式對,禁止搶佔並確保程式碼塊在同一個CPU上執行,以保持處理的區域性性和防止資料結構的競爭狀態。 // 因為 cpu頁幀快取是每個cpu針對頁框的資料結構,操作過程中不能切換cpu } // rmqueue_bulk 迴圈分配大量大小為order的頁框 static int rmqueue_bulk(struct zone *zone, unsigned int order, unsigned long count, struct list_head *list) { unsigned long flags; int i; int allocated = 0; struct page *page; for (i = 0; i < count; ++i) { page = __rmqueue(zone, order); if (page == NULL) break; allocated++; list_add_tail(&page->lru, list); } return allocated; }
-
驗證沒有分配成功 或者 申請的頁框數 order > 0,則從夥伴系統中區分配頁框塊
if (page == NULL) { spin_lock_irqsave(&zone->lock, flags); page = __rmqueue(zone, order); spin_unlock_irqrestore(&zone->lock, flags); }
-
如果在上一步還是分配失敗了,就返回NULL,否則 初始化第一個頁框的頁描述符
清除一些標誌,將private欄位置0,並將引用計數置1if (page != NULL) { // 這是核心的一個統計函式 mod_page_state_zone(zone, pgalloc, 1 << order); prep_new_page(page, order); if (gfp_flags & __GFP_ZERO) prep_zero_page(page, order, gfp_flags); // 分配的是一個頁塊, 初始化頁塊 if (order && (gfp_flags & __GFP_COMP)) prep_compound_page(page, order); }
釋放頁框到每cpu頁幀快取
釋放頁框到每cpu頁幀快取,核心使用free_hot_page
和free_cold_page
函式,這兩個函式是free_hot_cold_page
的前端函式
void fastcall free_hot_page(struct page *page)
{
free_hot_cold_page(page, 0);
}
void fastcall free_cold_page(struct page *page)
{
free_hot_cold_page(page, 1);
}
static void fastcall free_hot_cold_page(struct page *page, int cold)
{
struct zone *zone = page_zone(page);
struct per_cpu_pages *pcp;
unsigned long flags;
arch_free_page(page, 0);
kernel_map_pages(page, 1, 0);
//inc_page_state(pgfree);
if (PageAnon(page))
page->mapping = NULL;
free_pages_check(__FUNCTION__, page);
pcp = &zone->pageset[get_cpu()].pcp[cold];
//local_irq_save(flags);
if (pcp->count >= pcp->high)
pcp->count -= free_pages_bulk(zone, pcp->batch, &pcp->list, 0);
list_add(&page->lru, &pcp->list);
pcp->count++;
//local_irq_restore(flags);
put_cpu();
}
free_hot_cold_page
的執行流程如下
- 從page的flag欄位獲取當前頁框的管理區
struct zone *zone = page_zone(page);
- 獲取由cold標誌所選擇的 per_cpu_pages 的地址
pcp = &zone->pageset[get_cpu()].pcp[cold];
- 判斷 count 與 high的值,如果count >= high,就釋放掉batch個頁框,返回給夥伴系統
if (pcp->count >= pcp->high) pcp->count -= free_pages_bulk(zone, pcp->batch, &pcp->list, 0);
- 將當前頁框新增到
per_cpu_pages
,並增加count
欄位list_add(&page->lru, &pcp->list); pcp->count++;
值得注意的是,在當前linux 2.6核心,沒有頁框會被釋放到冷快取中。對於硬體快取,核心總是假設被釋放的頁框放入hot cache
中。當然,著並不意味著cold cache
是空的,當到達low
下界時,會使用buffered_rmqueue
申請頁框。
一些疑問
為什麼在申請的時候沒有呼叫arch_free_page(page, 0); kernel_map_pages(page, 1, 0);對應的相關的函式,卻在釋放的時候區呼叫了arch_free_page(page, 0); kernel_map_pages(page, 1, 0);
- 架構相關的釋放準備
arch_free_page
可能涉及到特定架構需要在實體記憶體頁面被釋放回記憶體池之前做的準備工作,例如清理或重置與該頁面相關的硬體特定資料(如TLB條目或其他快取機制)。這是一個預防性的步驟,確保頁面在重新分配前不保留舊資料的痕跡或配置。 - 更新核心頁表
kernel_map_pages
在這裡是用來在核心的地址空間中取消對映該頁面。這樣做的目的是防止釋放後的頁面被意外訪問,從而可能導致安全問題或資料錯誤。在頁面分配時,頁面會自動對映到需要的地址空間中,所以在分配時不需要顯式呼叫取消對映。 - 釋放時的安全檢查
free_pages_check
這一呼叫是用來在釋放頁面之前進行一系列的完整性和一致性檢查,這是為了確保釋放的頁面不會導致未定義的行為或核心崩潰。 - 效能最佳化
在頁面分配時,通常關注的是如何快速有效地找到一個足夠的頁面來滿足請求。這個過程中,核心會盡量減少對頁面的操作以提高效率。相反,在頁面釋放時,進行更多的清理和安全檢查是有益的,因為這可以為未來的分配提供一個更穩定和可靠的環境。 - 分配與釋放的不對稱
記憶體分配與釋放在作業系統中往往是不對稱的。分配時,系統的目標是儘快滿足請求,而釋放時則更注重徹底清理和正確歸還資源。這就解釋了為什麼在釋放過程中會有額外的步驟,而在分配時則可能沒有相應的對稱操作。