內存虛擬化簡介
前一章介紹了CPU虛擬化的內容,這一章介紹一下KVM的內存虛擬化原理。能夠說內存是除了CPU外最重要的組件,Guest最終使用的仍是宿主機的內存,因此內存虛擬化其實就是關於如何作Guest到宿主機物理內存之間的各類地址轉換,如何轉換會讓轉換效率更高呢,KVM經歷了三代的內存虛擬化技術,大大加快了內存的訪問速率。html
傳統的地址轉換
在保護模式下,普通的應用進程使用的都是本身的虛擬地址空間,一個64位的機器上的每個進程均可以訪問0到2^64的地址範圍,實際上內存並無這麼多,也不會給你這麼多。對於進程而言,他擁有全部的內存,對內核而言,只分配了一小段內存給進程,待進程須要更多的進程的時候再分配給進程。
一般應用進程所使用的內存叫作虛擬地址,而內核所使用的是物理內存。內核負責爲每一個進程維護虛擬地址到物理內存的轉換關係映射。
首先,邏輯地址須要轉換爲線性地址,而後由線性地址轉換爲物理地址。測試
邏輯地址 ==> 線性地址 ==> 物理地址
邏輯地址和線性地址之間經過簡單的偏移來完成。
優化
一個完整的邏輯地址 = [段選擇符:段內偏移地址],查找GDT或者LDT(經過寄存器gdtr,ldtr)找到描述符,經過段選擇符(selector)前13位在段描述符作index,找到Base地址,Base+offset就是線性地址。ui
爲何要這麼作?聽說是Intel爲了保證兼容性。url
邏輯地址到線性地址的轉換在虛擬化中沒有太多的須要介紹的,這一層不存在實際的虛擬化操做,和傳統方式同樣,最重要的是線性地址到物理地址這一層的轉換。spa
傳統的線性地址到物理地址的轉換由CPU的頁式內存管理,頁式內存管理。
頁式內存管理負責將線性地址轉換到物理地址,一個線性地址被分五段描述,第一段爲基地址,經過與當前CR3寄存器(CR3寄存器每一個進程有一個,線程共享,當發生進程切換的時候,CR3被載入到對應的寄存器中,這也是各個進程的內存隔離的基礎)作運算,獲得頁表的地址index,經過四次運算,最終獲得一個大小爲4K的頁(有可能更大,好比設置了hugepages之後)。整個過程都是CPU完成,進程不須要參與其中,若是在查詢中發現頁已經存在,直接返回物理地址,若是頁不存在,那麼將產生一個缺頁中斷,內核負責處理缺頁中斷,並把頁加載到頁表中,中斷返回後,CPU獲取到頁地址後繼續進行運算。.net
KVM中的內存結構
因爲qemu-kvm進程在宿主機上做爲一個普通進程,那對於Guest而言,須要的轉換過程就是這樣。線程
Guest虛擬內存地址(GVA) | Guest線性地址 | Guest物理地址(GPA) | Guest ------------------ | HV HV虛擬地址(HVA) | HV線性地址 | HV物理地址(HPA)
What's the fu*k ?這麼多...
彆着急,Guest虛擬地址到HV線性地址之間的轉換和HV虛擬地址到線性地址的轉換過程能夠省略,這樣看起來就更清晰一點。3d
Guest虛擬內存地址(GVA) | Guest物理地址(GPA) | Guest ------------------ | HV HV虛擬地址(HVA) | HV物理地址(HPA)
前面也說到KVM經過不斷的改進轉換過程,讓KVM的內存虛擬化更加的高效,咱們從最初的軟件虛擬化的方式介紹。code
軟件虛擬化方式實現
第一層轉換,由GVA->GPA的轉換和傳統的轉換關係同樣,經過查找CR3而後進行頁表查詢,找到對應的GPA,GPA到HVA的關係由qemu-kvm負責維護,咱們在第二章KVM啓動過程的demo裏面就有介紹到怎樣給KVM映射內存,經過mmap的方式把HV的內存映射給Guest。
struct kvm_userspace_memory_region region = { .slot = 0, .guest_phys_addr = 0x1000, .memory_size = 0x1000, .userspace_addr = (uint64_t)mem, };
能夠看到,qemu-kvm的kvm_userspace_memory_region結構體描述了guest的物理地址起始位置和內存大小,而後描述了Guest的物理內存在HV的映射userspace_addr
,經過多個slot,能夠把不連續的HV的虛擬地址空間映射給Guest的連續的物理地址空間。
軟件模擬的虛擬化方式由qemu-kvm來負責維護GPA->HVA的轉換,而後再通過一次HVA->HPA的方式,從過程上來看,這樣的訪問是很低效的,特別是在當GVA到GPA轉換時候產生缺頁中斷,這時候產生一個異常Guest退出,HV捕獲異常後計算出物理地址(分配新的內存給Guest),而後從新Entry。這個過程會可能致使頻繁的Guest退出,且轉換過程過長。因而KVM使用了一種叫作影子頁表的技術。
影子頁表的虛擬化方式
影子頁表的出現,就是爲了減小地址轉換帶來的開銷,直接把GVA轉換到HVP的技術。在軟件虛擬化的內存轉換中,GVA到GPA的轉換經過查詢CR3寄存器來完成,CR3保存了Guest中的頁表基地址,而後載入MMU來作地址轉換。
在加入了影子頁表的技術後,當訪問到CR3寄存器的時候(多是因爲Guest進程後致使的),KVM捕獲到這個操做,CPU虛擬化章節 EXIT_REASON_CR_ACCESS,qemu-kvm經過載入特俗的CR3和影子頁表來欺騙Guest這個就是真實的CR3,後面的操做就和傳統的訪問內存的方式一致,當須要訪問物理內存的時候,只會通過一層的影子頁表的轉換。
影子頁表由qemu-kvm進程維護,實際上就是一個Guest的頁表到宿主機頁表的映射,每一級的頁表的hash值對應到qemu-kvm中影子頁表的一個目錄。在初次GVA->HPA的轉換時候,影子頁表沒有創建,此時Guest產生缺頁中斷,和傳統的轉換過程同樣,通過兩次轉換(VA->PA),而後影子頁表記錄GVA->GPA->HVA->HPA。這樣產生GVA->GPA的直接關係,保存到影子頁表中。
影子頁表的引入,減小了GVA->HPA的轉換過程,可是壞處在於qemu-kvm須要爲Guest的每一個進程維護一個影子頁表,這將帶來很大的內存開銷,同時影子頁表的創建是很耗時的,若是Guest進程過多,將致使頻繁的影子頁表的導入與導出,雖然用了cache技術,可是仍是軟件層面的,效率並非最好,因此Intel和AMD在此基礎上提供了硬件虛擬化技術。
EPT硬件加速的虛擬化方式
EPT(extended page table)能夠看作一個硬件的影子頁表,在Guest中經過增長EPT寄存器,當Guest產生了CR3和頁表的訪問的時候,因爲對CR3中的頁表地址的訪問是GPA,當地址爲空時候,也就是Page fault後,產生缺頁異常,若是在軟件模擬或者影子頁表的虛擬化方式中,此時會有VM退出,qemu-kvm進程接管並獲取到此異常。可是在EPT的虛擬化方式中,qemu-kvm忽略此異常,Guest並不退出,而是按照傳統的缺頁中斷處理,在缺頁中斷處理的過程當中會產生EXIT_REASON_EPT_VIOLATION,Guest退出,qemu-kvm捕獲到異常後,分配物理地址並創建GVA->HPA的映射,並保存到EPT中,將EPT載入到MMU,下次轉換時候直接查詢根據CR3查詢EPT表來完成GVA->HPA的轉換。之後的轉換都由硬件直接完成,大大提升了效率,且不須要爲每一個進程維護一套頁表,減小了內存開銷。
在筆者的測試中,Guest和HV的內存訪問速率對比爲3756MB/s對比4340MB/s。能夠看到內存訪問已經很接近宿主機的水平了。
總結
KVM內存的虛擬化就是一個將虛擬機的虛擬內存轉換爲宿主機物理內存的過程,Guest使用的依然是宿主機的物理內存,只是在這個過程當中怎樣減小轉換帶來的開銷成爲優化的主要點。 KVM通過軟件模擬->影子頁表->EPT的技術的進化,效率也愈來愈高。