Linux 系統(tǒng)虛擬化模型及障礙
x86架構(gòu)CPU虛擬化
Gerald J. Popek和Robert P. Goldberg在1974年發(fā)表的論文“Formal Requirements for Virtualizable[A1] [A2] Third Generation Architectures”中提出了符合虛擬化的3個條件:
(1)等價性,即VMM需要在宿主機(jī)上為虛擬機(jī)模擬出一個本質(zhì)上與物理機(jī)一致的環(huán)境。虛擬機(jī)在這個環(huán)境上運行與其在物理機(jī)上運行別無二致,除了可能因為資源競爭或者VMM的干預(yù)導(dǎo)致在虛擬環(huán)境中表現(xiàn)上略有差異,比如虛擬機(jī)的I/O、網(wǎng)絡(luò)等因宿主機(jī)的限速或者多個虛擬機(jī)共享資源,導(dǎo)致速度可能要比獨占物理機(jī)的慢一些。
(2)高效性,即虛擬機(jī)指令執(zhí)行的性能與其在物理機(jī)上運行相比并無明顯損耗。該標(biāo)準(zhǔn)要求虛擬機(jī)中的絕大部分指令無須VMM干預(yù)而直接運行在物理CPU上,比如我們在x86架構(gòu)上通過Qemu運行的ARM系統(tǒng)并不是虛擬化,而是仿真(Emulator)。
(3)資源控制,即VMM完全控制系統(tǒng)資源。由VMM控制協(xié)調(diào)宿主機(jī)資源給各個虛擬機(jī),而不能由虛擬機(jī)控制了宿主機(jī)的資源。
陷入和模擬模型
為了滿足GeraldJ. Popek和Robert P. Goldberg提出的滿足虛擬化的3個條件,一個典型的解決方案是Trap andEmulate模型。
一般來說,處理器可以歸結(jié)為兩種運行模式:系統(tǒng)模式和用戶模式。相應(yīng)的,CPU的指令也分為特權(quán)指令和非特權(quán)指令。特權(quán)指令只能在系統(tǒng)模式運行,如果特權(quán)指令運行在用戶模式就將觸發(fā)處理器異常。操作系統(tǒng)將內(nèi)核運行在系統(tǒng)模式,因為內(nèi)核需要管理系統(tǒng)資源,需要運行特權(quán)指令,而普通的用戶程序則運行在用戶模式。
在虛擬化場景下,虛擬機(jī)的用戶程序仍然運行在用戶模式,但是虛擬機(jī)的內(nèi)核將運行在用戶模式,這種方式稱為Ring Compression。在這種方式下,虛擬機(jī)中的非特權(quán)指令直接運行在處理器上,滿足了Popek和Goldberg提出的虛擬化標(biāo)準(zhǔn)中高效的要求,即指令的大部分無須VMM干預(yù)直接在處理器上運行。但是,當(dāng)虛擬機(jī)執(zhí)行特權(quán)指令時,因為是在用戶模式執(zhí)行特權(quán)指令,將觸發(fā)處理器異常,從而陷入到VMM中,由VMM代理虛擬機(jī)完成系統(tǒng)資源的訪問,即所謂的模擬(emulate)。如此,又滿足了Popek和Goldberg提出的虛擬化標(biāo)準(zhǔn)中VMM控制系統(tǒng)資源的要求,虛擬機(jī)將不會因為可以直接運行特權(quán)指令而修改宿主機(jī)的資源,從而破壞宿主機(jī)的環(huán)境。
x86架構(gòu)虛擬化的障礙
Gerald J. Popek和Robert P. Goldberg指出,修改系統(tǒng)資源的,或者在不同模式下行為有不同表現(xiàn)的,都屬于敏感指令。在虛擬化場景下,VMM需要監(jiān)測到這些敏感指令。一個支持虛擬化的體系架構(gòu)的敏感指令都屬于特權(quán)指令,即在非特權(quán)級別執(zhí)行這些敏感指令時,CPU會拋出異常,進(jìn)入VMM的異常處理函數(shù),從而實現(xiàn)了控制VM訪問敏感資源的目的。
但是,x86架構(gòu)恰恰不能滿足Gerald J. Popek和Robert P. Goldberg定義的這個準(zhǔn)則,且并不是所有的敏感指令都是特權(quán)指令,有些敏感指令在非特權(quán)模式下執(zhí)行時并不會拋出異常,此時VMM就無法攔截或者處理VM的行為。以修改FLAGS寄存器中的IF(interrupt flag)為例,我們首先使用指令pushfd將寄存器FLAGS的內(nèi)容壓到棧中,然后將棧頂?shù)腎F清0,最后使用popf指令從棧中恢復(fù)FLAGS寄存器。如果將虛擬機(jī)內(nèi)核運行在ring 1,x86的CPU并不會拋出異常,而只是默默地忽略指令popfd,因此虛擬機(jī)關(guān)閉IF的目的并沒有生效。
有人提出半虛擬化的方式,即修改Guest的代碼,但是這不符合虛擬化的透明準(zhǔn)則。后來,人們提出了二進(jìn)制翻譯的方式,包括靜態(tài)翻譯和動態(tài)翻譯。靜態(tài)翻譯就是在運行前,掃描整個可執(zhí)行文件,對敏感指令進(jìn)行翻譯,重新形成一個新的文件。靜態(tài)翻譯是有其局限性的,必須提前處理,而且有些指令只有在運行時才產(chǎn)生的副作用,無法靜態(tài)處理。于是,動態(tài)翻譯應(yīng)運而生,即在運行時以代碼塊為單元動態(tài)地修改二進(jìn)制代碼。動態(tài)翻譯在很多VMM中得到應(yīng)用,而且優(yōu)化的效果非常不錯。
VMX擴(kuò)展
雖然程序員們從軟件層面采用了多種方案去解決x86架構(gòu)在虛擬化方面的問題,但是軟件層的解決方案除了額外的開銷外,也給VMM的實現(xiàn)帶來了巨大的復(fù)雜性。于是,Intel嘗試從硬件層面解決這個問題。Intel并沒有將那些非特權(quán)的敏感指令修改為特權(quán)指令,因為并不是所有的特權(quán)指令都需要Trap and Emulate。我們舉個典型的例子,每當(dāng)操作系統(tǒng)內(nèi)核切換進(jìn)程時,都會切換cr3寄存器,使其指向當(dāng)前運行進(jìn)程的頁表。當(dāng)使用影子頁表進(jìn)行GVA到HPA的映射時,需要捕獲Guest的每一次設(shè)置cr3寄存器的操作,VMM模塊使其指向影子頁表。而當(dāng)啟用了硬件層面的EPT支持后,cr3仍然指向Guest的進(jìn)程頁表,無須捕捉Guest設(shè)置cr3寄存器的操作,也就是說,雖然寫cr3寄存器是特權(quán)指令,但是其不需要陷入VMM。
Intel開發(fā)了VT技術(shù)支持虛擬化,為CPU增加了Virtual-Machine Extensions,簡稱為VMX。一旦啟動了CPU的VMX支持,CPU將提供2種運行模式:VMX Root Mode和VMX non-Root Mode,每一種模式都支持ring0 ~ ring3。VMM運行在VMX RootMode,除了支持VMX外,VMX Root Mode和普通的模式并無本質(zhì)區(qū)別。VM運行在VMX non-Root Mode,Guest無須再采用Ring Compression方式,Guest kernel可以直接運行在VMX non-Root Mode的ring0,如圖1所示。
圖1 VMX運行模式
處在VMX RootMode的VMM可以通過執(zhí)行CPU提供的虛擬化指令VMLaunch切換到VMX non-Root Mode,因為這個過程相當(dāng)于進(jìn)入Guest[3] ,所以通常也被稱為VM entry。當(dāng)Guest內(nèi)部執(zhí)行了敏感指令,比如某些I/O操作后,將觸發(fā)CPU發(fā)生陷入的動作,從VMX non-Root Mode切換回VMX Root Mode,這個過程相當(dāng)于退出VM,所以也稱為VM exit。然后VMM將對Guest 的操作進(jìn)行模擬。相比于Ring Compression方式,即將Guest的內(nèi)核也運行在用戶模式(ring 1 ~ ring 3)的方式,支持VMX擴(kuò)展的CPU[4] :
(1)運行于Guest模式時,Guest用戶空間的系統(tǒng)調(diào)用直接陷入Guest模式的內(nèi)核空間,而不是再陷入到Host模式的內(nèi)核空間。
(2)對于外部中斷,因為需要讓VMM控制系統(tǒng)的資源,所以處于Guest模式的CPU收到外部中斷,則觸發(fā)CPU從Guest模式退出到Host模式,由Host內(nèi)核處理外部中斷。處理完中斷后,再重新切入Guest模式。為了提高I/O效率,Intel支持外設(shè)透傳模式,在這種模式下,Guest可以不必產(chǎn)生VM exit,“設(shè)備虛擬化”一章將討論這種特殊方式。
(3)不再是所有的特權(quán)指令都會導(dǎo)致處于Guest模式的CPU發(fā)生VM exit,僅當(dāng)運行敏感指令時才會導(dǎo)致CPU從Guest模式陷入Host模式,因為有的特權(quán)指令并不需要由VMM介入處理。
如同一個CPU可以分時運行多個任務(wù)一樣,每個任務(wù)有自己的上下文,由調(diào)度器在調(diào)度時切換上下文,從而實現(xiàn)同一個CPU同時運行多個任務(wù)。在VMX擴(kuò)展下,同一個物理CPU“一人分飾多角”,分時運行著Host及Guest,在不同模式間按需切換,因此,不同模式也需要保存自己的上下文。為此,VMX設(shè)計了一個保存上下文的數(shù)據(jù)結(jié)構(gòu):VMCS。每一個Guest都有一個VMCS實例,當(dāng)物理CPU加載了不同的VMCS時,將運行不同的Guest,,如圖2所示。
圖2 多個Guest切換
VMCS中主要保存著兩大類數(shù)據(jù),一類是狀態(tài),包括Host的和Guest的,另外一類是控制Guest運行時的行為。
(1)Guest-state area,保存虛擬機(jī)狀態(tài)的區(qū)域。當(dāng)發(fā)生VM exit時,Guest的態(tài)保存在這個區(qū)域;當(dāng)VM entry時,這些狀態(tài)將被裝載到CPU中。這些都是硬件層面的自動行為,VMM無須編碼干預(yù)。
(2)Host-state area,保存宿主機(jī)狀態(tài)的區(qū)域。當(dāng)發(fā)生VM exit時,CPU自動從VMCS裝載這些狀態(tài)到物理CPU;當(dāng)VM entry時,CPU自動將狀態(tài)保存到這個區(qū)域。
(3)VM-exit information fields。當(dāng)虛擬機(jī)發(fā)生VM exit時,VMM需要知道導(dǎo)致VM exit的原因,然后才能對癥下藥,進(jìn)行相應(yīng)的模擬操作。為此,CPU會自動將Guest退出的原因保存在這個區(qū)域,供VMM使用。
(4)VM-execution control fields。這個區(qū)域中的各種字段控制著虛擬機(jī)運行時的一些行為,比如設(shè)置Guest運行時訪問cr3時是否觸發(fā)VM exit;控制VM entry與exit時的行為的VM-entry control fields和VM-exitcontrol fields。我們不再一一列出細(xì)節(jié),讀者如有需要可以查閱Intel手冊。
在創(chuàng)建VCPU時,KVM模塊將為每個VCPU申請一個VMCS,每次CPU準(zhǔn)備切入Guest模式時,將設(shè)置其VMCS指針指向即將切入的Guest對應(yīng)的VMCS實例:
- commit 6aa8b732ca01c3d7a54e93f4d701b8aabbe60fb7
- [PATCH] kvm: userspace interface
- linux.git/drivers/kvm/vmx.c
- static struct kvm_vcpu *vmx_vcpu_load(structkvm_vcpu *vcpu)
- {
- u64phys_addr = __pa(vcpu->vmcs);
- int cpu;
- cpu =get_cpu();
- …
- if(per_cpu(current_vmcs, cpu) != vcpu->vmcs) {
- …
- per_cpu(current_vmcs, cpu) = vcpu->vmcs;
- asmvolatile (ASM_VMX_VMPTRLD_RAX "; setna %0"
- : "=g"(error) : "a"(&phys_addr),"m"(phys_addr)
- : "cc");
- …
- }
- …
- }
并不是所有的狀態(tài)都由CPU自動保存與恢復(fù),我們還需要考慮效率。以cr2寄存器為例,大多數(shù)時候,從Guest退出到Host再次進(jìn)入Guest期間,Host并不會改變cr2寄存器的值,而且寫cr2的開銷不小,如果每次VM entry都更新一次cr2,除了浪費CPU指令周期,毫無意義。因此,將這些狀態(tài)交給VMM由軟件自行控制更合適。
VCPU生命周期
對于每個虛擬處理器(VCPU),VMM使用一個線程代表VCPU這個實體。在Guest運轉(zhuǎn)過程中,每個VCPU基本都在如圖1-3所示的狀態(tài)中不斷地轉(zhuǎn)換。
圖3 VCPU生命周期
在用戶空間準(zhǔn)備好后,VCPU所在線程向內(nèi)核中KVM模塊向發(fā)起一個ioctl請求KVM_RUN,告知內(nèi)核中的KVM模塊,用戶空間的操作已經(jīng)完成,可以切入Guest模式運行Guest了。
在進(jìn)入內(nèi)核態(tài)后,KVM模塊將調(diào)用CPU提供的虛擬化指令切入Guest模式。如果是首次運行Guest,則使用VMLaunch指令,否則使用VMResume指令。在這個切換過程中,首先,CPU的狀態(tài),也就是Host的狀態(tài),將會被保存到VMCS中存儲Host狀態(tài)的區(qū)域,非CPU自動保存的狀態(tài)由KVM自行保存。然后,加載存儲在VMCS中的Guest的狀態(tài)到物理CPU,非CPU自動恢復(fù)的狀態(tài)則由KVM自行恢復(fù)。
物理CPU切入Guest模式,運行Guest指令。當(dāng)執(zhí)行Guest指令遇到敏感指令時,CPU將從Guest模式切回到Host模式的ring0,進(jìn)入Host內(nèi)核的KVM模塊。在這個切換過程中,首先,CPU的狀態(tài),也就是Guest的狀態(tài),將會被保存到VMCS中存儲Guest狀態(tài)的區(qū)域,然后,加載存儲在VMCS中的Host的狀態(tài)到物理CPU。同樣的,非CPU自動保存的狀態(tài),由KVM模塊自行保存。
處于內(nèi)核態(tài)的KVM模塊從VMCS中讀取虛擬機(jī)退出原因,嘗試在內(nèi)核中處理。如果內(nèi)核中可以處理,那么虛擬機(jī)就不必再切換到Host模式的用戶態(tài)了,處理完后,直接快速切回Guest。這種退出也稱為輕量級虛擬機(jī)退出。
如果內(nèi)核態(tài)的KVM模塊不能處理虛擬機(jī)退出,那么VCPU將再進(jìn)行一次上下文切換,從Host的內(nèi)核態(tài)切換到Host的用戶態(tài),由VMM的用戶空間部分進(jìn)行處理。VMM用戶空間處理完畢后,再次發(fā)起切入Guest模式的指令。在整個虛擬機(jī)運行過程中,這個過程循環(huán)往復(fù)。
下面是內(nèi)核空間切入、切出Guest的代碼:
- commit 6aa8b732ca01c3d7a54e93f4d701b8aabbe60fb7
- [PATCH] kvm: userspace interface
- linux.git/drivers/kvm/vmx.c
- static int vmx_vcpu_run(struct kvm_vcpu *vcpu, …)
- {
- u8 fail;
- u16fs_sel, gs_sel, ldt_sel;
- intfs_gs_ldt_reload_needed;
- again:
- …
- /*Enter guest mode */
- "jne launched \n\t"
- ASM_VMX_VMLAUNCH "\n\t"
- "jmp kvm_vmx_return \n\t"
- "launched: " ASM_VMX_VMRESUME "\n\t"
- ".globl kvm_vmx_return \n\t"
- "kvm_vmx_return: "
- /*Save guest registers, load host registers, keep flags */
- …
- if(kvm_handle_exit(kvm_run, vcpu)) {
- …
- goto again;
- }
- }
- return 0;
- }
在從Guest退出時,KVM模塊首先調(diào)用函數(shù)kvm_handle_exit嘗試在內(nèi)核空間處理Guest退出。函數(shù)kvm_handle_exit有個約定,如果在內(nèi)核空間可以成功處理虛擬機(jī)退出,或者是因為其他干擾比如外部中斷導(dǎo)致虛擬機(jī)退出等無須切換到Host的用戶空間,則返回1;否則返回0,表示需要求助KVM的用戶空間部分處理虛擬機(jī)退出,比如需要KVM用戶空間的模擬設(shè)備處理外設(shè)請求。
如果內(nèi)核空間成功處理了虛擬機(jī)的退出,則函數(shù)kvm_handle_exit返回1,我們看到上述代碼直接跳轉(zhuǎn)到標(biāo)簽again處,然后程序流程會再次切入Guest。這種虛擬機(jī)退出被稱為輕量級虛擬機(jī)退出。如果函數(shù)kvm_handle_exit返回0,則函數(shù)vmx_vcpu_run結(jié)束執(zhí)行,CPU從內(nèi)核空間返回到用戶空間,以kvmtool為例,其相關(guān)代碼片段如下:
- commit 8d20223edc81c6b199842b36fcd5b0aa1b8d3456
- Dump KVM_EXIT_IO details
- kvmtool.git/kvm.c
- int main(int argc, char *argv[])
- {
- …
- for (;;){
- kvm__run(kvm);
- switch (kvm->kvm_run->exit_reason) {
- caseKVM_EXIT_IO:
- …
- }
- …
- }
根據(jù)代碼可見,kvmtool發(fā)起進(jìn)入Guest的代碼處于一個無限的for循環(huán)中。當(dāng)從KVM內(nèi)核空間返回用戶空間后,kvmtool在用戶空間處理Guest的請求,比如調(diào)用模擬設(shè)備處理I/O請求。在處理完Guest的請求后,重新進(jìn)入下一輪for循環(huán),kvmtool再次請求KVM模塊切入Guest。
王柏生 資深技術(shù)專家,先后就職于中科院軟件所、紅旗Linux和百度,現(xiàn)任百度主任架構(gòu)師。在操作系統(tǒng)、虛擬化技術(shù)、分布式系統(tǒng)、云計算、自動駕駛等相關(guān)領(lǐng)域耕耘多年,有著豐富的實踐經(jīng)驗。著有暢銷書《深度探索Linux操作系統(tǒng)》(2013年出版)。
謝廣軍 計算機(jī)專業(yè)博士,畢業(yè)于南開大學(xué)計算機(jī)系。資深技術(shù)專家,多年的IT行業(yè)工作經(jīng)驗。現(xiàn)擔(dān)任百度智能云副總經(jīng)理,負(fù)責(zé)云計算相關(guān)產(chǎn)品的研發(fā)。多年來一直從事操作系統(tǒng)、虛擬化技術(shù)、分布式系統(tǒng)、大數(shù)據(jù)、云計算等相關(guān)領(lǐng)域的研發(fā)工作,實踐經(jīng)驗豐富。
本文內(nèi)容節(jié)選自《深度探索Linux虛擬化技術(shù)》,已獲得機(jī)械工業(yè)出版社華章公司授權(quán)。
本文轉(zhuǎn)載自微信公眾號「Linux閱碼場」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系Linux閱碼場公眾號。