linux 頁框管理(三) 每cpu頁幀快取

kuraxii發表於2024-04-14

每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。0hot pages1cold 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_flagshot 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有效)。

  1. 計算申請的頁框是冷快取還是熱快取

    int cold = !!(gfp_flags & __GFP_COLD);
    
  2. 首先判斷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;
    }
    
    
  3. 驗證沒有分配成功 或者 申請的頁框數 order > 0,則從夥伴系統中區分配頁框塊

    if (page == NULL) {
    	spin_lock_irqsave(&zone->lock, flags);
    	page = __rmqueue(zone, order);
    	spin_unlock_irqrestore(&zone->lock, flags);
    }
    
  4. 如果在上一步還是分配失敗了,就返回NULL,否則 初始化第一個頁框的頁描述符
    清除一些標誌,將private欄位置0,並將引用計數置1

     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);
     }
    

釋放頁框到每cpu頁幀快取

釋放頁框到每cpu頁幀快取,核心使用free_hot_pagefree_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 的執行流程如下

  1. 從page的flag欄位獲取當前頁框的管理區
    struct zone *zone = page_zone(page);
    
  2. 獲取由cold標誌所選擇的 per_cpu_pages 的地址
    pcp = &zone->pageset[get_cpu()].pcp[cold];
    
  3. 判斷 count 與 high的值,如果count >= high,就釋放掉batch個頁框,返回給夥伴系統
    if (pcp->count >= pcp->high)
    	pcp->count -= free_pages_bulk(zone, pcp->batch, &pcp->list, 0);
    
  4. 將當前頁框新增到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);

  1. 架構相關的釋放準備 arch_free_page
    可能涉及到特定架構需要在實體記憶體頁面被釋放回記憶體池之前做的準備工作,例如清理或重置與該頁面相關的硬體特定資料(如TLB條目或其他快取機制)。這是一個預防性的步驟,確保頁面在重新分配前不保留舊資料的痕跡或配置。
  2. 更新核心頁表 kernel_map_pages
    在這裡是用來在核心的地址空間中取消對映該頁面。這樣做的目的是防止釋放後的頁面被意外訪問,從而可能導致安全問題或資料錯誤。在頁面分配時,頁面會自動對映到需要的地址空間中,所以在分配時不需要顯式呼叫取消對映。
  3. 釋放時的安全檢查 free_pages_check
    這一呼叫是用來在釋放頁面之前進行一系列的完整性和一致性檢查,這是為了確保釋放的頁面不會導致未定義的行為或核心崩潰。
  4. 效能最佳化
    頁面分配時,通常關注的是如何快速有效地找到一個足夠的頁面來滿足請求。這個過程中,核心會盡量減少對頁面的操作以提高效率。相反,在頁面釋放時,進行更多的清理和安全檢查是有益的,因為這可以為未來的分配提供一個更穩定和可靠的環境。
  5. 分配與釋放的不對稱
    記憶體分配與釋放在作業系統中往往是不對稱的。分配時,系統的目標是儘快滿足請求,而釋放時則更注重徹底清理和正確歸還資源。這就解釋了為什麼在釋放過程中會有額外的步驟,而在分配時則可能沒有相應的對稱操作。

相關文章