背景
Read the fucking source code! --By 魯迅
A picture is worth a thousand words. --By 高爾基
說明:
KVM版本:5.9.1
QEMU版本:5.0.0
工具:Source Insight 3.5, Visio
文章同步在部落格園:https://www.cnblogs.com/LoyenWang/
1. 概述
本文圍繞ARMv8 CPU的虛拟化展開;
本文會結合Qemu + KVM的代碼分析,捋清楚上層到底層的脈絡;
本文會提供一個Sample Code,用于類比Qemu和KVM的關系,總而言之,大同小異,大題小做,大道至簡,大功告成,大恩不言謝;
先來兩段前戲。
AI的世界,程式的執行不再冰冷,CPU對a.out說,hello啊,world已經ok啦,下來return吧!
既然要說CPU的虛拟化,那就先簡要介紹一下CPU的工作原理:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsISPrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdsATOfd3bkFGazxCMx8VesATMfhHLlN3XnxCMwEzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5SMiRzNyUjYzMDNjZ2N3MWY5czY2kDOhRWMmJWOyYzNz8CXzIzLcRDMxIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjL2M3Lc9CX6MHc0RHaiojIsJye.png)
CPU的根本任務是執行指令,我們常說的取指-譯碼-執行-訪存-寫回,就是典型的指令Pipeline操作;
從CPU的功能出發,可以簡要分成三個邏輯子產品:
Control Unit:CPU的指揮中心,協調資料的移動;
ALU:運算單元,執行CPU内部所有的計算;
Register:寄存器和Cache,都算是CPU内部的存儲單元,其中寄存器可用于存儲需要被譯碼和執行的指令、資料、位址等;
CPU從記憶體中讀取指令進行譯碼并執行,執行的過程中需要去通路記憶體中的資料,CPU内部的寄存器可以暫存中間的指令和資料等資訊,通常說的CPU的context指的就是CPU寄存器值;
在硬體支援虛拟化之前,Qemu純軟體虛拟化方案,是通過tcg(tiny code generator)的方式來進行指令翻譯,翻譯成Host處理器架構的指令來執行。硬體虛拟化技術,是讓虛拟機能直接執行在Host CPU上,讓Host CPU直接來執行虛拟機,結合CPU的實際工作原理,應該怎麼來了解呢?來張圖:
CPU通過pc寄存器擷取下一條執行指令,進行取指譯碼執行等操作,是以給定CPU一個Context,自然就能控制其執行某些代碼;
CPU的虛拟化,最終目标讓虛拟機執行在CPU上,無非也是要進行CPU的Context切換,控制CPU去執行對應的代碼,下文會進一步闡述;
既然都講CPU了,那就捎帶介紹下ARMv8的寄存器吧:
通用寄存器:
圖中描述的是EL3以下,AArch32與AArch64寄存器對應關系;
AArch64中,總共31個通用寄存器,64bit的為X0-X30,32bit的為W0-W30;
特殊用途寄存器:
這些特殊用途的寄存器,主要分為三種:1)存放異常傳回位址的ELR_ELx;2)各個EL的棧指針SP_ELx;3)CPU的狀态相關寄存器;
CPU的狀态PSTATE:
CPU的狀态在AArch32時是通過CPSR來擷取,在AArch64中,使用PSTATE,PSTATE不是一個寄存器,它表示的是儲存目前CPU狀态資訊的一組寄存器或一些标志資訊的統稱;
好了,ARMv8的介紹該打住了,否則要跑偏了。。。
Linux系統有兩種執行模式:kernel模式與user模式,為了支援虛拟化功能的CPU,KVM向Linux核心提供了guest模式,用于執行虛拟機系統非I/O的代碼;
user模式,對應的是使用者态執行,Qemu程式就執行在user模式下,并循環監聽是否有I/O需要模拟處理;
kernel模式,運作kvm子產品代碼,負責将CPU切換到VM的執行,其中包含了上下文的load/restore;
guest模式,本地運作VM的非I/O代碼,在某些異常情況下會退出該模式,Host OS開始接管;
好了啦,前戲結束,開始直奔主題吧。
2. 流程分析
不管你說啥,我上來就是一句中國萬歲,對不起,跑題了。我上來就是一張Qemu初始化流程圖:
看過Qemu源代碼的人可能都有種感覺,一開始看好像摸不到門框,這圖簡要畫了下關鍵子產品的流程;
Qemu的源代碼,後續的文章會詳細介紹,本文隻focus在vcpu相關部分;
除了找到了qemu_init_vcpu的入口,這張圖好像跟本文的vcpu的虛拟化關系不是很大,不管了,就算是給後續的Qemu分析打個廣告吧。
Qemu初始化流程圖中,找到了qemu_init_vcpu的入口,順着這個qemu_init_vcpu就能找到與底層KVM子產品互動的過程;
Qemu中為每個vcpu建立了一個線程,操作裝置節點來建立和初始化vcpu;
是以,接力棒甩到了KVM核心子產品。
來一張前文的圖:
前文中分析過,系統在初始化的時候會注冊字元裝置驅動,設定好了各類操作函數集,等待使用者層的ioctl來進行控制;
Qemu中設定KVM_CREATE_VCPU,将觸發kvm_vm_ioctl_create_vcpu的執行,完成vcpu的建立工作;
在底層中進行vcpu的建立工作,主要是配置設定一個kvm_vcpu結構,并且對該結構中的字段進行初始化;
其中有一個用于與應用層進行通信的資料結構struct kvm_run,配置設定一頁記憶體,應用層會調用mmap來進行映射,并且會從該結構中擷取到虛拟機的退出原因;
kvm_arch_vcpu_create主要完成體系架構相關的初始化,包括timer,pmu,vgic等;
create_hyp_mappings将kvm_vcpu結構體建立映射,以便在Hypervisor模式下能通路該結構;
create_vcpu_fd注冊了kvm_vcpu_fops操作函數集,針對vcpu進行操作,Qemu中設定KVM_ARM_VCPU_INIT,将觸發kvm_arch_vcpu_ioctl_vcpu_init的執行,完成的工作主要是vcpu的核心寄存器,系統寄存器等的reset操作,此外還包含了上層設定下來的值,放置在struct kvm_vcpu_init中;
Qemu中為每一個vcpu建立一個使用者線程,完成了vcpu的初始化後,便進入了vcpu的運作,而這是通過kvm_cpu_exec函數來完成的;
kvm_cpu_exec函數中,調用kvm_vcpu_ioctl(,KVM_RUN,)來讓底層的實體CPU進行運作,并且監測VM的退出,而這個退出原因就是存在放在kvm_run->exit_reason中,也就是上文中提到過的應用層與底層互動的機制;
使用者層通過KVM_RUN指令,将觸發KVM子產品中kvm_arch_vcpu_ioctl_run函數的執行:
vcpu最終是要放置在實體CPU上執行的,很顯然,我們需要進行context的切換:儲存好Host的Context,并切換到Guest的Context去執行,最終在退出時再恢複回Host的Context;
__guest_enter函數完成最終的context切換,進入Guest的執行,當Guest退出時,fixup_guest_exit将會處理exit_code,判斷是否繼續傳回Guest執行;
當最終Guest退出到Host時,Host調用handle_exit來處理異常退出,根據kvm_get_exit_handler去查詢異常處理函數表對應的處理函數,最終進行執行處理;
3. Sample Code
上文已經将Qemu+KVM的CPU的虛拟化大概的輪廓已經介紹了,方方面面,問題不大;
來一段Sample Code類比Qemu和KVM的關系,在Ubuntu16.04系統上進行測試;
簡要介紹一下:
tiny_kernel.S,相當于Qemu中運作的Guest OS,完成的功能很簡單,沒錯,就是Hello, world列印;
tiny_qemu.c,相當于Qemu,用于加載Guest到vCPU上運作,最終通過kvm放到實體CPU上運作;
魯迅在1921年的時候,說過這麼一句話:Talk is cheap, show me the code。
tiny_kernel.S:
tiny_qemu.c:
為了表明我沒有騙人,上一張在Ubuntu16.04的虛拟機上運作的結果圖吧:
草草收工吧。
4. 參考
ARMv8-A Architecture Overview
ARMv8 Techinology Preview
Arm Architecture Reference Manual, Armv8, for Armv8-A architecture profile
Virtual lockstep for fault tolerance and architectural vulnerability analysis
歡迎關注個人公衆号,不定期分享技術文章: