1. 程式人生 > >深入理解 Linux 核心---記憶體管理

深入理解 Linux 核心---記憶體管理

核心中的函式以比較直接了當的方式獲得動態記憶體:

  • __get_free_pages() 或 alloc_pages() 從分割槽頁框分配器獲得頁框。
  • kmem_cache_alloc() 或 kmalloc() 使用 slab 分配器為專用或通用物件分配塊。
  • vmalloc() 或 vmalloc_32() 獲得一塊非連續的記憶體。

如果請求的記憶體區得以滿足,會返回一個頁描述符地址或線性地址。

在這裡插入圖片描述
RAM 的某些部分被永久地分配給核心,並用來存放核心程式碼及靜態核心資料結構。

RAM 的其餘部分稱為動態記憶體。整個系統的效能取決於如果有效地管理動態記憶體。

後續部分:
頁框管理和記憶體區管理會涉及對連續實體記憶體區處理的兩種不同技術。
非連續記憶體區管理涉及處理非連續記憶體區的技術。

頁框管理

Linux 採用 4KB 頁框大小作為標準的記憶體分配單元,有如下兩個原因:

  • 分頁單元引發的缺頁異常很容易得到解釋,或者是請求的頁存在但不允許程序對其訪問,或者是請求的頁不存在。
  • 主存和磁碟之間傳輸小塊資料時更高效。

頁描述符

頁框的狀態資訊儲存在一個型別為 page 的頁描述符中,長度為 32 位元組,所有的頁描述存放在 mem_map 陣列中。

virt_to_page(addr) 巨集產生線性地址 addr 對應的頁描述符。
pfn_to_page(pfn) 巨集產生與頁框號 pfn 對應的頁描述符地址。

頁描述符中的其中兩個欄位:

  • _count,頁的引用計數器。 -1:相應頁框空閒,並可被分配給任一程序或核心本身;>= 0:頁框被分配給了一個或多個程序,或用於存放一些核心資料結構。
    page_count() 返回 _count 加 1 後的值,即該頁的使用者數目。
  • flags,包含多達 32 個用來描述頁框狀態的標誌。

非一致記憶體訪問(NUMA)

系統的實體記憶體被劃分為幾個節點。一個單獨的節點內,任一給定 CPU 訪問所有頁面的時間是相同的,但對於不同 CPU,訪問不同頁面的時間可能不同。

每個節點都有一個型別為 pg_data_t 的描述符,存放於單向連結串列 pgdat_list。

每個節點內的實體記憶體可分為幾個區。

80x86 體系結構的兩種硬約束:

  • ISA 匯流排的 DMA 處理器只能對 RAM 的前 16MB 定址。
  • 大容量 RAM 的現代 32 位計算機中,因為線性地址空間太小,CPU 無法直接訪問所有實體記憶體。

為此,實體記憶體被劃分為 3 個區

  • ZONE_DMA,包含低於 16MB 的記憶體頁框
  • ZONE_NORMAL,包含高於 16MB 且低於 896MB 的記憶體頁框。
  • ZONE_HIGHMEM,包含從 896MB 開始高於 896MB 的記憶體頁框。

ZONE_DMA 和 ZONE_NORMAL 區包含記憶體的“常規”頁框,把它們線性對映到線性地址空間的第 4 個 GB後,核心就可以直接訪問。
ZONE_HIGHMEM 區包含的記憶體頁不能由核心直接訪問,儘管它們頁線性地對映到了線性地址空間的第 4 個 GB。64 位體系結構中,ZONE_HIGNMEM 為空。

每個區都有自己的描述符,其中許多欄位用於回收頁框。

每個頁描述符都有到記憶體節點和到節點區的連結。為節省空間,連結方式被編碼成索引存放在 flags 欄位的高位。

page_zone() 引數為頁描述符的地址,它讀取頁描述符中 flags 欄位的最高位,然後通過檢視 zone_table 陣列來確定相應區描述符的地址。在啟動時用所有記憶體節點的所有區描述符的地址初始化該陣列。

核心呼叫一個記憶體分配函式時,必須指明請求頁框所在的區。zonelist 為區描述符指標陣列。

保留的頁框池

當請求記憶體時,如果沒有足夠的空閒記憶體可用,發出請求的核心控制路徑會被阻塞,直到有記憶體釋放。

但是,一些核心控制路徑不能被阻塞,如在處理中斷或執行臨界區內的程式碼時。此時,應當產生原子分配請求。

為儘量保證原子請求不失敗,核心保留了一個頁框池,只有在記憶體不足時才使用。

保留記憶體的數量(以 KB 為單位)存放在 min_free_kbytes 變數中,初始值在核心初始化時設定,與直接對映到核心線性地址空間第 4 個 GB 的實體記憶體數量—即包含在 ZONE_DMA 和 ZONE_NORMAL 記憶體區內的頁框數目有關。

在這裡插入圖片描述

min_free_kbytes 的初始值不能小於 128 也不能大於 65536。

ZONE_DMA 和 ZONE_NORMAL 區將一定數量的頁框作為保留記憶體。

記憶體區描述符的 pages_min 欄位為區保留頁框的數目。

分割槽頁框分配器

在這裡插入圖片描述

區分配器接受動他記憶體分配與釋放的請求。

每個區內,頁框被夥伴系統處理。為達到更好的系統性能,一小部分頁框保留在快取記憶體中,以快速滿足對單個頁框的分配請求。

高階記憶體頁框的核心對映

直接對映的實體記憶體末端、高度記憶體的始端所對應的線性地址存放在 high_memory 變數中,被設定為 896MB。

896MB 邊界以上的頁框不對映到核心線性地址空間的第 4 個 GB,因此,不能被核心直接訪問。

所以,返回所分配頁框線性地址的頁分配器函式不適用於高階記憶體,即不適用於 ZONE_HIGHMEM 區內的頁框。

32位平臺上,Linux 需要通過某種方法執行核心使用所有可使用的 RAM,達到 PAE 所支援的 64GB,採用的方法如下:

  • 高階記憶體頁框只能通過 alloc_pages() 和 alloc_page() 分配,它們不返回第一個被分配也看看的線性地址,因為如果該頁框屬於高階記憶體,則其線性地址不存在。取而代之,它們返回第一個被分配頁框的頁描述符的線性地址,該地址是存在的,因為頁描述符分配在低端記憶體後,在核心初始化階段不會改變。
  • 沒有線性地址的高階記憶體中的頁框不能被核心訪問。核心線性地址空間的最後 128MB 中的一部分專門用於對映高階記憶體頁框,這種對映是暫時的,否則只有 128MB 的高階記憶體可被訪問。通過重複使用線性地址,可在不同時間訪問整個高階記憶體。

核心採用三種不同的機制將頁框對映到高階記憶體:

  • 永久核心對映。建立永久核心對映可能會阻塞當前程序。
  • 臨時核心對映。建立臨時核心對映時,核心控制路徑不能被阻塞。
  • 非連續記憶體分配。

以上技術沒有一種可確保對整個 RAM 同時定址,因為只有 128MB 的線性地址留給對映高階記憶體。

永久核心對映

永久核心對映可建立高階頁框到核心地址空間的長期對映。主要使用核心頁表中一個專門的頁表,地址存放在 pkmap_page_table 變數中。

LAST_PKMAP 巨集產生頁表的表項數,512 或 1024,取決於 PAE 是否被啟用。因此,核心一次最多訪問 2MB 或 4MB 的高階記憶體。

頁表對映的線性地址從 PKMAP_BASE 開始。pkmap_count 陣列存放 LAST_PKMAP 個計數器,每個計數器對應 pkmap_page_table 中的一項。計數器有 3 中情況:

  • 0,對應的頁表項沒有對映任何高階記憶體頁框,並且是可用的。
  • 1,對應的頁表項沒有對映任何高階記憶體頁框,但不可用,因為上次使用後其相應的 TLB 表項還未被重新整理。
  • n,對應的頁表項對映一個高階記憶體頁框,有 n-1 個核心成分在使用該頁框。

page_address_htable 散列表記錄高階記憶體頁框與永久對映的線性地址之間的聯絡。
該表中的 page_address_map 資料結構為高階記憶體中的每一個頁框進行當前對映,它包含一個指向頁描述符的指標和分配給該頁框的線性地址。

page_address() 返回頁框對應的線性地址,NULL 表示頁框在高階記憶體中且沒有被對映。引數為頁描述符指標 page,有如下兩種情況:

  1. 如果頁框不在高階記憶體中(PG_highmem 標誌為 0),線性地址存在,並且可通過以下方式獲得:計算頁框下標,將其轉換為實體地址,根據該實體地址得到相應的線性地址。
    __va((unsigned long)(page - mem_map) << 12)
  2. 如果頁框在高階記憶體中(PG_highmem 標誌為 1),如果在 page_address_htbale 散列表中找到頁框,則返回其線性地址,否則返回 NULL。

kmap() 建立永久核心對映,相當於如下程式碼:

void *kmap(struct page *page)
{
	if(!PageHighMem(page))  // 如果頁框不屬於高階記憶體
		return page_address(page);  // 返回頁框對應的線性地址
	return kmap_high(page);   
}

void *kmap_high(struct page *page)
{
	unsigned long vaddr;
	spin_lock(&kmap_lock);    
	vaddr = (unsigned long)page->virtual;
	if(!vaddr)   // 如果頁框沒有被對映
		vaddr = map_new_virtual(page);  // 把頁框的實體地址插入到 pkmap_page_table 某項中,並在 page_address_htbale 中加入一個元素
	pkmap_count[(vaddr-PKMAP_BASE) >> PAGE_SHIFT]++;   // 使得頁框的線性地址對應的計數器加 1
	spin_unlock(&kmap_lock);
	return (void *)vaddr;  // 返回對頁框對映的線性地址
}

map_new_virtual() 執行兩個巢狀迴圈:

for(;;)
{
	int count;
	DECLARE_WAITQUEUE(wait, current);
	for(count = LAST_PKMAP; cout > 0; --count)  // 掃描 pkmap_count 中所有計數器,直到找到一個空值
	{
		last_pkmap_nr = (last_pkmap_nr + 1) & (LAST_PKMAP - 1);
		if(!last_pkmap_nr)  // 搜尋到最後一個計數器
		{
		    // 開始另一趟計數器掃描,以搜尋值為 1 的計數器,並將它們的計數器重置為 0,
		    // 刪除 page_address_htable 中對應的元素,並在 pkmap_page_table 的所有項上進行 TLB 重新整理。
			flush_all_zero_pkmaps();  
			count = LAST_PKMAP;
		}
		
		if(!pkmap_count[last_pkmap_nr])  // 找到一個未使用的項
		{
			unsigned long vaddr = PKMAP_BASE + (last_pkmap_nr << PAGE_SHIFT);  // 確定該項對應的線性地址
			set_pte(&(pkmap_page_table[last_pkmap_nr]), mk_pte(page, __pgprot(0x63))); 
			pkmap_count[last_pkmap_nr] = 1;  // 表示該項已經被使用
			set_page_address(page, (void *) vaddr);  // 向 page_address_htbale 中插入一個新元素
			return vaddr;   // 返回線性地址
		}
	}

	// 沒有找到空的計數器,阻塞當前程序,
	// 直到某個程序釋放了 pkmap_page_table 頁表中的一個表項
	current->state = TASK_UNINTERRUPTIBLE;  
	add_wait_queue(&pkmap_map_wait, &wait);  // 插入等待佇列
	spin_unlock(&kmap_lock);
	schedule();   // 放棄 CPU
	remove_wait_queue(&pkmap_map_wait, &wait);
	spin_lock(&kmap_lock);

    // 如果沒有其他程序已經對映該頁,返回線性地址
	if(page_address(page))  
		return (unsigned long) page_address(page);
}

kunmap() 撤銷先前由 kmap() 建立的永久核心對映。如果頁在高階記憶體中,則呼叫 kunmap_high():

void kunmap_high(struct page *page)
{
	spin_lock(&kmap_lock);

	// 從頁的線性地址計算出在 pkmap_count 陣列的索引
	// 計數器減 1 後與 1 比較,匹配成功表明沒有程序在使用頁
	if((--pkmap_count[((unsigned long)page_address(page)-PKMAP_BASE)>>PAGE_SHIFT]) == 1)
		if(waitqueue_active(&pkmap_map_wait))  // 喚醒由 map_new_virtual() 新增在等待佇列中的程序
			wake_up(&pkmap_map_wait);  

	spin_unlock(&kmap_lock);
}

臨時核心對映

因為臨時核心對映從不阻塞當前程序,所以可用在中斷處理程式和可延遲函式的內部。

高階記憶體的任一頁框都可通過一個視窗對映到核心空間,留給臨時核心對映的視窗樹非常少。

每個 CPU 都有自己包含 13 個視窗的集合,用 enum km_type 資料結構表示,其中定義的每個符號標識了視窗的線性地址。

同一視窗不能被兩個不同的控制路徑同時使用。km_type 結構中的每個符號只能由一種核心成分使用,並以該成份命名。最後一個符號 KM_TYPE_NR 不表示線性地址,而是每個 CPU 可使用的視窗數。

km_type 中的每個符號(除最後一個)都是固定對映的線性地址的一個下標。

enum fixed_addresses 包含了符號 FIX_KMAP_BEGIN 和 FIX_KMAP_END,FIX_KMAP_END = FIX_KMAP_BEGIN + (KM_TYPE_NR * NR_CPUS) - 1。

核心用 fix_to_virt(FIX_KMAP_BEGIN) 線性地址對應的頁表項地址初始化 kmap_pte 變數。

kmap_atomic() 建立臨時核心對映,等價於如下程式碼

void *kmap_atomic(struct page *page, enum km_type type)
{
	enum fixed_address idx;
	unsigned long vaddr;
	
	current_thread_info()->preempt_count++;
	if(!PageHighMem(page))
		return page_address(page);

	// type 和 CPU 識別符號(通過 smp_processor_id())指定用哪個固定對映的線性地址來對映請求頁
	idx = type + KM_TYPE_NR * smp_precessor_id();  

	vaddr = fix_to_virt(FIX_KMAP_BEGIN + idx);  // 得到線性地址
	set_pte(kmap_pte-idx, mk_pte(page, 0x063));  // 初始化 kmap_pte 變數
	__flush_tlb_single(vaddr);  // 重新整理 TLB 項
	return (void *)vaddr;  // 返回線性地址
}

kunmap_atomic() 銷燬臨時核心對映。80x86 結構中,減少當前程序的 preempt_count,因此,在請求臨時對映前,如果核心控制路徑是可搶佔的,那麼它銷燬同一對映後可再次被搶佔。
該函式還會檢查當前程序的 TIF_NEED_RESCHED 標誌是否被置位,如果是,呼叫 schedule()。

夥伴系統演算法

避免外碎片的方法之一:開發一種適當的技術記錄現存的空閒連續頁框塊的情況,以儘量避免為滿足對小塊的請求而分割大的空閒塊。

Linux 採用夥伴系統演算法解決外碎片問題。所有的空閒頁框被分組為 11 個塊連結串列,每個塊連結串列分別包含大小為 1,2,4,8,16,32,64,128,256,512,1024 個連續的頁框。
每個塊的第一個頁框的實體地址是該塊大小的整數倍。

工作原理:

如果要請求一個 256 個頁框的塊(1MB)

  • 先在 256 個頁框的連結串列中查詢一個空閒塊,如果沒有找到,查詢下一個更大的頁塊,即在 512 個頁框的連結串列中查詢,如果找到空閒塊,將 512 的頁框分成兩等份,一般用於滿足請求,另一半插入到 256 個頁框的連結串列中。
  • 如果 512 個頁框的塊連結串列中頁沒有找到空閒塊,繼續在 1024 個頁框的連結串列中找,如果找到空閒塊,其中的 256 用於滿足請求,剩餘的 768 個頁框中,512 個插入到 512 個頁框的連結串列中,256 個插入到 256 個頁框的連結串列中。
  • 如果 1024 個頁框的連結串列中也沒有找到空閒快,放棄併發出出錯訊號。

以上過程的逆過程為頁框塊的釋放過程。核心試圖把大小為 b 的一對空閒夥伴塊合併為一個大小為 2b 的單獨塊。滿足以下條件的兩個塊稱為夥伴:

  • 兩個塊具有相同的大小,記作 b。
  • 它們的實體地址連續。
  • 第一塊的第一個頁框的實體地址是 2 * b * 2 12 2^{12} 的倍數。
    演算法是迭代的,如果成功合併所釋放的塊,會試圖合併 2b 的塊,以再次試圖形成更大的塊。

資料結構

每個記憶體區採用不同的夥伴系統。80x86 中,有三種夥伴系統:

  • 處理適合 ISA DMA 的頁框
  • 處理“常規”頁框
  • 處理高階記憶體頁框

每個夥伴系統使用的主要資料結構如下:

  • mem_map 陣列。每個記憶體區都會用到 mem_map 的一個子集,記憶體區描述符的 zone_mem_map 和 size 欄位分別描述 mem_map 中子集的第一個元素和元素個數。
  • 包含 free_area 陣列,11 個元素,每個元素對應一種塊大小,該陣列位於記憶體描述符的 free_area 欄位中。

區描述符中 free_area 陣列的第 k 個元素標識所有大小為 2 k 2^k 的空閒塊,該元素的

  • free_list 欄位是雙向迴圈連結串列的頭,該雙向鏈迴圈連結串列中集中了大小為 2 k 2^k 頁的空閒塊對應的頁描述符。
  • nr_free,指定了大小為 2 k 2^k 的空閒塊的個數。

一個 2 k 2^k 的空閒頁塊的第一個頁的描述符的 private 欄位存放了塊的 order,即數字 k,這樣的話,當頁塊被釋放時,核心可確定該塊的夥伴是否頁空閒,如果是的話,把兩個塊結合成大小為 2 k + 1 2^{k+1} 頁的單一塊。

分配塊

__rmqueue() 在記憶體區中找到一個空閒塊。需要兩個引數:

  • 區描述符的地址
  • order,標識請求的空閒頁塊大小的對數值

如果頁塊被成功分配,__rmqueue() 返回第一個被分配頁框的頁描述符,否則,返回 NULL。

__rmqueue() 假設呼叫者已經禁止了本地中斷並獲得了包含夥伴系統資料結構的 zone->lock 自旋鎖。從所請求的 order 的連結串列開始,掃描每個可用塊連結串列:

struct free_area *area;
unsigned int current_order;

for(current_order=order; current_order<11; ++current_order)
{
	area = zone->free_area + currernt_order;
	if(!list_empty(&area->free_list))
		goto block_found;
}
return NULL;

如果找到一個合適的空閒塊,從連結串列中刪除它的第一個頁框描述符,並減少記憶體區描述符中的 free_pages 的值:

block_found:
	page = list_entry(area->free_list.next, strut page, lru);
	list_del(&page->lru);
	ClearPagePrivate(page);
	page->private = 0;
	area->nr_free--;
	zone->free_pages -= 1UL << order;

如果從 curr_order 連結串列中找到的塊大於請求的 order,就執行一個 while 迴圈:當為了滿足 2 h 2^h 個頁框的請求而有必要使用 2 k 2^k 個頁框的塊時(h < k),程式就分配前面的 2 h 2^h 個頁框,而把後面 2 k 2 h 2^k - 2^h 個頁框迴圈再分配給 free_area 連結串列中下標為 h 到 k 之間的元素:

size = 1 << curr_order;
while(curr_order > order)
{
	area--;
	curr_order--;
	size >>= 1;
	buddy = page + size;
	list_add(&buddy->lru, &area->free_list);
	area->nr_free++;
	buddy->private = curr_order;
	SetPagePrivate(buddy);
}
return page;

找到合適的空閒塊後,返回所分配的第一個頁框對應的頁描述符的地址 page。

釋放塊

__free_pages_bulk() 按照夥伴系統的策略釋放頁框,有 3 個基本輸入引數:

  • page,被釋放塊中所包含的第一個頁框描述符的地址。
  • zone,記憶體區描述符的地址。
  • order,塊大小的對數。
// 宣告和初始化一些區域性變數
struct page *base = zone->zone_mem_map;
unsigned long buddy_idx;
unsigned long page_idx = page - base;  // 第一個頁框的下標
struct page *buddy, *coalesced;
int order_size = 1 << order;  

// 增加記憶體區中空閒頁框的計數器
zone->free_pages += order_size;

while(order < 10)
{
	buddy_idx = page_idx ^ (1 << order);  // 擁有 page_idx 頁描述符下標的塊的夥伴
	buddy = base + buddy_idx;  // 夥伴塊的頁描述符
	if(!page_is_buddy(buddy, order))  // 檢查 buddy 是否描述了大小為 order_size 的空閒頁框塊的第一個頁
		break;  // 跳出迴圈,因為獲得的空閒塊不能和其他空閒塊合併
	list_del(&buddy->lru);  // 夥伴塊被釋放,將它從 order 排序的空閒塊連結串列上刪除
	zone->free_area[order].nr_free--;
	ClearPagePrivate(buddy);
	buddy->private = 0;
	page_idx &= buddy_idx;
	order++;   
}

page_is_buddy() 不滿足後,說明獲得的空閒塊不能再和其他空閒塊合併,將獲得空閒塊插入適當的連結串列,並以塊大小 order 更新第一個頁框的 private 欄位:

coalesced = base + page_idx;
coalesced->private = order;
SetPagePrivate(coalesced);
list_add(&coalesced->lru, &zone->free_area[order].free_list);
zone->free_area[order].nr_free++;

int page_is_buddy(struct page *page, int order)
{
	// buddy 的第一個頁必須為空閒(_count = -1)
	// 必須屬於動態記憶體(PG_reserved 位清零)
	// private 欄位必須存放將要被釋放的塊的 order
	if(PagePrivate(buddy) && page->private == order && !PageReserved(buddy) && page_count(page) == 0)
		return 1;
	return 0;
}

每 CPU 頁框快取記憶體

為提高系統性能,每個記憶體區定義了一個“每 CPU”頁框快取記憶體,包含一些預分配的頁框,被用於滿足本地 CPU 發出的單一記憶體請求。

為每個記憶體區和每個 CPU 提供了兩個快取記憶體:

  • 熱快取記憶體,存放頁框中所包含的內容很可能就在 CPU 硬體快取記憶體中
  • 冷快取記憶體

如果核心或使用者態程序再剛分配到頁框後就立即向頁框寫,那麼從熱快取記憶體中獲得的頁框對系統性能有利。
實際上,每次對頁框儲存單元的訪問都會導致將另一個頁框中的一行放入硬體快取記憶體,除非硬體快取記憶體已經包含了那行。

如果頁框將要被 DMA 操作填充,那麼從冷快取記憶體中獲得頁框是方便的。這種情況下,不會涉及到 CPU,且硬體快取記憶體的行不會被修改。

實現每 CPU 頁框快取記憶體的主要資料結構是存放再記憶體區描述符的 pageset 欄位中的一個 per_cpu_pageset 陣列資料結構。
per_cpu_pageset 陣列中的每個元素對應一個 CPU,每個元素包含兩個 per_cpu_pages 描述符,一個留給熱快取記憶體,另一個留給冷快取記憶體。

核心使用兩個位監視熱快取記憶體和冷快取記憶體的大小:

  • 如果頁框個數低於下界 low,核心會從夥伴系統中分配 batch 個單一頁框來補充對應的快取記憶體。
  • 如果頁框個數高過上界 high,核心從快取記憶體中釋放 batch 個頁框到夥伴系統中。

通過每 CPU 頁框快取記憶體分配頁框

buffered_rmqueue() 在指定的區中分配頁框。使用每 CPU 頁框快取記憶體來處理單一頁框請求。

引數:

  • 區描述符地址
  • 請求分配的記憶體大小的對數 order
  • 分配標誌 gfp_flags。如果 gfp_flags 中的 __GFP_COLD 標誌被置位,頁框從冷快取記憶體獲取,否則從熱快取記憶體獲取。

buffered_rmqueue() 本質上執行如下操作:

  1. if(order != 0) 每 CPU 頁框快取記憶體不能被使用,跳到第 4 步。
  2. if(per_cpu_pages 描述符的 count 欄位 <= low 欄位) __GFP_COLD 標誌所標識的區本地每 CPU 快取記憶體需要補充,執行如下子步驟:
    a. 通過反覆呼叫 __rmqueue() 從夥伴系統中分配 batch 個單一頁框。
    b. 將已分配頁框的描述符插入快取記憶體連結串列中。
    c. count += 實際被分配頁框的個數。
  3. if(count > 0) 函式從快取記憶體連結串列中獲得第一個頁框,count -= 1,跳到第 5 步。
  4. 記憶體請求還沒有被滿足,或因為請求跨越了幾個連續頁框,或者是因為被選中的頁框快取記憶體為空。呼叫 __rmqueue() 從夥伴系統中分配所請求的頁框。
  5. 如果記憶體請求得到滿足,初始化第一個頁框的頁描述符:清除一些標誌,private = 0,頁框引用計數器 += 1。如果 gfp_flags 中的 __GPF_ZERO 標誌被置位,則函式將被分配的記憶體區域填充 0。
  6. 返回第一個頁框的頁描述符地址,如果記憶體分配請求失敗則返回 NULL。

釋放頁框到每 CPU 頁框快取記憶體

核心使用 free_hot_page() 和 free_cold_page() 釋放單個頁框到每 CPU 頁框快取記憶體,這兩個函式都是 free_hot_cold_page() 的簡易封裝,接收的引數為:將要釋放的頁框的描述符地址 page 和 cold 標誌。

free_hot_cold_page() 執行如下操作:

  1. 從 page->flags 欄位獲取包含該頁框的記憶體區描述符地址。
  2. 獲取由 cold 標誌選擇的記憶體區快取記憶體的 per_cpu_pages 描述符的地址。
  3. 如果 count >= high,呼叫 free_pages_bulk() 清空快取記憶體,將區描述符、將被釋放的頁框個數(batch 欄位)、快取記憶體連結串列的地址及數字 0(0 到 order 個頁框)傳遞
    給該函式。free_pages_bulk() 反覆呼叫 __free_pages_bulk() 釋放指定數量的(從快取記憶體連結串列獲得的)頁框到記憶體區的夥伴系統中。
  4. 釋放的頁框新增到快取記憶體連結串列上,count++。

Linux 2.6 核心,沒有頁框被釋放到冷快取記憶體。

區分配器

區分配器是核心頁框分配器的前端,它必須分配一個包含足夠多空閒頁框的記憶體區來滿足記憶體請求。區分配器必須滿足如下目標:

  • 保護保留的頁框池。
  • 當記憶體不足且允許阻塞當前程序時,觸發頁框回收演算法;一旦某些頁框被釋放,區分配器將再次嘗試分配。
  • 儘可能保護小而珍貴的 ZONE_DMA 區。

對一組連續頁框的請求實質上是通過執行 alloc_pages 巨集來處理的。接著,該巨集又呼叫 __alloc_pages(),該函式是區分配器的核心,接收如下 3 個引數:

  • gfp_mask,在記憶體分配請求中指定的標誌。
  • order,將要分配的一組連續頁框數量的對數。
  • zonelist,指向 zonelist 資料結構的指標,按優先次序描述了適於記憶體分配的區。

__alloc_pages() 掃描包含在 zonelist 資料結構中的每個區:

for(i=0; (z = zonelist->zone[i]) != NULL; i++)
{
	if(zone_watermark_ok(z, order, ...)) // 將當前區的空閒頁框個數與一個閾值比較
	{
		page = buffered_rmqueue(z, order, gfp_mask);  // 返回第一個被分配的頁框的頁描述符
		if(page)
			return page;
	}
}

zone_watermark_ok() 接收幾個引數,它們覺得區中空閒頁框個數的閾值 min。如果滿足如下兩個條件返回 1:

  1. 區中至少還有 min 個空閒頁框,不包含為記憶體不足保留的頁框(區描述符的 lowmem_reserve 欄位)。
  2. 在 order 至少為 k 的塊中起碼還有 min/ 2 k 2^k 個空閒頁框。

閾值 min 由 zone_watermark_ok() 確定:

  • 作為引數的 base 值可以是 pages_min、pages_low 和 pages_high 中的任意一個。
  • 如果作為引數的 gfp_high 標誌被置位,那麼 base 值被 2 除。通常,當 gfp_mask 中的 __GFP_WAIT 標誌被置位(能從高階記憶體中分配頁框),則該標誌為 1。
  • 如果作為引數的 can_try_harder 標誌被置位,則閾值將會再減少四分之一。當 gfp_mask 中的 __GFP_WAIT 標誌被置位,或者如果當前程序是一個實時程序並在程序上下文中已經完成了記憶體分配,則 can_try_harder 標誌等於 1。

__alloc_pages() 本質上執行如下步驟:

  1. 對區第一次掃描。第一次掃描中,將閾值 min 被設為 z->pages_low,z 指向正在被分析的區描述符(引數 can_try_harder 和 gfp_high 被設為 0)。
  2. 如果函式在上一步沒有終止,那麼沒有剩下多少空閒記憶體:喚醒 kswapd 核心執行緒來非同步地開始回收頁框。
  3. 對區第二次掃描,將 z->pages_min 作為閾值 base 傳遞。實際閾值由 can_try_harder 和 gfp_high 標誌決定。
  4. 如果函式在上一步沒有終止,那麼系統記憶體不足。如果產生記憶體分配請求的核心控制路徑不是一箇中斷處理程式或一個可延遲函式,並且它試圖回收頁框,那麼執行對區的第三次掃描,試圖分配頁框並忽略記憶體不足的閾值,即不呼叫 zone_watermark_ok()。只有這種情況下才允許核心控制路徑耗用為記憶體不足預留的頁。如果沒有任何區包含足夠的頁,返回 NULL。
  5. 正在呼叫的核心控制路徑並沒有試圖回收記憶體。如果 gfp_mask 的 __GFP_WAIT 標誌沒有被置位,返回 NULL,在這種情況下,需要阻塞當前程序。
  6. 當前程序可被阻塞,呼叫 cond_resched() 檢查是否有其它的程序需要 CPU。
  7. 設定 current 的 PF_MEMALLOC 標誌來表示程序已經準備好執行記憶體回收。
  8. 將一個指向 reclaim_state 資料結構的指標存入 current->reclaim_state,其欄位 reclaimed_slab 被初始化為 0.
  9. try_to_free_pages() 試圖回收一些頁框,可能會阻塞當前程序。一旦函式返回,__alloc_pages() 就重設 current 的 PF_MEMALLOC 標誌並再次呼叫 cond_resched()。
  10. 如果上一步已經釋放了一些頁框,在執行與第 3 步相同的區掃描。如果記憶體分配請求不能被滿足,函式決定是否對區繼續掃描:如果 __GFP_NORETRY 標誌被清除,且記憶體分配請求跨越了多達 8 個頁框或 __GFP_REPEAT 和 __GFP_NOFAIL 標誌其中之一被置位,就呼叫 blk_congestion_wait() 使程序休眠一會,跳到第 6 步。否則,返回 NULL。
  11. 如果第 9 步沒有釋放任何頁框,意味著空閒頁框已經少到了危險的地步,並且不可能回收任何頁框。如果允許核心控制路徑執行依賴於檔案系統的操作來殺死一個程序,且 __GFP_NORETRY 標誌為 0,那麼執行如下子步驟:
    a. 使用等於 z->pages_high 的閾值再一次是掃描區。
    b. out_of_memory() 殺死一個程序以釋放一些記憶體。
    c. 跳回第 1 步。

釋放一組頁框

釋放頁框的所有核心巨集和函式都依賴於 __free_pages() 函式,它接收 2 個引數:

  • 將要釋放的第一個頁框的頁描述符的地址 page
  • 將要釋放的一組連續頁框的數量的對數 order

執行如下步驟:

  1. 檢查第一個頁框是否真正屬於動態記憶體(PG_reserved 標誌被清 0);如果不是,則終止。
  2. page->count–,if(page->count >= 0) 終止。
  3. if(order == 0) free_hot_page() 釋放頁框到適當區的每 CPU 熱快取記憶體。
  4. if(order > 0) 將頁框加入到本地連結串列,free_pages_bulk() 釋放頁框到適當區的夥伴系統。

記憶體區管理

夥伴系統演算法採用頁框作為基本記憶體區,適用於對大塊記憶體的請求,但不適用於處理對小記憶體區的請求。

一種典型的解決方法是提供按幾何分佈的記憶體區大小,即記憶體區大小取決於 2 的冪而不取決於所存放的資料大小。
為此,核心建立了 13 個按幾何分佈的空閒記憶體區連結串列,大小從 32 到 131072 位元組。
用一個動態連結串列來記錄每個頁框所包含的空閒記憶體。

slab 分配器

演算法基於下列前提:

  • 所存放資料的型別可以影響記憶體區的分配方式。slab 分配器將記憶體區看作物件。為避免重複初始化,slab 分配器不丟棄已分配物件,而是釋放後儲存在記憶體中。
  • 核心函式傾向於反覆請求同一型別的記憶體區。slab 分配器將反覆被請求的頁框儲存在快取記憶體中並很快重新使用它們。
  • 對記憶體區的請求可根據它們發生的頻率分類。
  • 引入的物件大小不是幾何分佈時,可藉助處理器硬體快取記憶體得到較好的效能。
  • 硬體快取記憶體的高效能是儘可能限制對夥伴系統分配器呼叫的另一個理由。

slab 分配器將物件分組放進快取記憶體。每個快取記憶體都是同種型別物件的一種“儲備”。

包含快取記憶體的主記憶體區被劃分為多個 slab,每個 slab 由多個連續的頁框組成,頁框中包含已分配物件和空閒物件。

在這裡插入圖片描述

快取記憶體描述符

每個快取記憶體都由 kmem_cache_t 型別的資料結構描述。

slab 描述符

slab 描述符存放在兩個可能的地方:

  • 外部 slab 描述符,存放在 slab 外部,位於 cache_sizes 指向的一個不適合 ISA DMA 的普通快取記憶體種。
  • 內部 slab 描述符,存放在 slab 內部,位於分配給 slab 的第一個頁框的起始位置。

當物件小於 512MB,或記憶體碎片在 slab 內部為 slab 描述符留下足夠空間時,slab 分配器選用第二種方案。
如果 slab 描述符存放在 slab 外部,那麼快取記憶體描述符的 flags 欄位中的 CFLAGS_OFF_SLAB 標誌被置 1,否則被置 0。

在這裡插入圖片描述

普通和專用快取記憶體

普通快取記憶體只用於 slab 分配器。
專用快取記憶體由核心的其餘部分使用。

普通快取記憶體是:

  • 第一個快取記憶體是 kmem_cache,包含由核心使用的其餘快取記憶體的快取記憶體描述符,該快取記憶體描述符包含於 cache_cache 變數。
  • 另外一些快取記憶體包含用作普通用途的用途區。

kmem_cache_init() 和 kmem_cache_sizes_init() 建立普通快取記憶體。

kmem_cache_create() 建立專用快取記憶體。

  • 首先根據引數確定處理新快取記憶體的最佳方法
  • 然後從 cache_cache 普通快取記憶體中為新快取記憶體分配一個快取記憶體描述符,並插入到 cache_chain 連結串列。

kmem_cache_destroy() 撤銷一個快取記憶體並將它從 cache_chain 連結串列上刪除,主要用於模組中。
為避免浪費記憶體空間,核心必須在撤銷快取記憶體本身之前就撤銷所有的 slab。
kmem_cache_shrink() 通過反覆呼叫 slab_destroy() 撤銷快取記憶體種所有的 slab。

所有普通和專用快取記憶體的名字都可以在執行期間通過讀取 /proc/slabinfo 檔案得到。
該檔案也指明快取記憶體中空閒物件的個數和已分配物件的個數。

slab 分配器與分割槽頁框分配器的介面

slab 分配器建立新的 slab 時,它呼叫 kmem_getpages() 依靠分割槽頁框分配器來獲得一組連續的空閒頁框。

// cachep:指向要申請頁框的快取記憶體描述符
// flags:說明如何請求頁框。與 cachep 的 gfpflags 欄位的專用快取記憶體分配標誌相結合
void *kem_getpages(kmem_cache_t *cachep, int flags)
{
	struct page *page;
	int i;

	flags |= cachep->gfpflags;
	page = alloc_pages(flags, cachep->gfporder);  // 記憶體分配請求的大小由 cachep->gfporder 指定
	if(!page)
		return NULL;
	i = (1 << cache->gfporder);
	if(cachep->flags & SLAB_RECLAIM_ACCOUNT)  // 如果已經建立了 slab 快取記憶體且 SLAB_RECLAIM_ACCOUNT 標誌置位
		atomic_add(i, &slab_reclaim_pages);  // slab_reclaim_pages 被適當增加
	while(i--)
		SetPageSlab(page++);  // 將所分配頁框的頁描述符中的 PG_slab 標誌置位
	return page_address(page);
}

kmem_freepages() 釋放分配給 slab 的頁框

void kmem_freepages(kmem_cache_t *cachep, void *addr)
{
	unsigned long i = (1 << cachep->gfporder);
	strucct page *page = virt_to_page(addr);  // 從線性地址 addr 開始釋放頁框

	if(current->recalim_state)  // 如果當前程序正在執行記憶體回收
		current->reclaim_state->reclaimed_slab += i;    // reclaimed_slab 被增加,於是剛被釋放的頁就能通過頁框回收演算法被記錄下來
	while(i--)
		ClearPageSlab(page++);
	free_pages((unsigned long) addr, cachep->gfporder);
	if(cachep->flags & SLAB_RECLAIM_ACCOUNT)  // 如果 SLAB_RECLAIM_ACCOUNT 標誌置位
		atomic_sub(1 << cachep->gfporder, &slab_reclaim_pages);  // slab_reclaim_pages 被適當減少
}

給快取記憶體分配 slab

以下兩個條件都為真時,才給快取記憶體分配 slab:

  • 已發出一個分配新物件的請求。
  • 快取記憶體中無空閒物件。

slab 分配器呼叫 cache_grow() 給快取記憶體分配一個新的 slab。
cache_grow() 呼叫 kmem_getpages() 從分割槽頁框分配器獲得一組頁框來存放一個單獨的 slab,然後呼叫 alloc_slabmgmt() 獲得一個新的 slab 描述符。
如果快取記憶體描述符的 CFLAGS_OFF_SLAB 標誌置位,則從其 slabp_cache 欄位指向的普通快取記憶體中分配該新的 slab 描述符,否則,從 slab 的第一個頁框中分配該 slab 描述符。

給定一個頁框,核心必須確定它是否被 slab 分配器使用,如果是,就迅速得到相應快取記憶體和 slab 描述符的地址。
因此,cache_grow() 掃描分配給新 slab 的頁框的所有頁描述符,並將快取記憶體描述符和 slab 描述符的地址分別賦給頁描述符中 lru 欄位的 next 欄位和 prev 欄位。

接著,cahce_grow() 呼叫 cache_init_objs(),將構造方式應用到新 slab 包含的所有物件上。

最後,cache_grow() 呼叫 list_add_tail() 將新的 slab 描述符 *slabp 新增到快取記憶體描述符 *cachep 的全空 slab 連結串列的末端,並更新快取記憶體中的空閒物件計數器:

list_add_tail(&slabp->list, &cachep->lists->slabs_free);
cachep->lists->free_objects += cachep->num;

從高度快取中釋放 slab

以下兩種條件下會撤銷 slab

  • slab 快取記憶體中有太多的空閒物件。
  • 被週期性呼叫的定時器函式確定是否有完全未使用的 slab 能被釋放。

slab_destroy() 撤銷一個 slab,並釋放相應的頁框到分割槽頁框分配器:

void slab_destroy(kmem_cache_t *cachep, slab_t *slabp)
{
	if(cachep->dtor)  // 如果快取記憶體為它的物件提供了析構方法
	{
		int i;
		for(i=0; i<cachep->num; i++)
		{
			void *objp = slabp->s_mem + cachep->objsize * i;
			(cachep->dtor)(objp, cachep, 0);  // 使用析構方法釋放 slab 中的物件
		}
	}
	kmem_freepages(cachep, slapb->s_mem - slabp->colouroff);  // 把 slab 使用的所有連續頁框返回給夥伴系統
	if(cachep->flags & CFLAGS_OFF_SLAB)  // 如果 slab 描述符存放在 slab 的外面
		kmem_cache_free(cachep->slabp_cache, slabp);  // 從 slab 描述符的快取記憶體釋放這個 slab 描述符
}

物件描述符

每個物件都有型別為 kmem_bufctl_t 的一個描述符。
物件描述符存放在一個數組中,位於相應的 slab 描述符之後。
因此,與 slab 描述符本身類似,slab 的物件描述符頁可以用兩種可能的方式存放:

  • 外部物件描述符,存放在 slab 的外面,位於快取記憶體描述符的 slabp_cache 欄位的一個普通快取記憶體中。記憶體區的大小取決於 slab 中的物件數。
  • 內部物件描述符,存放在 slab 內部,位於描述符所描述的物件前。

物件描述符是一個無符號整數,只有物件空閒時才有意義。
它包含下一個空閒物件在 slab 中的下標,因此實現了 slab 內部空閒物件的一個簡單鏈表。
空閒物件連結串列中的最後一個元素的物件描述符用值 BUFCTL_END(0xffff) 標記。

對齊記憶體中的物件

slab 分配器所管理的物件可以在記憶體中對齊,即存放它們的記憶體單元的起始實體地址是一個對齊因子的倍數,通常為 2 的倍數。

slab 分配器所允許的最大對齊因子是 4096,即頁框大小,意味著通過訪問物件的實體地址或線性地址就可以訪問對齊物件。

通常,如果記憶體單元的實體地址大小是字大小對齊的,則微機對記憶體單元的存器會非常快。
因此,預設情況下,kmem_cache_create() 根據 BTES_PER_WORD 巨集所指定的字大小來對齊物件。

當建立一個新的 slab 快取記憶體時,可讓其中的物件在第一級硬體快取記憶體中對齊。
為此,需設定 SLAB_HWCACHE_ALIGN 快取記憶體描述符標誌。
kmem_cache_create() 按如下方式處理請求:

  • 如果物件大小大於快取記憶體行的一半,就在 RAM 中根據 L1_CACHE_BYTES 的倍數(行的開始)對齊物件。
  • 否則,物件的大小為 L1_CACHE_BYTES 的因子取整,可避免小物件跨越兩個快取記憶體行。
    在這裡插入圖片描述

slab 著色

同一硬體快取記憶體行可以對映 RAM 中很多不同的塊。
相同大小的物件傾向於存放在快取記憶體內相同的偏移量處。
在不同的 slab 內具有相同偏移量的物件最終可能對映在同一快取記憶體行中。
快取記憶體的硬體可能因此花費記憶體週期,在同一快取記憶體行於 RAM 記憶體單元間來回傳輸兩個物件,而其他的快取記憶體行卻未被充分使用。
slab 著色可避免以上情況:將顏色(不同的隨機數)分配給 slab。

slab 內放置物件的方式有多種,取決於以下變數:

  • num,可在 slab 中存放物件的個數
  • osize,物件的大小,包括對齊的位元組。
  • dsize,slab 描述符的大小加上所有物件描述符的大小,等於硬體快取記憶體行大小的最小倍數。如果 slab 描述符和物件描述符都在 slab 外部,該值為 0.
  • free,slab 內未用位元組數。

一個 slab 中的總位元組長度 = (num * osize) + dsize + free

slab 分配器利用空閒未用的位元組 free 對 slab 著色。
“著色”是用來再細分 slab,並允許記憶體分配器把物件展開在不同的線性地址中,以便核心從微處理器的硬體快取記憶體中獲得更好效能。

具有不同顏色的 slab 把 slab 的第一個物件存放在不同的記憶體單元,同時滿足對齊約束。
可用顏色的個數是 f r e e / a l n free / aln ,存放在快取記憶體描述符的 colour 欄位。

如果用顏色 col 對一個 slab 著色,第一個物件的偏移量 = c o l a l n + d s i z e col * aln + dsize 位元組。

將當前顏色存放在快取記憶體描述符的 colour_next 欄位後,顏色就可以在給定物件型別的 slab 之間均勻分佈。
cache_grow() 把 colour_next 表示的顏色賦給一個新的 slab,並遞增該欄位。
colour_next 值變為 colour 後,從 0 重新開始。
此外,cache_grow() 從快取記憶體描述符的 colour_off 欄位獲取 aln,根據 slab 內物件的個數計算 dsize,最後將 c o l a l n + d s i z e col * aln + dsize 值存放在 slab 描述符的 colouroff 欄位。
在這裡插入圖片描述

空閒 slab 物件的本地快取記憶體

為了減少處理器之間對自旋鎖的競爭,並更好地利用硬體快取記憶體,slab 分配器的每個快取記憶體包含一個被稱為 slab 本地快取記憶體的每 CPU 資料結構,它由一個指向被釋放物件的小指標陣列組成。
slab 物件的大多數分配和釋放只與本地陣列有關,當本地陣列下溢或上溢時才涉及 slab 資料結構。

快取記憶體描述符的 array 欄位是一組指向 array_cache 資料結構的指標,系統中的每個 CPU 對應一個元素。
每個 array_cache 資料結構是空閒物件的本地快取記憶體的一個描述符。

本地快取記憶體描述符不包含本地快取記憶體本身的地址,它位於描述符之後。
本地快取記憶體存放的是指向已釋放物件的指標,而不是物件本身,物件本身位於快取記憶體的 slab 中。

當建立一個新的 slab 快取記憶體時,kmem_cache_create() 決定本地快取記憶體的大小,並分配本地快取記憶體,將其指標存放再快取記憶體描述符的 array 欄位。
本地快取記憶體大小取決於存放再 slabg 快取記憶體中物件的大小,範圍從 1(相對於非常大的物件