x86 kvm 影子 MMU¶
mmu(在 arch/x86/kvm 中,檔案 mmu.[ch] 和 paging_tmpl.h)負責向客戶機呈現標準的 x86 mmu,同時將客戶機物理地址轉換為主機物理地址。
mmu 程式碼嘗試滿足以下要求
- 正確性
客戶機不應該能夠確定它執行在模擬的 mmu 上,除了時間(我們嘗試遵守規範,而不是模擬特定實現的特徵,例如 tlb 大小)
- 安全性
客戶機不能觸及未分配給它的主機記憶體
- 效能
最小化 mmu 帶來的效能損失
- 縮放
需要縮放到大記憶體和大 vcpu 客戶機
- 硬體
支援全系列的 x86 虛擬化硬體
- 整合
Linux 記憶體管理程式碼必須控制客戶機記憶體,以便交換、頁面遷移、頁面合併、透明巨頁和類似功能可以正常工作而無需更改
- 髒跟蹤
報告對客戶機記憶體的寫入,以實現即時遷移和基於幀緩衝區的顯示
- 佔用空間
保持鎖定的核心記憶體量較低(大多數記憶體應該是可收縮的)
- 可靠性
避免多頁或 GFP_ATOMIC 分配
縮寫¶
pfn |
主機頁幀號 |
hpa |
主機物理地址 |
hva |
主機虛擬地址 |
gfn |
客戶機幀號 |
gpa |
客戶機物理地址 |
gva |
客戶機虛擬地址 |
ngpa |
巢狀客戶機物理地址 |
ngva |
巢狀客戶機虛擬地址 |
pte |
頁表條目(也用於泛指分頁結構條目) |
gpte |
客戶機 pte(指 gfn) |
spte |
影子 pte(指 pfn) |
tdp |
二維分頁(NPT 和 EPT 的供應商中立術語) |
支援的虛擬和真實硬體¶
mmu 支援第一代 mmu 硬體,它允許在客戶機進入期間原子切換當前分頁模式和 cr3,以及二維分頁(AMD 的 NPT 和 Intel 的 EPT)。 它暴露的模擬硬體是傳統的 2/3/4 級 x86 mmu,支援全域性頁、pae、pse、pse36、cr0.wp 和 1GB 頁。 模擬硬體還能夠在支援 NPT 的主機上公開支援 NPT 的硬體。
翻譯¶
mmu 的主要工作是程式設計處理器的 mmu 以轉換客戶機的地址。 在不同的時間需要不同的翻譯
當客戶機分頁被停用時,我們將客戶機物理地址翻譯為主機物理地址 (gpa->hpa)
當客戶機分頁啟用時,我們將客戶機虛擬地址翻譯為客戶機物理地址,再翻譯為主機物理地址 (gva->gpa->hpa)
當客戶機啟動自己的客戶機時,我們將巢狀客戶機虛擬地址翻譯為巢狀客戶機物理地址,再翻譯為客戶機物理地址,再翻譯為主機物理地址 (ngva->ngpa->gpa->hpa)
主要的挑戰是將 1 到 3 個翻譯編碼到僅支援 1 個(傳統)和 2 個 (tdp) 翻譯的硬體中。 當所需翻譯的數量與硬體匹配時,mmu 以直接模式執行;否則,它以影子模式執行(參見下文)。
記憶體¶
客戶機記憶體 (gpa) 是使用 kvm 的程序的使用者地址空間的一部分。 使用者空間定義了客戶機地址和使用者地址之間的翻譯 (gpa->hva); 請注意,兩個 gpa 可能別名到同一個 hva,但反之則不然。
這些 hva 可以使用主機可用的任何方法進行支援:匿名記憶體、檔案支援記憶體和裝置記憶體。 記憶體可能隨時被主機分頁。
事件¶
mmu 由事件驅動,一些來自客戶機,一些來自主機。
客戶機生成的事件
寫入控制暫存器(尤其是 cr3)
invlpg/invlpga 指令執行
訪問丟失或受保護的翻譯
主機生成的事件
gpa->hpa 翻譯的更改(透過 gpa->hva 更改或 hva->hpa 更改)
記憶體壓力(收縮器)
影子頁面¶
主要資料結構是影子頁面,“struct kvm_mmu_page”。 一個影子頁面包含 512 個 sptes,可以是葉子 spte,也可以是非葉子 spte。 一個影子頁面可能包含葉子 spte 和非葉子 spte 的混合。
非葉子 spte 允許硬體 mmu 到達葉子頁面,並且不直接與翻譯相關。 它指向其他影子頁面。
葉子 spte 對應於編碼到一個分頁結構條目中的一個或兩個翻譯。 這些始終是翻譯堆疊的最低級別,可選的更高級別翻譯留給 NPT/EPT。 葉子 pte 指向客戶機頁面。
下表顯示了葉子 pte 編碼的翻譯,更高級別的翻譯在括號中
非巢狀客戶機
nonpaging: gpa->hpa paging: gva->gpa->hpa paging, tdp: (gva->)gpa->hpa巢狀客戶機
non-tdp: ngva->gpa->hpa (*) tdp: (ngva->)ngpa->gpa->hpa (*) the guest hypervisor will encode the ngva->gpa translation into its page tables if npt is not present
- 影子頁面包含以下資訊
- role.level
此影子頁面所屬的影子分頁層次結構中的級別。 1=4k sptes, 2=2M sptes, 3=1G sptes, 等等。
- role.direct
如果設定,從此頁面可訪問的葉子 sptes 用於線性範圍。 示例包括真實模式翻譯、由小主機頁面支援的大客戶機頁面,以及 NPT 或 EPT 啟用時的 gpa->hpa 翻譯。 線性範圍從 (gfn << PAGE_SHIFT) 開始,其大小由 role.level 確定(第一級為 2MB,第二級為 1GB,第三級為 0.5TB,第四級為 256TB)如果清除,此頁面對應於由 gfn 欄位表示的客戶機頁表。
- role.quadrant
當 role.has_4_byte_gpte=1 時,客戶機使用 32 位 gptes,而主機使用 64 位 sptes。 這意味著客戶機頁表包含比主機更多的 ptes,因此需要多個影子頁面來影子一個客戶機頁面。 對於第一級影子頁面,role.quadrant 可以是 0 或 1,表示客戶機頁表中的第一個或第二個 512-gpte 塊。 對於第二級頁表,每個 32 位 gpte 都轉換為兩個 64 位 sptes(因為每個第一級客戶機頁面都由兩個第一級影子頁面影子),因此 role.quadrant 取值範圍為 0..3。 每個象限對映 1GB 虛擬地址空間。
- role.access
從父 ptes 繼承的客戶機訪問許可權,格式為 uwx。 注意,執行許可權是正的,而不是負的。
- role.invalid
頁面無效,不應使用。 它是一個當前被釘住的根頁面(由指向它的 cpu 硬體暫存器); 一旦它被取消釘住,它將被銷燬。
- role.has_4_byte_gpte
反映了頁面有效的客戶機 PTE 的大小,即,如果使用直接對映或 64 位 gptes,則為“0”; 如果使用 32 位 gptes,則為“1”。
- role.efer_nx
包含頁面有效的 efer.nx 的值。
- role.cr0_wp
包含頁面有效的 cr0.wp 的值。
- role.smep_andnot_wp
包含 cr4.smep && !cr0.wp 的值,頁面有效(此值為 true 的頁面與其他頁面不同;請參見下面 cr0.wp=0 的處理)。
- role.smap_andnot_wp
包含 cr4.smap && !cr0.wp 的值,頁面有效(此值為 true 的頁面與其他頁面不同;請參見下面 cr0.wp=0 的處理)。
- role.smm
如果頁面在系統管理模式下有效,則為 1。 此欄位確定了用於構建此影子頁面的 kvm_memslots 陣列; 它也用於透過 kvm_memslots_for_spte_role 宏和 __gfn_to_memslot 從 struct kvm_mmu_page 返回到 memslot。
- role.ad_disabled
如果 MMU 例項無法使用 A/D 位,則為 1。 EPT 在 Haswell 之前沒有 A/D 位; 如果 L1 虛擬機器監控程式不啟用它們,影子 EPT 頁表也不能使用 A/D 位。
- role.guest_mode
指示影子頁面是為巢狀客戶機建立的。
- role.passthrough
頁面不受客戶機頁表的支援,但其第一個條目指向一個。 如果 NPT 使用 5 級頁表(主機 CR4.LA57=1)並且影子 L1 的 4 級 NPT (L1 CR4.LA57=0),則設定此標誌。
- mmu_valid_gen
此頁面的 MMU 生成,用於在 VM 中快速清除所有 MMU 頁面,而不會阻塞 vCPU 太長時間。 具體來說,KVM 更新每個 VM 的有效 MMU 生成,導致每個 mmu 頁面的 mmu_valid_gen 不匹配。 這使得所有現有的 MMU 頁面都過時。 過時的頁面不能使用。 因此,vCPU 必須在重新進入客戶機之前載入一個新的有效根。 MMU 生成始終為“0”或“1”。 注意,TDP MMU 不使用此欄位,因為非根 TDP MMU 頁面只能從它們自己的根訪問。 因此,TDP MMU 只需在根頁面中使用 role.invalid 即可使所有 MMU 頁面無效。
- gfn
要麼是包含由此頁面影子的翻譯的客戶機頁表,要麼是線性翻譯的基本頁幀。 請參見 role.direct。
- spt
一個充滿 64 位 sptes 的頁面,包含此頁面的翻譯。 可由 kvm 和硬體訪問。 spt 指向的頁面的 page->private 將指回影子頁面結構。 spt 中的 sptes 要麼指向客戶機頁面,要麼指向較低級別的影子頁面。 具體來說,如果 sp1 和 sp2 是影子頁面,則 sp1->spt[n] 可以指向 __pa(sp2->spt)。 sp2 將透過 parent_pte 指回 sp1。 spt 陣列形成一個 DAG 結構,其中影子頁面作為節點,客戶機頁面作為葉子。
- shadowed_translation
一個包含 512 個影子翻譯條目的陣列,每個存在的 pte 一個。 用於從 pte 反向對映到 gfn 以及其訪問許可權。 當設定 role.direct 時,shadow_translation 陣列不會分配。 這是因為當使用時,此陣列的任何元素中包含的 gfn 都可以從 gfn 欄位計算出來。 此外,當設定 role.direct 時,KVM 不跟蹤每個 gfn 的訪問許可權。 請參見 role.direct 和 gfn。
- root_count / tdp_mmu_root_count
root_count 是 Shadow MMU 中根影子頁面的引用計數器。 vCPU 在獲取將用作根頁面的影子頁面時會提高引用計數,即,將直接載入到硬體中的頁面(CR3、PDPTR、nCR3 EPTP)。 當引用計數非零時,根頁面不能被銷燬。 請參見 role.invalid。 tdp_mmu_root_count 類似,但專門在 TDP MMU 中用作原子引用計數。
- parent_ptes
指向此頁面的 spt 的 pte/ptes 的反向對映。 如果 parent_ptes 位 0 為零,則只有一個 spte 指向此頁面,並且 parent_ptes 指向這個單個 spte,否則,存在多個 sptes 指向此頁面,並且 (parent_ptes & ~0x1) 指向具有父 sptes 列表的資料結構。
- ptep
指向此影子頁面的 SPTE 的核心虛擬地址。 此欄位專門由 TDP MMU 使用,它是與 parent_ptes 的聯合。
- unsync
如果為 true,則此頁面中的翻譯可能與客戶機的翻譯不匹配。 這相當於 tlb 的狀態,當 pte 被更改但在 tlb 條目被重新整理之前。 因此,當客戶機執行 invlpg 或透過其他方式重新整理其 tlb 時,unsync ptes 會同步。 對葉子頁面有效。
- unsync_children
頁面中有多少個 sptes 指向 unsync 的頁面(或具有未同步的子頁面)。
- unsync_child_bitmap
一個位圖,指示 spt 中哪些 sptes(直接或間接地)指向可能未同步的頁面。 用於快速定位從給定頁面可訪問的所有未同步頁面。
- clear_spte_count
僅在 32 位主機上存在,其中 64 位 spte 不能以原子方式寫入。 讀取器在執行 MMU 鎖定時使用此值來檢測正在進行的更新並重試它們,直到寫入器完成寫入。
- write_flooding_count
客戶機可能會多次寫入頁表,如果頁面需要防寫,則會導致大量模擬(參見下面的“同步和未同步頁面”)。 葉子頁面可以不同步,這樣它們就不會觸發頻繁的模擬,但對於非葉子頁面來說這是不可能的。 此欄位計算自上次實際使用頁表以來的模擬次數; 如果在此頁面上過於頻繁地觸發模擬,KVM 將取消對映該頁面以避免將來進行模擬。
- tdp_mmu_page
如果影子頁面是 TDP MMU 頁面,則為 1。 當 KVM 遍歷任何可能包含來自 TDP MMU 和影子 MMU 的頁面的資料結構時,此變數用於分叉控制流。
反向對映¶
mmu 維護一個反向對映,由此給定頁面的所有 ptes 都可以透過其 gfn 訪問。 例如,這在交換頁面時使用。
同步和未同步頁面¶
客戶機使用兩個事件來同步其 tlb 和頁表:tlb 重新整理和頁面失效 (invlpg)。
tlb 重新整理意味著我們需要同步從客戶機的 cr3 可訪問的所有 sptes。 這很昂貴,因此我們保持所有客戶機頁表的防寫,並在寫入 gpte 時將 sptes 同步到 gptes。
一個特殊情況是客戶機頁表可以從當前客戶機 cr3 訪問。 在這種情況下,客戶機有義務在使用翻譯之前發出 invlpg 指令。 我們透過刪除客戶機頁面的防寫並允許客戶機自由修改它來利用這一點。 當客戶機呼叫 invlpg 時,我們會同步修改後的 gptes。 這減少了當客戶機修改多個 gptes 時,或者當客戶機頁面不再用作頁表而用於隨機客戶機資料時,我們必須執行的模擬量。
作為副作用,我們必須在 tlb 重新整理時重新同步所有可訪問的未同步影子頁面。
對事件的反應¶
客戶機頁面錯誤(或 npt 頁面錯誤,或 ept 違規)
這是最複雜的事件。 頁面錯誤的根本原因可能是
真正的客戶機錯誤(客戶機翻譯不允許訪問)(*)
訪問丟失的翻譯
訪問受保護的翻譯 - 當記錄髒頁時,記憶體受到防寫 - 同步的影子頁面受到防寫 (*)
訪問不可翻譯的記憶體 (mmio)
(*) 不適用於直接模式
頁面錯誤的處理方式如下
如果錯誤程式碼的 RSV 位被設定,則頁面錯誤是由客戶機訪問 MMIO 引起的,並且快取的 MMIO 資訊可用。
遍歷影子頁表
檢查 spte 中有效的生成號(參見下面的“快速失效 MMIO sptes”)
將資訊快取到 vcpu->arch.mmio_gva、vcpu->arch.mmio_access 和 vcpu->arch.mmio_gfn,並呼叫模擬器
如果錯誤程式碼的 P 位和 R/W 位都被設定,則這可能被處理為“快速頁面錯誤”(無需獲取 MMU 鎖即可修復)。 請參見 KVM 鎖概述 中的描述。
如果需要,遍歷客戶機頁表以確定客戶機翻譯 (gva->gpa 或 ngpa->gpa)
如果許可權不足,將錯誤反映回客戶機
確定主機頁面
如果是 mmio 請求,則沒有主機頁面; 將資訊快取到 vcpu->arch.mmio_gva、vcpu->arch.mmio_access 和 vcpu->arch.mmio_gfn
遍歷影子頁表以找到翻譯的 spte,根據需要例項化丟失的中間頁表
如果是 mmio 請求,將 mmio 資訊快取到 spte 並設定 spte 上的一些保留位(參見 kvm_mmu_set_mmio_spte_mask 的呼叫者)
嘗試不同步頁面
如果成功,我們可以讓客戶機繼續並修改 gpte
模擬指令
如果失敗,取消影子頁面並讓客戶機繼續
更新任何被指令修改的翻譯
invlpg 處理
遍歷影子頁面層次結構並刪除受影響的翻譯
嘗試重新例項化指示的翻譯,希望客戶機在不久的將來使用它
客戶機控制暫存器更新
mov to cr3
查詢新的影子根
同步新可訪問的影子頁面
mov to cr0/cr4/efer
為新的分頁模式設定 mmu 上下文
查詢新的影子根
同步新可訪問的影子頁面
主機翻譯更新
mmu 通知器被呼叫並更新了 hva
透過反向對映查詢受影響的 sptes
刪除(或更新)翻譯
模擬 cr0.wp¶
如果未啟用 tdp,則主機必須保持 cr0.wp=1,以便頁面防寫適用於客戶機核心,而不是客戶機使用者空間。 當客戶機 cr0.wp=1 時,這不會出現問題。 但是當客戶機 cr0.wp=0 時,我們無法將 gpte.u=1, gpte.w=0 的許可權對映到任何 spte(語義要求允許任何客戶機核心訪問加上使用者讀取訪問)。
我們透過將許可權對映到兩個可能的 sptes 來處理此問題,具體取決於錯誤型別
核心寫入錯誤:spte.u=0, spte.w=1(允許完全核心訪問,禁止使用者訪問)
讀取錯誤:spte.u=1, spte.w=0(允許完全讀取訪問,禁止核心寫入訪問)
(使用者寫入錯誤會生成 #PF)
在第一種情況下,還有兩個額外的複雜性
如果 CR4.SMEP 被啟用:由於我們將頁面變成了核心頁面,因此核心現在可以執行它。 我們透過也設定 spte.nx 來處理此問題。 如果我們收到使用者提取或讀取錯誤,我們將更改 spte.u=1 並且 spte.nx=gpte.nx 返回。 為了使這起作用,當使用影子分頁時,KVM 強制 EFER.NX 為 1。
如果 CR4.SMAP 被停用:由於頁面已更改為核心頁面,因此當 CR4.SMAP 被啟用時無法重複使用它。 我們將 CR4.SMAP && !CR0.WP 設定到影子頁面的 role 中以避免這種情況。 注意,這裡我們不關心 CR4.SMAP 被啟用的情況,因為 KVM 將由於許可權檢查失敗而直接將 #PF 注入到客戶機。
為了防止在 cr0.wp 更改為 1 後,一個被轉換為具有 cr0.wp=0 的核心頁面的 spte 被核心寫入,我們將 cr0.wp 的值作為頁面 role 的一部分。 這意味著使用 cr0.wp 的一個值建立的 spte 不能在 cr0.wp 具有不同值時使用 - 它將簡單地被影子頁面查詢程式碼錯過。 當在使用 cr0.wp=0 和 cr4.smep=0 建立的 spte 在將 cr4.smep 更改為 1 之後使用時,存在類似的問題。 為了避免這種情況,!cr0.wp && cr4.smep 的值也成為頁面 role 的一部分。
大頁面¶
mmu 支援大客戶機頁面和小客戶機頁面的所有組合。 支援的頁面大小包括 4k、2M、4M 和 1G。 4M 頁面被視為兩個單獨的 2M 頁面,無論是在客戶機上還是在主機上,因為 mmu 始終使用 PAE 分頁。
要例項化一個大的 spte,必須滿足四個約束
spte 必須指向一個大的主機頁面
客戶機 pte 必須是至少等效大小的大 pte(如果啟用了 tdp,則沒有客戶機 pte,並且滿足此條件)
如果 spte 將是可寫的,則大頁面幀可能不會與任何防寫頁面重疊
客戶機頁面必須完全包含在單個記憶體插槽中
為了檢查最後兩個條件,mmu 為每個記憶體插槽和大頁面大小維護一組 ->disallow_lpage 陣列。 每個防寫頁面都會導致其 disallow_lpage 遞增,從而阻止例項化大的 spte。 未對齊的記憶體插槽末尾的幀具有人為膨脹的 ->disallow_lpages,因此它們永遠無法被例項化。
快速失效 MMIO sptes¶
如上面的“對事件的反應”中所述,kvm 會將 MMIO 資訊快取在葉子 sptes 中。 當新增一個新的 memslot 或更改現有的 memslot 時,此資訊可能會過時並且需要失效。 這還需要在遍歷所有影子頁面時保持 MMU 鎖定,並且使用類似的技術使其更具可伸縮性。
MMIO sptes 有一些備用位,這些位用於儲存生成號。 全域性生成號儲存在 kvm_memslots(kvm)->generation 中,並且每當客戶機記憶體資訊更改時都會增加。
當 KVM 找到 MMIO spte 時,它會檢查 spte 的生成號。 如果 spte 的生成號不等於全域性生成號,它將忽略快取的 MMIO 資訊並透過慢速路徑處理頁面錯誤。
由於只有 18 位用於儲存 mmio spte 上的生成號,因此當發生溢位時,所有頁面都會被清除。
不幸的是,單個記憶體訪問可能會多次訪問 kvm_memslots(kvm),最後一次訪問發生在檢索生成號並將其儲存到 MMIO spte 中時。 因此,MMIO spte 可能是基於過時的資訊建立的,但具有最新的生成號。
為了避免這種情況,在 synchronize_srcu 返回後,生成號會再次遞增; 因此,kvm_memslots(kvm)->generation 的位 63 僅在 memslot 更新期間設定為 1,而某些 SRCU 讀取器可能正在使用舊副本。 我們不希望使用使用奇數生成號建立的 MMIO sptes,並且我們可以在不丟失 MMIO spte 中的位的情況下做到這一點。“正在進行的更新”位生成號不儲存在 MMIO spte 中,因此當從 spte 中提取生成時,它隱式為零。 如果 KVM 不走運並在正在進行的更新期間建立 MMIO spte,則對 spte 的下一次訪問將始終是快取未命中。 例如,在更新視窗期間的後續訪問將由於正在進行的標誌差異而未命中,而在更新視窗關閉後的訪問將具有更高的生成號(與 spte 相比)。
進一步閱讀¶
來自 KVM 論壇 2008 的 NPT 簡報 https://www.linux-kvm.org/images/c/c8/KvmForum2008%24kdf2008_21.pdf