1. 程式人生 > >linux核心記憶體管理學習之二(實體記憶體管理--夥伴系統)

linux核心記憶體管理學習之二(實體記憶體管理--夥伴系統)

linux使用夥伴系統來管理實體記憶體頁。

一、夥伴系統原理

1. 夥伴關係

定義:由一個母實體分成的兩個各方面屬性一致的兩個子實體,這兩個子實體就處於夥伴關係。在作業系統分配記憶體的過程中,一個記憶體塊常常被分成兩個大小相等的記憶體塊,這兩個大小相等的記憶體塊就處於夥伴關係。它滿足 3 個條件 :
  •  兩個塊具有相同大小記為 2^K
  •  它們的實體地址是連續的
  •  從同一個大塊中拆分出來

2. 夥伴演算法的實現原理

為了便於頁面的維護,將多個頁面組成記憶體塊,每個記憶體塊都有 2 的方冪個頁,方冪的指數被稱為階 order。order相同的記憶體塊被組織到一個空閒連結串列中。夥伴系統基於2的方冪來申請釋放記憶體頁。
當申請記憶體頁時,夥伴系統首先檢查與申請大小相同的記憶體塊連結串列中,檢看是否有空閒頁,如果有就將其分配出去,並將其從連結串列中刪除,否則就檢查上一級,即大小為申請大小的2倍的記憶體塊空閒連結串列,如果該連結串列有空閒記憶體,就將其分配出去,同時將剩餘的一部分(即未分配出去的一半)加入到下一級空閒連結串列中;如果這一級仍沒有空閒記憶體;就檢查它的上一級,依次類推,直到分配成功或者徹底失敗,在成功時還要按照夥伴系統的要求,將未分配的記憶體塊進行劃分並加入到相應的空閒記憶體塊連結串列
在釋放記憶體頁時,會檢查其夥伴是否也是空閒的,如果是就將它和它的夥伴合併為更大的空閒記憶體塊,該檢查會遞迴進行,直到發現夥伴正在被使用或者已經合併成了最大的記憶體塊。

二、linux中的夥伴系統相關的結構

系統中的每個實體記憶體頁(頁幀)都對應一個struct page資料結構,每個節點都包含了多個zone,每個zone都有struct zone表示,其中儲存了用於夥伴系統的資料結構。zone中的
struct free_area      free_area[MAX_ORDER];
用於管理該zone的夥伴系統資訊。夥伴系統將基於這些資訊管理該zone的實體記憶體。該陣列中每個陣列項用於管理一個空閒記憶體頁塊連結串列,同一個連結串列中的記憶體頁塊的大小相同,並且大小為2的陣列下標次方頁。MAX_ORDER定義了支援的最大的記憶體頁塊大小。
struct free_area的定義如下
struct free_area {
       structlist_head       free_list[MIGRATE_TYPES];
       unsignedlong        nr_free;
};
  • nr_free:其中nr_free表示記憶體頁塊的數目,對於0階的表示以1頁為單位計算,對於1階的以2頁為單位計算,n階的以2的n次方為單位計算。
  • free_list:用於將具有該大小的記憶體頁塊連線起來。由於記憶體頁塊表示的是連續的物理頁,因而對於加入到連結串列中的每個記憶體頁塊來說,只需要將記憶體頁塊中的第一個頁加入該連結串列即可。因此這些連結串列連線的是每個記憶體頁塊中第一個記憶體頁,使用了struct page中的struct list_head成員lru。free_list陣列元素的每一個對應一種屬性的型別,可用於不同的目地,但是它們的大小和組織方式相同。
因此在夥伴系統看來,一個zone中的記憶體組織方式如下圖所示:


基於夥伴系統的記憶體管理方式專注於記憶體節點的某個記憶體域的管理,但是系統中的所有zone都會通過備用列表連線起來。夥伴系統和記憶體域/節點的關係如下圖所示:


系統中夥伴系統的當前資訊可以通過/proc/buddyinfo檢視:


這是我的PC上的資訊,這些資訊描述了每個zone中對應於每個階的空閒記憶體頁塊的數目,從左到右階數依次升高。

三、避免碎片

1.碎片概念

夥伴系統也存在一些問題,在系統長時間執行後,實體記憶體會出現很多碎片,如圖所示:
 
這是雖然可用記憶體頁還有很多,但是最大的連續實體記憶體也只有一頁,這對於使用者程式不成問題,因為使用者程式通過頁表對映,應用程式看到的總是連續的虛擬記憶體。但是對於核心來說就不行了,因為核心有時候需要使用連續的實體記憶體。

2.linux解決方案

碎片問題也存在於檔案系統,檔案系統中的碎片可以通過工具來解決,即分析檔案系統,然後重新組織檔案的位置,但是這種方不適用於核心,因為有些物理頁時不能隨意移動。核心採用的方法是反碎片(anti-fragmentation)。為此核心根據頁的可移動性將其劃分為3種不同的型別:
  • 不可移動的頁:在記憶體中有固定位置,不能移動。分配給核心核心的頁大多是此種類型
  • 可回收的頁:不能移動,但是可以刪除,其內容可以從某些源重新生成。
  • 可移動的頁:可以隨意移動。屬於使用者程序的頁屬於這種型別,因為它們是通過頁表對映的,因而在移動後只需要更新使用者程序頁表即可。
頁的可移動性取決於它屬於上述三類中的哪一類,核心將頁面按照不同的可移動性進行分組,通過這種技術,雖然在不可移動頁中仍可能出現碎片,但是由於具有不同可移動性的頁不會進入同一個組,因而其它兩個型別的記憶體塊就可以獲得較好的“對抗碎片”的特性。
需要注意的是按照可移動性對記憶體頁進行分組時在執行中進行的,而不是在一開始就設定好的。

1.資料結構

核心定義了MIGRATE_TYPES中遷移型別,其定義如下: 
enum {
	MIGRATE_UNMOVABLE,
	MIGRATE_RECLAIMABLE,
	MIGRATE_MOVABLE,
	MIGRATE_PCPTYPES,	/* the number of types on the pcp lists */
	MIGRATE_RESERVE = MIGRATE_PCPTYPES,
	MIGRATE_ISOLATE,	/* can't allocate from here */
	MIGRATE_TYPES
};
其中前三種分別對應於三種可移動性,其它幾種的含義:
  • MIGRATE_PCPTYPES:是per_cpu_pageset,即用來表示每CPU頁框快取記憶體的資料結構中的連結串列的遷移型別數目
  • MIGRATE_RESERVE:是在前三種的列表中都沒用可滿足分配的記憶體塊時,就可以從MIGRATE_RESERVE分配
  • MIGRATE_ISOLATE:用於跨越NUMA節點移動實體記憶體頁,在大型系統上,它有益於將實體記憶體頁移動到接近於是用該頁最頻繁地CPU
每種型別都對應free_list中的一個數組項。
類似於從zone中的分配,如果無法從指定的遷移型別分配到頁,則會按照fallbacks指定的次序從備用遷移型別中嘗試分配,它定義在page_alloc.c中。
雖然該特性總是編譯進去的,但是該特性只有在系統中有足夠的記憶體可以分配到每種遷移型別對應的連結串列時才有意義,也就是說每個可以遷移性連結串列都要有“適量”的記憶體,核心需要對“適量”的判斷是基於兩個巨集的:
  • pageblock_order:核心認為夠大的一個分配的階。
  • pageblock_nr_pages:核心認為啟用該特性時每個遷移連結串列需要具有的最少的記憶體頁數。它的定義是基於pageblock_order的。
基於這個“適量”的概念核心會在build_all_zonelists中判斷是否要啟用該特性。page_group_by_mobility_disabled表示是否啟用了該特性。
核心定義了兩個標誌:__GFP_MOVABLE和 __GFP_RECLAIMABLE分別用來表示可移動遷移型別和可回收遷移型別,如果沒有設定這兩個標誌,則表示是不可移動的。如果頁面遷移特性被禁止了,則所有的頁都是不可移動頁。
struct zone中包含了一個欄位pageblock_flags,它用於跟蹤包含pageblock_nr_pages個頁的記憶體區的屬性。在初始化期間,核心自動保證對每個遷移型別,在pageblock_flags中都分配了足夠儲存NR_PAGEBLOCK_BITS個位元的空間。
set_pageblock_migratetype用於設定一個以指定的頁為起始地址的記憶體區的遷移型別。
頁的遷移型別是預先分配好的,對應的位元位總是可用,在頁釋放時,必須將其返還給正確的連結串列。get_pageblock_migratetype可用於從struct page中獲取頁的遷移型別。
通過/proc/pagetypeinfo可以獲取系統當前的資訊。
在記憶體初始化期間memmap_init_zone會將所有的記憶體頁都初始化為可移動的。該函式在paging_init中會最終被調到(會經過一些中間函式,其中就有free_area_init_node)。

3.虛擬可移動記憶體

核心還提供了一種機制來解決碎片問題,即使用虛擬記憶體域ZONE_MOVABLE。其思想是:可用記憶體劃分為兩個部分,一部分用於可移動分配,一部分用於不可移動分配。這樣就防止了不可移動頁向可移動記憶體區域引入碎片。
該機制需要管理員來配置兩部分記憶體的大小。
kernel引數kernelcore用於指定用於不可移動分配的記憶體數量,如果指定了該引數,其值會儲存在required_kernelcore會基於它來計算。
kernel引數movablecore用於指定用於可移動分配的記憶體數量,如果指定了該引數,則其值會被儲存在required_movablecore中,同時會基於它來計算required_kernelcore,程式碼如下(函式find_zone_movable_pfns_for_nodes):
		corepages = totalpages - required_movablecore;
		required_kernelcore = max(required_kernelcore, corepages);
如果計算出來的required_kernelcore為0,則該機制將無效。
該zone是一個虛擬zone,它不和任何實體記憶體相關聯,該域中的記憶體可能來自高階記憶體或者普通記憶體。用於不可移動分配的記憶體會被均勻的分佈到系統的各個記憶體節點中;同時用於可移動分配的記憶體只會取自最高記憶體域的記憶體,zone_movable_pfn記錄了取自各個節點的用於可移動分配的記憶體的起始地址。

四、初始化記憶體域和節點資料結構

在記憶體管理的初始化中,架構相關的程式碼要完成系統中可用記憶體的檢測,並要將相關資訊提交給架構無關的程式碼。架構無關的程式碼free_area_init_nodes負責完成管理資料結構的建立。該函式需要一個引數max_zone_pfn,它由架構相關的程式碼提供,其中儲存了每個記憶體域的最大可用頁幀號。核心定義了兩個陣列:

static unsigned long __meminitdata arch_zone_lowest_possible_pfn[MAX_NR_ZONES];
static unsigned long __meminitdata arch_zone_highest_possible_pfn[MAX_NR_ZONES];
這兩個陣列在free_area_init_nodes用於儲存來自max_zone_pfn的資訊,並將它轉變成[low,high]的形式。
然後核心開始呼叫find_zone_movable_pfns_for_nodes對ZONE_MOVABLE域進行初始化。
然後核心開始為每一個節點呼叫free_area_init_node,這個函式將完成:
  1. 呼叫calculate_node_totalpages計算節點中頁的總數
  2. 呼叫alloc_node_mem_map負責初始化struct pglist_data中的node_mem_map,為它分配的記憶體將用於儲存本節點的所有實體記憶體的struct page結構。這片記憶體將對其到夥伴系統的最大分配階上。而且如果當前節點是第0個節點,則該指標資訊還將儲存在全域性變數mem_map中。
  3. 呼叫free_area_init_core完成初始化進一步的初始化

free_area_init_core將完成記憶體域資料結構的初始化,在這個函式中

  1. nr_kernel_pages記錄直接對映的頁面數目,而nr_all_pages則記錄了包括高階記憶體中頁數在內的頁數
  2. 會呼叫zone_pcp_init初始化該記憶體域的每CPU快取
  3. 會呼叫init_currently_empty_zone初始化該zone的wait_table,free_area列表
  4. 呼叫memmap_init初始化zone的頁,所有頁都被初始化為可移動的

五、分配器API

夥伴系統只能分配2的整數冪個頁。因此申請時,需要指定請求分配的階。
有很多分配和釋放頁的API,都定義在gfp.h中。最簡單的是alloc_page(gfp_mask)用來申請一個頁, free_page(addr)用來釋放一個頁。
這裡更值得關注的獲取頁面時的引數gfp_mask,所有獲取頁面的API都需要指定該引數。它用來影響分配器的行為,其中有是分配器提供的標誌,標誌有兩種:
zone修飾符:用於告訴分配器從哪個zone分配記憶體
行為修飾符:告訴分配器應該如何進行分配
其中zone修飾符定義為

#define __GFP_DMA	((__force gfp_t)___GFP_DMA)
#define __GFP_HIGHMEM	((__force gfp_t)___GFP_HIGHMEM)
#define __GFP_DMA32	((__force gfp_t)___GFP_DMA32)
#define __GFP_MOVABLE	((__force gfp_t)___GFP_MOVABLE)  /* Page is movable */
#define GFP_ZONEMASK	(__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE)
這些定義都一目瞭然,需要指出的是如果同時指定了__GFP_MOVABLE和__GFP_HIGHMEM,則會從虛擬的ZONE_MOVABLE分配。
更詳細的可以參考gfp.h,其中包含了所有的標誌及其含義。

1.分配頁

__alloc_pages會完成最終的記憶體分配,它是夥伴系統的核心程式碼(但是在核心程式碼中,這種命名方式的函式都是需要小心呼叫的,一般都是給實現該功能的程式碼自己呼叫,不作為API提供出去的,因而它的包裝器才是對外提供的API,也就是alloc_pages_node)。

1.選擇頁

選擇頁中最重要的函式是get_page_from_freelist,它負責通過標誌和分配階來判斷分配是否可以進行,如果可以就進行實際的分配。該函式還會呼叫zone_watermark_ok根據指定的標識判斷是否可以從給定的zone中進行分配。該函式需要struct zonelist的指標指向備用zone,噹噹前zone不能滿足分配需求時就依次遍歷該列表嘗試進行分配。整體的分配流程是:

  1. 呼叫get_page_from_freelist嘗試進行分配,如果成功就返回分配到的頁,否則
  2. 喚醒kswapd,然後再次呼叫get_page_from_freelist嘗試進行分配,如果成功就返回分配的頁,否則
  3. 如果分配的標誌允許不檢查閾值進行分配,則以ALLOC_NO_WATERMARKS為標誌再次呼叫get_page_from_freelist嘗試分配,如果成功則返回分配的頁;如果不允許不檢查閾值或者仍然失敗,則
  4. 如果不允許等待,就分配失敗,否則
  5. 如果支援壓縮,則嘗試先對記憶體進行一次壓縮,然後再呼叫get_page_from_freelist,如果成功就返回,否則
  6. 進行記憶體回收,然後再呼叫get_page_from_freelist,如果成功就返回,否則
  7. 根據回收記憶體並嘗試分配的結果以及分配標誌,可能會呼叫OOM殺死一個程序然後再嘗試分配,也可能不執行OOM這一步的操作,如果執行了,則在失敗後可能就徹底失敗,也可能重新回到第2步,也可能繼續下一步
  8. 回到第2步中呼叫get_page_from_freelist的地方或者再嘗試一次先壓縮後分配,如果走了先壓縮再分配這一步,這就是最後一次嘗試了,要麼成功要麼失敗,不會再繼續嘗試了

2.移出所選擇的頁

在函式get_page_from_freelist中,會首先在zonelist中找到一個具有足夠的空閒頁的zone,然後會呼叫buffered_rmqueue進行處理,在分配成功時,該函式會把所分配的記憶體頁從zone的free_list中移出,並且保證剩餘的空閒記憶體頁滿足夥伴系統的要求,該函式還會把記憶體頁的遷移型別存放在page的private域中。
該函式的步驟如圖所示:


可以看出buffered_rmqueue的工作過程為:

  1. 如果申請的是單頁,會做特殊處理,核心會利用每CPU的快取加速這個過程。並且在必要的時候會首先填充每CPU的快取。函式rmqueue_bulk用於從夥伴系統獲取記憶體頁,並新增到指定的連結串列,它會呼叫函式__rmqueue。
  2. 如果是分配多個頁,則會首先呼叫__rmqueue從記憶體域的夥伴系統中選擇合適的記憶體塊,這一步可能失敗,因為雖然記憶體域中有足夠數目的空閒頁,但是頁不一定是連續的,如果是這樣這一步就會返回NULL。在這一步中如果需要還會將大的記憶體塊分解成小的記憶體塊來進行分配,即按照夥伴系統的要求進行分配。
  3. 無論是分配單頁還是多個頁,如果分配成功,在返回分配的頁之前都要呼叫prep_new_page,如果這一步的處理不成功就會重新進行分配(跳轉到函式buffered_rmqueue的開始),否則返回分配的頁。
函式__rmqueue的執行過程:
  1. 首先呼叫__rmqueue_smallest嘗試根據指定的zone,分配的階,遷移型別進行分配,該函式根據指定的資訊進行查詢,在找到一個可用的空閒記憶體頁塊後會將該記憶體頁塊從空閒記憶體頁塊連結串列中刪除,並且會呼叫expand使得剩餘的記憶體頁塊滿足夥伴系統的要求。如果在這一步成功就返回,否則執行下一步
  2. 呼叫__rmqueue_fallback嘗試從備用zone分配。該函式用於根據前一型別的備用列表嘗試從其它備用列表分配,但是需要注意的是這裡會首先嚐試最大的分配階,依次降低分配的階,直到指定的分配的階,採用這個策略是為了避免碎片—如果要用其它遷移型別的記憶體,就拿一塊大的過來,而不是在其它遷移型別的小區域中到處引入碎片。同時如果從其它遷移型別的空閒記憶體頁塊分配到的是一個較大的階,則整塊記憶體頁塊的遷移型別可能會發生改變,從原來的型別改變為申請分配時所請求的型別(即遷移型別發生了改變)。分配成功時的動作和__rmqueue_smallest類似,移出記憶體頁,呼叫expand。
函式prep_new_page的操作
  1. 對頁進行檢查,以確保頁確實是可用的,否則就返回一個非0值導致分配失敗
  2. 設定頁的標記以及引用計數等等。
  3. 如果設定而來__GFP_COMP標誌,則呼叫prep_compound_page將頁組織成複合頁(hugetlb會用到這個)。
複合頁的結構如圖所示:


複合頁具有如下特性:

  • 複合頁中第一個頁稱為首頁,其它所擁有頁都稱為尾頁
  • 組成複合頁的所有的private域都指向首頁
  • 第一個尾頁的lru的next域指向釋放複合頁的函式指標
  • 第一個尾頁的lru的prev域用於指向複合頁所對應的分配的階,即多少個頁

2.釋放頁

__free_pages是釋放頁的核心函式,夥伴系統提供出去的API都是它的包裝器。其流程:

  1. 減小頁的引用計數,如果計數不為0則直接返回,否則
  2.  如果釋放的是單頁,則呼叫free_hot_cold_page,否則
  3.  呼叫__free_pages_ok
free_hot_cold_page會把頁返還給每-CPU快取而不是直接返回給夥伴系統,因為如果每次都返還給夥伴系統,那麼將會出現每次的分配和釋放都需要夥伴系統進行分割和合並的情況,這將極大的降低分配的效率。因而這裡採用的是一種“惰性合併”,單頁會首先返還給每-CPU快取,當每-CPU快取的頁面數大於一個閾值時(pcp->high),則一次將pcp->patch個頁返還給夥伴系統。free_pcppages_bulk在free_hot_cold_page中用於將記憶體頁返還給夥伴系統,它會呼叫函式__free_one_page。
函式__free_pages_ok最終頁會調到__free_one_page來釋放頁,__free_one_page會將頁面釋放返還給夥伴系統,同時在必要時進行遞迴合併。
在__free_one_page進行合併時,需要找到釋放的page的夥伴的頁幀號,這是通過__find_buddy_index來完成的,其程式碼非常簡單:
__find_buddy_index(unsigned long page_idx,unsigned int order)
{
       returnpage_idx ^ (1 << order);
}
根據異或的規則,這個結果剛好可以得到鄰居的頁幀號。因為根據linux的管理策略以及夥伴系統的定義,夥伴系統中每個記憶體頁塊的第一個頁幀號用來標誌該頁,因此對於order階的兩個夥伴,它們只有1<<order這個位元位是不同的,這樣,只需要將該位元與取反即可,而根據異或的定義,一個位元和0異或還是本身,一個位元和1異或剛好可以取反。因此就得到了這個算式。
如果可以合併還需要取得合併後的頁幀號,這個更簡單,只需要讓兩個夥伴的頁幀號相與即可。
__free_one_page呼叫page_is_buddy來對夥伴進行判斷,以決定是否可以合併。

六、不連續記憶體頁的分配

核心總是嘗試使用物理上連續的記憶體區域,但是在分配記憶體時,可能無法找到大片的物理上連續的記憶體區域,這時候就需要使用不連續的記憶體,核心分配了其虛擬地址空間的一部分(vmalloc區)用於管理不連續記憶體頁的分配。
每個vmalloc分配的子區域都自包含的,在核心的虛擬地址空間中vmalloc子區域之間都通過一個記憶體頁隔離開來,這個間隔用來防止不正確的訪問。

1.用vmalloc分配記憶體

vmalloc用來分配在虛擬地址空間連續,但是在實體地址空間不一定連續的記憶體區域。它只需要一個以位元組為單位的長度引數。為了節省寶貴的較低端的記憶體區域,vmalloc會使用高階記憶體進行分配。
核心使用struct vm_struct來管理vmalloc分配的每個子區域,其定義如下:
struct vm_struct {
	struct vm_struct	*next;
	void			*addr;
	unsigned long		size;
	unsigned long		flags;
	struct page		**pages;
	unsigned int		nr_pages;
	phys_addr_t		phys_addr;
	const void		*caller;
};
每個vmalloc子區域都對應一個該結構的例項。
  • next:指向下一個vmalloc子區域
  • addr:vmalloc子區域在核心虛擬地址空間的起始地址
  • size:vmalloc子區域的長度
  • flags:與該區域相關標誌
  • pages:指標,指向對映到虛擬地址空間的實體記憶體頁的struct page例項
  • nr_pages:對映的物理頁面數目
  • phys_addr:僅當用ioremap映射了由實體地址描述的記憶體頁時才需要改域,它儲存實體地址
  • caller:申請者

2.建立vmalloc子區域

所有的vmalloc子區域都被連線儲存在vmlist中,該連結串列按照addr排序,順序是從小到大。當建立一個新的子區域時需要,需要找到一個合適的位置。查詢合適的位置採用的是首次適用演算法,即從vmalloc區域找到第一個可以滿足需求的區域,查詢這樣的區域是通過函式__get_vm_area_node完成的。其分配過程以下幾步:
  1. 呼叫__get_vm_area_node找到合適的區域
  2. 呼叫__vmalloc_area_node分配實體記憶體頁
  3. 呼叫map_vm_area將實體記憶體頁對映到核心的讀你地址空間
  4. 將新的子區域插入vmlist連結串列
在從夥伴系統分配實體記憶體頁時使用了標誌:GFP_KERNEL | __GFP_HIGHMEM
還有其它的方式來建立虛擬地址空間的連續對映:
  1. vmalloc_32:與vmallo工作方式相同,但是確保所使用的實體地址總可以用32位指標定址
  2. vmap:將一組物理頁面對映到連續的虛擬地址空間
  3. ioremap:特定於處理器的分配函式,用於將取自實體地址空間而、由系統匯流排用於I/O操作的一個記憶體塊,對映到核心的虛擬地址空間

3.釋放記憶體

vfree用於釋放vmalloc和vmalloc_32分配的記憶體空間,vunmap用於釋放由vmap和ioremap分配的空間(iounmap會調到vunmap)。最終都會歸結到函式__vunmap。
__vunmap的執行過程:
  1. 呼叫remove_vm_area從vmlist中找到一個子區域,然後將其從子區域刪除,再解除物理頁面的對映
  2. 如果設定了deallocate_pages,則將物理頁面歸還給夥伴系統
  3. 釋放管理虛擬記憶體的資料結構struct vm_struct

七、核心對映

高階記憶體可通過vmalloc機制對映到核心的虛擬地址空間,但是高階記憶體往核心虛擬地址空間的對映並不依賴於vmalloc,而vmalloc是用於管理不連續記憶體的,它也並不依賴於高階記憶體。

1.持久核心對映

如果想要將高階記憶體長期對映到核心中,則必須使用kmap函式。該函式需要一個page指標用於指向需要對映的頁面。如果沒有啟用高階記憶體,則該函式直接返回頁的地址,因為所有頁面都可以直接對映。如果啟用了高階記憶體,則:
  • 如果不是高階記憶體的頁面,則直接返回頁面地址,否則
  • 呼叫kmap_high進行處理

1.使用的資料結構

vmalloc區域後的持久對映區域用於建立持久對映。pkmap_count是一個有LAST_PKMAP個元素的陣列,每個元素對應一個持久對映。每個元素的值是被對映頁的一個使用計數器:
  1. 0:相關的頁麼有被使用
  2. 1:該位置關聯的頁已經對映,但是由於CPU的TLB沒有重新整理而不能使用
  3. 大於1的其它值:表示該頁的引用計數,n表示有n-1處在使用該頁
資料結構
struct page_address_map {
	struct page *page;
	void *virtual;
	struct list_head list;
};
用於建立物理頁和其在虛擬地址空間位置之間的關係。
  • page:指向全域性資料結構mem_map陣列中的page例項的指標
  • virtual:該頁在虛擬地址空間中分配的位置
所有的持久對映儲存在一個散列表page_address_htable中,並用連結串列處理衝突,page_slot是雜湊函式。
函式page_address用於根據page例項獲取器對應的虛擬地址。其處理過程:
  1. 如果不是高階記憶體直接根據page獲得虛擬地址(利用__va(paddr)),否則
  2. 在散列表中查詢該page對應的struct page_address_map例項,獲取其虛擬地址

2.建立對映

函式kmap_high完成對映的實際建立,其工作過程:
  1. 呼叫page_address獲取對應的虛擬地址
  2. 如果沒有獲取到,則呼叫map_new_virtual獲取虛擬地址
  3. pkmap_count陣列中對應於該虛擬地址的元素的引用計數加1
新對映的建立在map_new_virtual中完成,其工作過程:
 
  1. 執行一個無限迴圈:
    1. 更新last_pkmap_nr為last_pkmap_nr+1
    2. 同時如果last_pkmap_nr為0,呼叫flush_all_zero_pkmaps,flush CPU快取記憶體
    3. 檢查pkmap_count陣列中索引last_pkmap_nr對應的元素的引用計數是否為0,如果是0就退出迴圈,否則
    4. 將自己加入到一個等待佇列
    5. 排程其它任務
    6. 被喚醒時會首先檢查是否有其它任務已經完成了新對映的建立,如果是就直接返回
    7. 回到迴圈頭部重新執行
  2. 獲取與該索引對應的虛擬地址
  3. 修改核心頁表,將該頁對映到獲取到的虛擬地址
  4. 更新該索引對應的pkmap_count元素的引用計數為1
  5. 呼叫set_page_address將新的對映加入到page_address_htable中
flush_all_zero_pkmaps的工作過程:
  1. 呼叫flush_cache_kmaps執行快取記憶體flush動作
  2. 遍歷pkmap_count中的元素,如果某個元素的值為1就將其減小為0,並刪除相關對映同時設定需要重新整理標記
  3. 如果需要重新整理,則呼叫flush_tlb_kernel_range重新整理指定的區域對應的tlb。

3.解除對映

kunmap用於解除kmap建立的對映,如果不是高階記憶體,什麼都不做,否則kunmap_high將完成實際的工作。kunmap_high的工作很簡單,將對應的pkmap_count中的元素的引用計數的值減1,如果新值為1,則看是否有任務在pkmap_map_wait上等待,如果有就喚醒它。根據該機制的涉及原理,該函式不能將引用計數減小到小於1,否則就是一個BUG。

2.臨時核心對映

kmap不能用於無法休眠的上線文,如果要在不可休眠的上下文呼叫,則需要呼叫kmap_atomic。它是原子的,特定於架構的。同樣的只有是高階記憶體時才會做實際的對映。
kmap_atomic使用了固定對映機制。在固定對映區域,系統中每個CPU都有一個對應的“視窗”,每個視窗對應於KM_TYPE_NR中不同的型別都有一項。這個對映的核心程式碼如下(取自powerpc):
	type = kmap_atomic_idx_push();
	idx = type + KM_TYPE_NR*smp_processor_id();
	vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx); 	
        __set_pte_at(&init_mm, vaddr, kmap_pte-idx, mk_pte(page, prot), 1);
	local_flush_tlb_page(NULL, vaddr);
固定對映區域為用於kmap_atomic預留記憶體區的程式碼如下:
enum fixed_addresses {
	FIX_HOLE,
	/* reserve the top 128K for early debugging purposes */
	FIX_EARLY_DEBUG_TOP = FIX_HOLE,
	FIX_EARLY_DEBUG_BASE = FIX_EARLY_DEBUG_TOP+((128*1024)/PAGE_SIZE)-1,
<strong>#ifdef CONFIG_HIGHMEM
	FIX_KMAP_BEGIN,	/* reserved pte's for temporary kernel mappings */
	FIX_KMAP_END = FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1,
#endif</strong>
	/* FIX_PCIE_MCFG, */
	__end_of_fixed_addresses
};