作者 | 王柏生、謝廣軍
導讀:本文摘自於王柏生、謝廣軍撰寫的《深度探索Linux系統虛擬化:原理與實現》一書,介紹了CPU虛擬化的基本概念,探討了x86架構在虛擬化時面臨的障礙,以及為支持CPU虛擬化,Intel在硬體層面實現的擴展VMX。
同時,介紹了在VMX擴展支持下,虛擬CPU從Host模式到Guest模式,再回到Host模式的完整生命周期。
Gerald J. Popek和Robert P. Goldberg在1974年發表的論文「Formal Requirements for Virtualizable Third Generation Architectures」中提出了虛擬化的3個條件:
1)等價性,即VMM需要在宿主機上為虛擬機模擬出一個本質上與物理機一致的環境。虛擬機在這個環境上運行與其在物理機上運行別無二致,除了可能因為資源競爭或者VMM的幹預導致在虛擬環境中表現略有差異,比如虛擬機的I/O、網絡等因宿主機的限速或者多個虛擬機共享資源,導致速度可能要比獨佔物理機時慢一些。
2)高效性,即虛擬機指令執行的性能與其在物理機上運行相比並無明顯損耗。該標準要求虛擬機中的絕大部分指令無須VMM幹預而直接運行在物理CPU上,比如我們在x86架構上通過Qemu運行的ARM系統並不是虛擬化,而是模擬。
3)資源控制,即VMM可以完全控制系統資源。由VMM控制協調宿主機資源給各個虛擬機,而不能由虛擬機控制了宿主機的資源。
陷入和模擬模型
為了滿足Gerald J. Popek和Robert P. Goldberg提出的虛擬化的3個條件,一個典型的解決方案是陷入和模擬(Trap and Emulate)模型。
一般來說,處理器分為兩種運行模式:系統模式和用戶模式。相應地,CPU的指令也分為特權指令和非特權指令。
特權指令只能在系統模式運行,如果在用戶模式運行就將觸發處理器異常。作業系統允許內核運行在系統模式,因為內核需要管理系統資源,需要運行特權指令,而普通的用戶程序則運行在用戶模式。
在陷入和模擬模型下,虛擬機的用戶程序仍然運行在用戶模式,但是虛擬機的內核也將運行在用戶模式,這種方式稱為特權級壓縮(Ring Compression)。在這種方式下,虛擬機中的非特權指令直接運行在處理器上,滿足了虛擬化標準中高效的要求,即大部分指令無須VMM幹預直接在處理器上運行。
但是,當虛擬機執行特權指令時,因為是在用戶模式下運行,將觸發處理器異常,從而陷入VMM中,由VMM代理虛擬機完成系統資源的訪問,即所謂的模擬(emulate)。
如此,又滿足了虛擬化標準中VMM控制系統資源的要求,虛擬機將不會因為可以直接運行特權指令而修改宿主機的資源,從而破壞宿主機的環境。
x86架構虛擬化的障礙
Gerald J. Popek和Robert P. Goldberg指出,修改系統資源的,或者在不同模式下行為有不同表現的,都屬於敏感指令。
在虛擬化場景下,VMM需要監測這些敏感指令。一個支持虛擬化的體系架構的敏感指令都屬於特權指令,即在非特權級別執行這些敏感指令時CPU會拋出異常,進入VMM的異常處理函數,從而實現了控制VM訪問敏感資源的目的。
但是,x86架構恰恰不能滿足這個準則。x86架構並不是所有的敏感指令都是特權指令,有些敏感指令在非特權模式下執行時並不會拋出異常,此時VMM就無法攔截處理VM的行為了。
我們以修改FLAGS寄存器中的IF(Interrupt Flag)為例,我們首先使用指令pushf將FLAGS寄存器的內容壓到棧中,然後將棧頂的IF清零,最後使用popf指令從棧中恢復FLAGS寄存器。如果虛擬機內核沒有運行在ring 0,x86的CPU並不會拋出異常,而只是默默地忽略指令popf,因此虛擬機關閉IF的目的並沒有生效。
有人提出半虛擬化的解決方案,即修改Guest的代碼,但是這不符合虛擬化的透明準則。後來,人們提出了二進位翻譯的方案,包括靜態翻譯和動態翻譯。靜態翻譯就是在運行前掃描整個可執行文件,對敏感指令進行翻譯,形成一個新的文件。
然而,靜態翻譯必須提前處理,而且對於有些指令只有在運行時才會產生的副作用,無法靜態處理。於是,動態翻譯應運而生,即在運行時以代碼塊為單元動態地修改二進位代碼。動態翻譯在很多VMM中得到應用,而且優化的效果非常不錯。
VMX
雖然大家從軟體層面採用了多種方案來解決x86架構在虛擬化時遇到的問題,但是這些解決方案除了引入了額外的開銷外,還給VMM的實現帶來了巨大的複雜性。於是,Intel嘗試從硬體層面解決這個問題。
Intel並沒有將那些非特權的敏感指令修改為特權指令,因為並不是所有的特權指令都需要攔截處理。舉一個典型的例子,每當作業系統內核切換進程時,都會切換cr3寄存器,使其指向當前運行進程的頁表。
但是,當使用影子頁表進行GVA到HPA的映射時,VMM模塊需要捕獲Guest每一次設置cr3寄存器的操作,使其指向影子頁表。而當啟用了硬體層面的EPT支持後,cr3寄存器不再需要指向影子頁表,其仍然指向Guest的進程的頁表。
因此,VMM無須再捕捉Guest設置cr3寄存器的操作,也就是說,雖然寫cr3寄存器是一個特權操作,但這個操作不需要陷入VMM。
Intel開發了VT技術以支持虛擬化,為CPU增加了Virtual-Machine Extensions,簡稱VMX。一旦啟動了CPU的VMX支持,CPU將提供兩種運行模式:VMX Root Mode和VMX non-Root Mode,每一種模式都支持ring 0 ~ ring 3。
VMM運行在VMX Root Mode,除了支持VMX外,VMX Root Mode和普通的模式並無本質區別。VM運行在VMX non-Root Mode,Guest無須再採用特權級壓縮方式,Guest kernel可以直接運行在VMX non-Root Mode的ring 0中,如圖1所示。
圖1 VMX運行模式
處於VMX Root Mode的VMM可以通過執行CPU提供的虛擬化指令VMLaunch切換到VMX non-Root Mode,因為這個過程相當於進入Guest,所以通常也被稱為VM entry。
當Guest內部執行了敏感指令,比如某些I/O操作後,將觸發CPU發生陷入的動作,從VMX non-Root Mode切換回VMX Root Mode,這個過程相當於退出VM,所以也稱為VM exit。
然後VMM將對Guest 的操作進行模擬。相比於將Guest的內核也運行在用戶模式(ring 1 ~ ring 3)的方式,支持VMX的CPU有以下3點不同:
1)運行於Guest模式時,Guest用戶空間的系統調用直接陷入Guest模式的內核空間,而不再是陷入Host模式的內核空間。
2)對於外部中斷,因為需要由VMM控制系統的資源,所以處於Guest模式的CPU收到外部中斷後,則觸發CPU從Guest模式退出到Host模式,由Host內核處理外部中斷。
處理完中斷後,再重新切入Guest模式。為了提高I/O效率,Intel支持外設透傳模式,在這種模式下,Guest不必產生VM exit,「設備虛擬化」一章將討論這種特殊方式。
3)不再是所有的特權指令都會導致處於Guest模式的CPU發生VM exit,僅當運行敏感指令時才會導致CPU從Guest模式陷入Host模式,因為有的特權指令並不需要由VMM介入處理。
如同一個CPU可以分時運行多個任務一樣,每個任務有自己的上下文,由調度器在調度時切換上下文,從而實現同一個CPU同時運行多個任務。在虛擬化場景下,同一個物理CPU「一人分飾多角」,分時運行著Host及Guest,在不同模式間按需切換,因此,不同模式也需要保存自己的上下文。
為此,VMX設計了一個保存上下文的數據結構:VMCS。每一個Guest都有一個VMCS實例,當物理CPU加載了不同的VMCS時,將運行不同的Guest如圖2所示。
圖2 多個Guest切換
VMCS中主要保存著兩大類數據,一類是狀態,包括Host的狀態和Guest的狀態,另外一類是控制Guest運行時的行為。其中:
1)Guest-state area,保存虛擬機狀態的區域。當發生VM exit時,Guest的狀態將保存在這個區域;當VM entry時,這些狀態將被裝載到CPU中。這些都是硬體層面的自動行為,無須VMM編碼幹預。
2)Host-state area,保存宿主機狀態的區域。當發生VM entry時,CPU自動將宿主機狀態保存到這個區域;當發生VM exit時,CPU自動從VMCS恢復宿主機狀態到物理CPU。
3)VM-exit information fields。當虛擬機發生VM exit時,VMM需要知道導致VM exit的原因,然後才能「對症下藥」,進行相應的模擬操作。為此,CPU會自動將Guest退出的原因保存在這個區域,供VMM使用。
4)VM-execution control fields。這個區域中的各種欄位控制著虛擬機運行時的一些行為,比如設置Guest運行時訪問cr3寄存器時是否觸發VM exit;控制VM entry與VM exit時行為的VM-entry control fields和VM-exit control fields。此外還有很多不同功能的區域,我們不再一一列舉,讀者如有需要可以查閱Intel手冊。
在創建VCPU時,KVM模塊將為每個VCPU申請一個VMCS,每次CPU準備切入Guest模式時,將設置其VMCS指針指向即將切入的Guest對應的VMCS實例:
commit 6aa8b732ca01c3d7a54e93f4d701b8aabbe60fb7[PATCH] kvm: userspace interfacelinux.git/drivers/kvm/vmx.cstatic struct kvm_vcpu *vmx_vcpu_load(struct kvm_vcpu *vcpu){ u64 phys_addr = __pa(vcpu->vmcs); int cpu; cpu = get_cpu(); … if (per_cpu(current_vmcs, cpu) != vcpu->vmcs) { … per_cpu(current_vmcs, cpu) = vcpu->vmcs; asm volatile (ASM_VMX_VMPTRLD_RAX "; setna %0" : "=g"(error) : "a"(&phys_addr), "m"(phys_addr) : "cc"); … } …}
並不是所有的狀態都由CPU自動保存與恢復,我們還需要考慮效率。以cr2寄存器為例,大多數時候,從Guest退出Host到再次進入Guest期間,Host並不會改變cr2寄存器的值,而且寫cr2的開銷很大,如果每次VM entry時都更新一次cr2,除了浪費CPU的算力毫無意義。因此,將這些狀態交給VMM,由軟體自行控制更為合理。
VCPU生命周期
對於每個虛擬處理器(VCPU),VMM使用一個線程來代表VCPU這個實體。在Guest運轉過程中,每個VCPU基本都在如圖3所示的狀態中不斷地轉換。
圖3 VCPU生命周期
1) 在用戶空間準備好後,VCPU所在線程向內核中KVM模塊發起一個ioctl請求KVM_RUN,告知內核中的KVM模塊,用戶空間的操作已經完成,可以切入Guest模式運行Guest了。
2) 在進入內核態後,KVM模塊將調用CPU提供的虛擬化指令切入Guest模式。如果是首次運行Guest,則使用VMLaunch指令,否則使用VMResume指令。在這個切換過程中,首先,CPU的狀態(也就是Host的狀態)將會被保存到VMCS中存儲Host狀態的區域,非CPU自動保存的狀態由KVM負責保存。然後,加載存儲在VMCS中的Guest的狀態到物理CPU,非CPU自動恢復的狀態則由KVM負責恢復。
3) 物理CPU切入Guest模式,運行Guest指令。當執行Guest指令遇到敏感指令時,CPU將從Guest模式切回到Host模式的ring 0,進入Host內核的KVM模塊。在這個切換過程中,首先,CPU的狀態(也就是Guest的狀態)將會被保存到VMCS中存儲Guest狀態的區域,然後,加載存儲在VMCS中的Host的狀態到物理CPU。同樣的,非CPU自動保存的狀態由KVM模塊負責保存。
4) 處於內核態的KVM模塊從VMCS中讀取虛擬機退出原因,嘗試在內核中處理。如果內核中可以處理,那麼虛擬機就不必再切換到Host模式的用戶態了,處理完後,直接快速切回Guest。這種退出也稱為輕量級虛擬機退出。
5) 如果內核態的KVM模塊不能處理虛擬機退出,那麼VCPU將再進行一次上下文切換,從Host的內核態切換到Host的用戶態,由VMM的用戶空間部分進行處理。VMM用戶空間處理完畢,再次發起切入Guest模式的指令。在整個虛擬機運行過程中,步驟1~5循環往復。
下面是KVM切入、切出Guest的代碼:
commit 6aa8b732ca01c3d7a54e93f4d701b8aabbe60fb7[PATCH] kvm: userspace interfacelinux.git/drivers/kvm/vmx.cstatic int vmx_vcpu_run(struct kvm_vcpu *vcpu, …){ u8 fail; u16 fs_sel, gs_sel, ldt_sel; int fs_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模塊首先調用函數kvm_handle_exit嘗試在內核空間處理Guest退出。函數kvm_handle_exit有個約定,如果在內核空間可以成功處理虛擬機退出,或者是因為其它幹擾比如外部中斷導致虛擬機退出等無須切換到Host的用戶空間,則返回1;
否則返回0,表示需要求助KVM的用戶空間處理虛擬機退出,比如需要KVM用戶空間的模擬設備處理外設請求。
如果內核空間成功處理了虛擬機的退出,則函數kvm_handle_exit返回1,在上述代碼中即直接跳轉到標籤again處,然後程序流程會再次切入Guest。
如果函數kvm_handle_exit返回0,則函數vmx_vcpu_run結束執行,CPU從內核空間返回到用戶空間,以kvmtool為例,其相關代碼片段如下:
commit 8d20223edc81c6b199842b36fcd5b0aa1b8d3456Dump KVM_EXIT_IO detailskvmtool.git/kvm.cint main(int argc, char *argv[]){ … for (;;) { kvm__run(kvm); switch (kvm->kvm_run->exit_reason) { case KVM_EXIT_IO: … } …}
根據代碼可見,kvmtool發起進入Guest的代碼處於一個for的無限循環中。當從KVM內核空間返回用戶空間後,kvmtool在用戶空間處理Guest的請求,比如調用模擬設備處理I/O請求。
在處理完Guest的請求後,重新進入下一輪for循環,kvmtool再次請求KVM模塊切入Guest。
福利
留言點讚數量最多的前三名
CSDN 攜手【機械工業出版社】送出
《深度探索Linux系統虛擬化:原理與實現》一本
截至11月25日18:00點
作者簡介:
王柏生
資深技術專家,先後就職於中科院軟體所、紅旗Linux和百度,現任百度主任架構師。在作業系統、虛擬化技術、分布式系統、雲計算、自動駕駛等相關領域耕耘多年,有著豐富的實踐經驗。
著有暢銷書《深度探索Linux作業系統》(2013年出版)。
謝廣軍
計算機專業博士,畢業於南開大學計算機系。
資深技術專家,有多年的IT行業工作經驗。現擔任百度智能雲副總經理,負責雲計算相關產品的研發。多年來一直從事作業系統、虛擬化技術、分布式系統、大數據、雲計算等相關領域的研發工作,實踐經驗豐富。