KVM最初的2小時——KVM從入門到放棄

  • 2019 年 10 月 11 日
  • 筆記

虛擬化的訴求和歷史的痛苦

那些不能銘記過去的人註定要重蹈覆轍。你還記得當年用Windows隱藏文件夾藏片嗎? 作為一個屌絲,虛擬化技術確實意義非常重大。這個最顯著的作用顯然就是藏片,作為一個程式設計師,如果還用Windows文件隱藏功能來藏片,這實在是污辱自己和女朋友的智商,讓廣大碼農抬不起頭來做人。最早可以幫你實質藏片的手段來自VMware。 VMware這個名字就是一種牛逼,VM就是virtual machine,ware是取自Software中的ware,1999年VMware發布了它的第一款產品VMware Workstation,在那個賽揚333和白衣飄飄的時代。

上面一幅圖看起來比較嗨皮,但是技術含量確實不低。你想在一個電腦上面,虛擬出來一個「假」電腦,但是一定要「假」到什麼程式呢?就是蒼老師在跑的時候,她意識不到這是個「假」電腦。造假從來都不是那麼容易的事情。會面臨來自CPU、記憶體、I/O的全方位障礙。

先說CPU方面,為了避免應用弄死整個系統,除了一些裸奔的RTOS(實時作業系統)以外,現代作業系統一般藉助CPU的不同模式來將作業系統內運行的軟體切割為用戶態和內核態。用戶態只能執行常規的CPU指令如運算,但凡涉及到訪問特定的硬體,如MMU、I/O等,用戶態的應用就需要陷入內核態,調用內核的系統服務來完成。

比如下面最簡單一段程式:

int main(int argc, char **argv) { int i; for(i=0;i<10000;i++); write(1, 「hellon」, 6); for(i=0;i<10000;i++); return 0; }

其中for(i=0;i<10000;i++);這樣的語句是在用戶態執行的,而write(1, 「hellon」, 6);則通過系統調用陷入到內核態。 這個陷入,不僅是軟體的一種變化,也是硬體模式的一種跨越。X86的處理器模式也從ring3非特權模式切換到了ring0特權模式了。非特權這樣的模式,可以保證用戶空間想幹壞事也幹不了,幹了壞事就現場被抓。

那麼問題就來了,沒有虛擬機guest的情況下,ring0隻有主機作業系統一個人玩,這個是絲毫沒有什麼問題的。有多個guest OS的情況下,guest OS的內核也想在ring0玩(至少它要覺得自己在ring0玩),但是事實上是它不能佔據ring0,否則就變成了宋哲,控制了不該控制的資源。這個時候,我們必須給guest OS做「特權解除(De-privileging)」,比如把guest OS的kernel放入許可權更低的ring1。但是,我們必須給它模擬出還是在ring3和ring0跑的樣子,因為OS原本是這樣理解的,全虛擬化的本質讓它感知不到被虛擬化了,就是追求這個等價性。既然蒼老師喜歡看到guest OS的內核在ring0建設社會主義的樣子,我們就要把蒼老師給矇騙過去。

模擬出還是ring0和3的樣子,這個事情還真的是不簡單。現在guest OS用戶態和內核態分別運行在CPU的ring3和ring1,然後蒼老師在的Windows的內核想讀CPU的一個暫存器知道CPU現在在什麼狀態,假設這個指令叫做ABC,由於現在雖然蒼老師Windows在內核態,但是CPU實際處於ring1,所以她讀到的是ring1,這顯然不符合應有的期待,虛擬化後蒼老師應該讀到ring0才對!

ABC這樣的指令關乎到系統全局資源的狀態讀取或者設置,我們一般稱呼這樣的指令為敏感指令(Sensitive instruction)。假設ABC這條敏感指令同時也是一條特權指令(Privilege instruction,在非特權模式執行的時候會引發硬體陷入特權模式的ring0),那麼蒼老師讀CPU的狀態的時候,陷入ring0,我們在ring0的VMM(virtual machine monitor)程式碼裡面偽造一個ring0值給蒼老師就萬事大吉了,這就是典型的「陷入-模擬」。只要能陷入,咱們就能模擬,就能製造幻覺。

如果所有的敏感指令都是特權指令,我們顯然是可以完美通過這種「陷入-模擬」的方法來實現虛擬化的。實際上,大部分敏感指令確實是特權指令。但是,無論是早期的X86,還是ARM,都有些敏感指令不是可以陷入的特權指令,我們稱呼它們為臨界指令(critical instruction)。不陷入就無法模擬,又關乎系統資源的讀取和設置,系統資源就像全局變數,主機看虛擬機也看,這個虛擬機看,那個虛擬機也看,你看我也看,那麼顯然是無法實現邏輯上的隔離了。我們顯然需要把跨機器的全局變數變成虛擬機內的模組級變數才靠譜。

早期為了解決上述問題,人們一般採用2種辦法:

  • 半虛擬化(para-virtualization)

直接在guest OS裡面把無法虛擬化的部分程式碼改掉,把ABC指令替換成一個陷入ring0的系統調用,既然你不陷入,哥就強行拉你下水。這顯然就不是全部的虛擬化了,這種叫做半虛擬化(para-virtualization)。

  • 二進位翻譯( binary translation )

不改程式碼,比如看到ABC這樣的指令,提前插入斷點來截獲之,交由 VMM 解釋執行,我們就把它強行翻譯為別的東西。其實這個也有那麼一點類似半虛擬化,你可以認為半虛擬化的改程式碼在編譯前,而二進位翻譯的改程式碼在運行時。

通常我們認為運行時候改,會比編譯前改,逼格要高那麼一點點。

由於半虛擬化需要系統內核的深度修改,在生產環境中,半虛擬化在技術支援和維護上會有很大的問題,早期的Xen就是用的這種方法。而早期的VMware用的手段則是進行二進位翻譯( binary translation ),把這些指令翻譯執行,不讓它的實際指令執行。翻譯的意思,就是類似明明我乾的就是ABC,它替換為xxxx,yyyy,zzzz,然後欺騙蒼老師現在是ring0:

這是虛擬化的訴求,也是歷史的痛苦。當然現在已經不是蒼老師的時代了,遙想公謹當年,蒼天有井獨自空,星落天川遙映瞳。小溪流泉映花彩,松江孤島一葉楓。哎,時代的車輪滾滾向前,碾壓著每一個屌絲。房子永遠越來越貴,家庭成本越來越高,碼農越來越老,外企個個在跑,每每念及此處,心裡孤單又寂寞。

現在無論是X86還是ARM,都通過修改CPU架構,解決了上述問題。Intel Virtualization Technology (VT-x) 和AMD』s AMD-V這樣的硬體虛擬化技術,在CPU引入一個新的模式VMX Root Mode。

Guest OS運行於non-root下的ring0,Guest OS上面的應用運行於non-root模式下的ring3,而host OS的內核和VMM則運行於root模式下。這樣做的一個好處是,對guest OS和app而言,它的軟體是透明的,內核感覺自己是ring0, APP感覺自己是ring3,看起來沒有執行「特權解除」一樣,也不用再去執行前面的實際在ring1,而要假裝在ring0的樣子。

root模式之下,也分為ring0-ring3。VMM和HOST OS運行在RING0, HOST APPs運行在ring3。當guest OS退出(VMExit)的時候,會進入root;VMM調度guest OS運行的時候,會進入guest OS(VMEntry)。

顯然,non-root模式之下的ring 0,不具備root模式下ring0同樣的特權。所以root模式下的ring0是fully privileged ring 0,而non-root模式下的ring0是less privileged ring 0。一些non-root下的異常、I/O訪問、指令和特定暫存器的訪問,將引發從non-root到root的切換事件。比如在non-root下執行INVD — Invalidate Internal Caches指令,就會引發VMExit事件。

而ARM處理器也引入了類似的技術,在ARM的非安全模式之下,分為EL0這個level跑guest APP,EL1這個level跑GUEST OS,而EL2這個level跑VMM,完成各個guest OS的切換。

除了CPU以外,記憶體也是一個大問題,主機OS在跑的時候,它通過CPU的MMU完成虛擬地址到物理地址的轉化。對於主機而言,它看到的物理記憶體是整個記憶體條。但是對於主機上面運行的虛擬機裡面的guest OS而言,它顯然不能直接看到物理的記憶體條。因為虛擬化的核心是把物理的東西邏輯化。所以蒼老師看到的物理地址,在她的眼裡依然是連續的,但是它顯然不能是記憶體條最終真正的物理地址。現代CPU一般通過提供第2級轉換來完成,一級是guest OS裡面虛擬地址(VA)到guest OS的物理地址(PA),另外一級是guest OS裡面的物理地址到真實記憶體條的地址(MA)。第2級的PA->MA的轉化由VMM來維護。對guest OS裡面運行的app而言,VA是連續的,實際上PA是非連續;對於guest OS裡面運行的kernel而言,PA是連續的,實際上MA是非連續的。總之,不在乎是否真的連續,只在乎你覺得是連續的就行!前面我已經反覆強調,虛擬化本質上是一種幻覺。在沒有記憶體虛擬化支援的時代,VMM一般是通過給guest OS的進程再維護一個guest OS虛擬地址到最終機器物理地址的影子頁表來完成地址轉換的。

CPU、記憶體以外,接下來的大問題就是I/O外設的一系列模擬。大家玩過VMware、Virtualbox的話,都知道我們可以在guest OS裡面加假硬碟、假光碟機、假網卡。

這些假的東西怎麼造呢?需要進行硬體的行為模擬。比如虛擬機guest OS裡面有一個網卡X,它有如下暫存器序列來發包:

write(reg1, 0xFF) write(reg2, 0xF0) write(reg3, 0x1)

為了模擬這個網卡,我們也需要捕獲上述的IO操作並進行模擬,由於所有的IO操作都會引發異常,最終陷入VMM,而VMM可以藉由host OS之上運行的一個應用進行行為級模擬並最終調用Host OS的系統調用來完成最後的操作。在VMware workstation中,這一步驟就由VMdriver、VMM和VMApp來協同完成。

一個典型的guest裡面的網路發包流程如下,顯然VMM上下文給了VMDriver,之後VMApp獲得I/O請求,VMApp弄清楚情況後,最終通過syscall調主機的服務把包通過主機的網卡發出去:

相似的,KVM 在 IO 虛擬化方面,就是使用 QEMU 這個應用軟體的方式來模擬 IO 設備。

KVM是個什麼鬼?

KVM(Kernel-based Virtual Machine)最初是由一個以色列的創業公司Qumranet開發的,KVM的開發人員並沒有選擇從底層開始新寫一個Hypervisor,而是選擇了基於Linux kernel,通過載入新的模組從而使linux Kernel本身變成一個Hypervisor。從Linux kernel 2.6.20開始就包含在Linux內核程式碼之中,可以重用Linux kernel的生態鏈和現有基礎設施。 KVM運行於帶硬體虛擬化支援的處理器,所以我們假定硬體裡面的CPU虛擬化擴展、記憶體虛擬化擴展等都是存在的。 KVM架構中涉及到3個重要組件:

  • Guest:客戶機系統,運行在虛擬的CPU(vCPU)、記憶體、虛擬的IO設備(Console、網卡、I/O 設備驅動等)。
  • KVM:運行在Linux內核空間,成為內核模組,提供CPU 和記憶體的虛級化,以及客戶機的 I/O 攔截。Guest 的 I/O 被KVM 攔截後,交給 QEMU 處理。KVM驅動給用戶空間的QEMU提供了/dev/kvm字元設備。
  • QEMU:修改過的為 KVM虛擬機使用的 QEMU 程式碼(稱為qemu-kvm),運行在用戶空間,除了提供硬體 I/O 設備的模擬,還通過對/dev/kvm設備執行IOCTL來和 KVM 交互執行創建虛擬機、vCPU等對guest的控制操作。

如果要把VMware Workstation與KVM進行一個對比的話,VMdriver、VMM有點類似KVM內核模組,而VMApp有點類似QEMU。

QEMU提供了guest管理的功能、I/O轉換的功能。

在ARM處理器上,KVM內核組件的運行情況如下圖:

而ARM V8.1 VHE支援後,則可以變成:

QEMU通過ioctl發出KVM_CREATE_VM、KVM_CREATE_VCPU等這種虛擬機、vCPU的創建指令,讓guest投入運行。之後QEMU執行KVM_RUN這樣的IOCTL,如果這個IOCTL返回,意味著VMexit。qemu根據返回的情況,確定是否是guest OS發生IO的動作,如果是,則進行IO模擬以及執行主機的系統調用來完成IO動作,偽程式碼類似:

open(「/dev/kvm」) ioctl(KVM_CREATE_VM) ioctl(KVM_CREATE_VCPU) … for (;;) { exit _reason = ioctl(KVM_RUN) switch (exit_reason) { case KVM_EXIT_IO: /* … */ case KVM_EXIT_HLT: /* … */ } }

Guest OS對設備暫存器的讀寫的地址、size和數據都可以傳遞給QEMU裡面的設備驅動,QEMU再進行行為級模擬即可,比如Guest OS想讀addr位置的一個暫存器:

static int xche_ioport_read(struct kvm_io_device *this, gpa_t addr, int len, void *data) { /根據不同的地址執行不同的操作/ switch (addr) { case: break; … } /*將數據拷貝到讀取的數據地址/ memcpy(data, (char *)&ret, len); return 0; }

第一次的運行

我們把KVM以及一些相關的管理工具都安裝了: sudo apt-get install qemu-kvm qemu-system libvirt-bin bridge-utils virt-manager 然後跑一個最小的Linux- Tinycore。從http://tinycorelinux.net/downloads.html下載這個最小Linux,或者直接在Ubuntu中 wget http://tinycorelinux.net/7.x/x86/release/TinyCore-current.iso 運行之: qemu-system-x86_64 -smp 2 –enable-kvm –cdrom /mnt/hgfs/Downloads/TinyCore-current.iso 這樣我們會發現Tinycore Linux跑起來了:

在模擬的qemu窗口中按下ctrl+alt 2,運行info kvm,發現KVM已經啟動:

這個時候我們會在host OS裡面看到一個qemu的進程:

baohua@ubuntu:~$ ps -ef | grep qemu baohua 101655 42512 14 10:34 pts/3 00:00:05 qemu-system-x86_64 -smp 2 –enable-kvm –cdrom /mnt/hgfs/Downloads/TinyCore-current.iso

由此可見,每個虛擬機在主機裡面就是一個普通的Linux進程。

工具和易用性的解決

用qemu-system-x86_64 -smp 2 –enable-kvm –cdrom /mnt/hgfs/Downloads/TinyCore-current.iso這樣的原始命令來運行KVM畢竟有些naive,我們需要一套強大的工具來方便KVM的部署。

這個時候,我們就不得不提libvirt,libvirt是一套免費、開源的支援Linux下主流虛擬化工具的C函數庫。其旨在為包括Xen、KVM、Virtualbox、VMware等在內的各種虛擬化工具提供一套方便、可靠的編程介面。所以libvirt可以認為是管理工具和具體虛擬機之間的一個紐帶。

Libvirtd是一個daemon進程,virsh、virt-manager等工具呼叫libvirtd,而Libvirtd則調用qemu-kvm操作虛擬機。

前面的Tinycore Linux,我們同樣可以在virt-manager裡面進行創建、啟動和停止。一路如下:

創建好後,啟動Tinycore:

我們用virsh工具來觀察一下這個虛擬機:

baohua@ubuntu:~$ virsh list –all Id Name State 3 linux running

我們現在強行用」virsh destroy linux」這個命令銷毀這個虛擬機:

用virsh start再次啟動它: