虛擬機淺析


轉載自:http://hi.baidu.com/_kouu/blog/item/b3f7bbca39030413be09e6f5.html
最近抽空翻看了《虛擬機-系統與進程的通用平臺》一書,又在網上翻了一些關於虛擬機的文章,受益非淺,略記一些自己的理解。

計算機系統由上自下主要可以分爲三個層次:應用程序-操作系統-硬件平臺。由此,就出現了兩種主要的虛擬機:進程虛擬機系統虛擬機。進程虛擬機爲一個應用程序提供虛擬的運行環境,它需要對操作系統和硬件平臺(或二者之一)作虛擬;而系統虛擬機則爲操作系統提供虛擬環境,主要是對硬件平臺的虛擬。

進程虛擬機
進程虛擬機對操作系統和硬件平臺進行虛擬,以滿足應用程序的運行要求。進程虛擬機一般作爲主機上的一個進程來運行,這個進程裝載客戶軟件,然後仿真運行之。舉例來說,進程虛擬機可以讓windows下的軟件在linux上運行(wine就是這樣一個虛擬機,不過它只是對操作系統的虛擬,並不涉及指令集的仿真)。

進程虛擬機大體上基於如下功能模塊來實現:(摘自《虛擬機》)


進程虛擬機啓動以後,加載器將客戶軟件加載到內存,並完成相關的初始化工作,包括對各種信號處理函數的設置等。
初始化完成後,客戶軟件的代碼被當作數據,由仿真引擎讀取,並進行解釋或翻譯。

仿真執行
仿真引擎讀取到的客戶軟件是一系列的指令集合,這些指令可能需要兩個層面的仿真:
一、指令集的仿真。如果客戶軟件的指令集與主機不相同,則需要將其翻譯成主機的指令集。
比如X86下的指令add %eax,4可能需要替換成PPC下的addi r4,r4,4。除了指令的變換,寄存器、內存地址都需要做相應的變換。比如X86下的eax替換成PPC下的r4。因爲PPC的寄存器比X86要多很多,這種寄存器的直接替換是可行的。但是如果反過來呢?用X86來仿真PPC的時候就不能這樣做了,實在不行只能將寄存器映射到內存上面(可憐的是內存比寄存器要慢很多)。內存地址也是需要變換的,客戶軟件在運行的時候總是認爲整個內存地址空間都是屬於自己的(儘管並不是所有地址空間都會使用),但是當它在進程虛擬機上面運行時,有一部分內存空間卻是屬於虛擬機本身的。進程虛擬機在仿真時需要對客戶軟件所使用的內存地址進行映射,使其不與虛擬機自己使用的內存相沖突。簡單的做法是給內存地址加一個基值,而虛擬機使用的內存都小於這個基值。
另外,像add這樣的指令還會改變條件碼EFLAGS寄存器。於是,在仿真完“增加”的功能之後,還需要進行一系列的判斷來決定每一個條件碼是否應該被置位(比如溢出標記、零結果標記、等)。這樣做會非常的累,所以虛擬機往往會採用懶惰的做法:對於每一個條件碼,記錄下最後一條會影響到它的指令。而當有後續指令需要關心某個條件碼時,再找到這條影響條件碼的指令,判斷這個條件碼是否應該被置位。
二、系統調用的模擬。如果客戶軟件與主機使用的不是同一操作系統,則需要對系統調用進行模擬。
客戶軟件中的系統調用代碼將被替換成主機上的相關調用(也可能不是系統調用)。但是有些時候,系統調用卻是無法模擬的。比如linux下的fork,在 windows下就難以模擬。
異常也是操作系統模擬的一個部分,比如進程虛擬機收到一個信號,可能需要報告給客戶軟件。要實現這一點,進程虛擬機需要爲所有(至少是客戶軟件需要關心的)信號註冊handler。然後截獲客戶軟件註冊信號handler的系統調用,爲其設置相應的異常索引表項(信號=>函數地址)。那麼當進程虛擬機收到一個信號時,虛擬機設置的相應handler被調用。這個handler可以通過異常索引表發現這個信號是被客戶軟件所關心的,於是跳轉到客戶軟件上對應的的函數地址去。

翻譯和優化
以上就是進程虛擬機的一個基本方法。然而,要想讓客戶軟件儘量快地運行,僅對客戶軟件進行解釋執行肯定是不夠的。客戶軟件中的每一條指令都需要被虛擬機一次一次地從內存讀出來,然後運行解釋程序,將其解釋成一系列主機上的指令,然後再去執行它們。如果對客戶軟件進行翻譯,將客戶軟件指令的解釋結果生成代碼塊,然後緩存起來,就可以免去每一次都進行解釋的過程。
以怎樣的粒度來翻譯代碼塊呢?是基本塊。一個基本塊開始於分支或跳轉後立即執行的指令、結束於下一條分支或跳轉指令。也就是說,一個基本塊會被順序執行,中間不存在分支或跳轉指令。而所有的分支或跳轉指令只會跳轉到一個基本塊的第一條指令。
翻譯好以後的基本塊被放入代碼cache中,當執行到一條跳轉指令時,通過跳轉的目標地址索引到代碼cache中對應的基本塊,則可以直接跳轉到該基本塊去執行。當然代碼cache中也可能找不到相應的基本塊(它可能還未被翻譯,或已從cache中清除),這就需要“跳轉”到客戶軟件的相應代碼,繼續解釋執行或進行一次新的翻譯。
而執行完一個被翻譯的基本塊後,程序流程該何去何從呢?按上面的定義,基本塊的最後一條指令是跳轉指令。但是它們是被翻譯以後的,是直接執行的,不由虛擬機干預的。所以最後一條跳轉指令應該跳轉到虛擬機的處理代碼中,由虛擬機程序繼續選擇下一個已翻譯的基本塊、或是解釋新的基本塊。(這種方式是可以進一步優化的,比如通過分支預測技術預測一組順序運行的基本塊,使它們儘量能夠順序運行。而只在預測失敗的情況下才跳轉回虛擬機的代碼中。)
進程虛擬機對於基本塊所能做的,除了翻譯之外,還可以優化,就像編譯器優化目標代碼那樣。但是編譯器只能做靜態的優化,而虛擬機卻是動態的,可以在實際執行過程中收集到更多有利於優化的信息。(比如像通過函數指針調用函數這樣的間接跳轉,很難通過靜態優化去預測跳轉目標,而動態優化則是有可能的。)

剖析
雖然執行翻譯後的基本塊比重新解釋這個基本塊要更快一些(省略了重新解釋的過程),但是如果這個基本塊總共只會執行一次呢?顯然翻譯基本塊的開銷更大一些(除了需要解釋之外,還需要分配和管理代碼cache等)。所以一味進行代碼翻譯,並不是上策。況且系統的內存有限,也未必能容得下客戶軟件的所有代碼翻譯。對翻譯後的基本塊進行優化也是這樣,優化本身也是有開銷的,一味進行優化也不是上策。
爲了解決解釋與翻譯(和優化)的矛盾,需要靠剖析客戶軟件來提供指導數據。簡單的說,剖析統計了各個基本塊在最近一段時間內的執行次數,如果大於某個值,則應該翻譯,再大於某個值,則應該優化,並且隨着執行次數的增加可能需要更高級別的優化。
爲了對客戶軟件進行剖析,在解釋執行基本塊的時候,可以很自然地增加統計剖析數據的代碼;而在翻譯後的代碼中也可以插入這樣的統計代碼(翻譯並不一定嚴格按照客戶軟件,這些統計代碼就是客戶軟件裏面沒有的)。

高級語言虛擬機
進程虛擬機裏面有一類特殊的虛擬機,叫做高級語言虛擬機,最出名的莫過於java。高級語言虛擬機和上面描述的進程虛擬機擁有幾乎相同的功能模塊,但是其客戶軟件並不是基於某種實際的操作系統和硬件平臺的,其操作系統和硬件平臺本身就是虛擬的,這種虛擬可以解決前面提到的解釋過程中遇到的很多問題,使解釋更容易,解釋過程也就更快。比如主機和客戶機寄存器不匹配的問題,虛擬指令集可以定義儘可能少的寄存器,以便實際的體系結構都能滿足它。再比如虛擬機可以定義一套系統API,通過API可以對具體的操作系統接口進行抽象,避免出現系統調用難以仿真的情況。

進程虛擬機一般用於將其他平臺上的應用程序快速遷移到主機上來運行。相比之下,程序移植需要付出更大的代價,並且很多程序並不開放源代碼,不太可能隨意移植。在快速遷移的另一面,根據上面的描述可以看出,應用程序在進程虛擬機下執行的性能比起在實際機器上執行應該是會打很大折扣的。
而像java這樣的高級語言虛擬機,定義了更利於仿真的指令集和API,可以把更多的時間用於動態優化,更利於性能的提升。極端情況下,可能比用C寫的運行在實際機器上的程序更高效。比如這個程序反覆執行的代碼非常集中(這些代碼顯然會被翻譯並優化,它們幾乎都是直接執行的了)、並且這些代碼在執行過程中可能得到更有突破性的動態優化。

系統虛擬機
系統虛擬機對硬件平臺進行虛擬,以滿足客戶操作系統的運行需要。操作系統是對性能要求很高的軟件,如果虛擬化使其動輒損失70%~80%的性能,這是讓人很難接受的。所以系統虛擬機多用於同一硬件平臺下的虛擬,避免指令集的仿真。
那麼既然是同一硬件平臺,爲什麼還要虛擬呢?其目的是在同一臺物理機器上虛擬出多個機器來。這樣做的好處,比如方便服務器部署、安全性考慮、等等。 windows下的vmware、linux下的xen、kvm+qemu都是這樣的虛擬機。

系統虛擬機的架構:(摘自《虛擬機》)


VMM管理硬件資源,提供客戶操作系統的運行環境,讓它們都以爲自己是在獨佔整個硬件資源的。VMM一般就是一個運行在實際機器上的操作系統,然後加入一些VMM的功能。比如kvm作爲linux內核的一個模塊,加載之後,linux內核就變成了一個VMM。而客戶操作系統一般是作爲VMM上的一個進程來實現的。
現在的操作系統一般都支持多進程,操作系統本身就給進程提供了一個虛擬機環境:通過分時複用,讓進程以爲自己獨佔了一個CPU;通過虛擬內存技術,讓進程以爲自己獨佔了整個內存空間;通過系統調用,讓進程能夠操控硬件設備。可見,將客戶操作系統作爲VMM上的一個進程來實現,本身就具備了基本的虛擬環境。
但是客戶操作系統畢竟是一個操作系統,它是需要跟硬件親密接觸的,進程的虛擬環境並不能滿足操作系統運行的需求:進程,一般就是指用戶進程,是不能執行CPU特權指令的。而操作系統有時候則必須要能夠執行。所以VMM需要給客戶操作系統提供執行特權指令的途徑;操作系統不僅要關心實際的內存地址空間,還需要爲運行於其上的進程提供虛擬地址空間,這一般是通過跟硬件協作來完成的(管理頁表,然後由mmu來執行內存地址映射)。所以VMM需要給客戶操作系統提供頁表和mmu的虛擬;操作系統是通過設備驅動程序直接操作硬件設備的,而在系統虛擬機中,硬件設備主要被VMM控制了(雖然也可能被VMM直接提供給客戶操作系統使用)。所以VMM需要給客戶操作系統提供設備的虛擬。

CPU的虛擬化
前面說到,系統虛擬機是在同一硬件平臺下的虛擬,客戶操作系統的指令是可以在主機CPU上直接執行的。但是由於特權指令的存在,在客戶操作系統上執行指令時,可能因爲遇到特權指令而觸發CPU異常。這種情況倒還比較好辦,VMM捕捉到這些異常,然後判斷:如果客戶操作系統正運行在它的用戶態,這種情況屬於客戶操作系統上的進程越權訪問,則觸發客戶操作系統的異常處理過程;而如果客戶操作系統正運行在它的內核態,則VMM會替它去執行這條特權指令。
然而,可能存在一些比較討厭的指令,它們在用戶態和內核態下面執行的效果不同,它們是非特權的敏感指令。這樣一來,客戶操作系統在它的內核態下執行這樣的指令時,實際上卻是在VMM的用戶態下去執行的,達不到預期的效果,可能造成程序錯誤。然而這樣的指令又不屬於特權指令,VMM根本無法通過CPU異常來捕捉。於是VMM只好在執行客戶操作系統的代碼之前先掃描一下將要執行的指令,如果遇到這樣的非特權的敏感指令,VMM就將其替換成一條陷阱指令,並且記錄下“XX地址原本是YY指令”(顯然這個過程還是比較耗時的)。當客戶操作系統執行到這裏時,就會硬鐺鐺地觸發一次CPU異常,然後再由VMM來處理。
據說當前大部分的硬件平臺都具有這樣的非特權的敏感指令,在它們之上建立的系統虛擬機很難避免上面說到的指令掃描與替換(vmware就是這樣做的)。而一些CPU爲了更好地支持虛擬化,也可能通過一些擴展來規避非特權的敏感指令。比如intel的VT-x技術,明確地爲虛擬化提供了一種名爲VMX的操作模式。在這種模式下,原有的非特權的敏感指令都變成了特權指令(此外還支持很多虛擬化的特徵)(kvm就是利用這種模式來實現的)。

內存的虛擬化
客戶操作系統本身有自己的虛擬內存管理,它維護了一套頁表來實現地址映射。而客戶操作系統認爲的物理地址實際上是VMM提供的虛擬地址,這個地址還需要通過由VMM維護的頁表來進行二次映射,才能得到真正的物理地址。硬件mmu只支持一次映射,而另一次映射如果要通過軟件來完成的話,內存訪問的效率將會大打折扣。
客戶操作系統的頁表(記爲A=>B)和VMM的頁表(記爲B=>C)對於VMM來說都是可見的,於是VMM可以將這兩個頁表綜合起來,生成一個A=>C的頁表,謂之影子頁表。真正被mmu使用的就是這個影子頁表。這樣一來,客戶操作系統的虛擬地址就只需要一次映射便能得到物理地址了。當然,頁表A=>B和頁表B=>C也必須都保存在內存中的,它們纔是真正邏輯上的頁表。程序讀寫頁表需要關心的就是它們,而當它們被更新時,VMM必須立刻生成新的影子頁表。
但是,如果客戶操作系統修改了自己的頁表,VMM又怎麼知道呢?這還得靠CPU異常。一般來說可以利用CPU特權來保護這些頁表所在的內存,當客戶操作系統需要更新它們時將觸發CPU訪存異常。然後再由VMM捕捉異常,替它完成頁表的更新,並且順便更新影子頁表。

設備的虛擬化
客戶操作系統裏面的驅動程序是直接操作硬件設備的,對於設備的虛擬化,一般有兩種辦法,一是把真實的設備暴露給客戶操作系統,讓它獨享這個設備。而如果想讓設備在多個虛擬機之間共享,則VMM需要對設備做虛擬,以便協調多個客戶操作系統對設備的使用。VMM需要模仿硬件接口實現虛擬設備(這一點我覺得是最複雜的),然後讓客戶操作系統看到這些虛擬設備。而客戶操作系統對設備的操作都被提交到VMM上,再由VMM轉換成實際對設備的操作。比如,VMM給某個客戶操作系統提供的一個虛擬磁盤,實際上可能是真實磁盤的一個分區,或者是其中的一個文件。又比如,VMM給某個客戶操作系統提供的一個虛擬顯示器,實際上可能是VMM所擁有的桌面系統中的一個窗口。再比如,VMM給各客戶操作系統提供的網卡,可能是複用實際的網卡來實現的。或者它們就是完全虛擬的網卡,因爲在VMM所管理的各個虛擬機之間的網絡通信,實際上是不需要藉助真實網卡的,只需要VMM做一些報文轉發(進程間通信)即可。

中斷的虛擬化
除了上述三個虛擬化方面外,中斷的虛擬化也是必不可少的。這方面看到的資料比較少,按我的理解,如果設備是由VMM虛擬的,那麼VMM可能要通過信號來告知客戶操作系統中斷的到來,信號處理函數就是客戶操作系統的中斷處理函數。
而像時鐘中斷這樣的東西,一般每1ms會觸發一次。我不知道這樣的中斷是否會讓客戶操作系統來處理?如果是,假設主機上運行了100個客戶操作系統,那麼在這1ms之間,將有100個時鐘中斷被處理、要經歷100次進程切換、中斷處理函數裏面可能還有VMM需要通過CPU異常來捕捉的特權指令。並且隨着客戶操作系統的個數增加,情況將會進一步惡化。不知道主機能撐得住多少?而如果時鐘中斷不會由客戶操作系統來處理呢?那麼,客戶操作系統讀寫本地時間的操作、定時器相關的操作、等等都必須被VMM捕獲並處理。不知道這樣是否可行?

準虛擬化方案
如上面說到的,要想爲客戶操作系統提供一套完整的虛擬機環境,VMM要做的事情還是非常多的,並且有一些事情還是很礙於效率的。於是就出現了準虛擬化的方案(相對於上面說的全虛擬化),這種方案最大的特點是需要修改客戶操作系統,使其知道自己是運行在虛擬機環境中的(linux下的xen就是這樣一種方案)。於是客戶操作系統會主動調用VMM的接口來請求需要的操作(就像進程使用系統調用那樣),而不是冒失地去直接操作,再被VMM捕獲異常。這樣下來,虛擬機與客戶操作系統將達成協作,性能會非常之高,幾乎能達到實際機器的運行效果。但是方案的缺點是:修改操作系統比較麻煩,並且隨着操作系統主幹版本的升級,修改版本可能也需要隨時同步升級。有些操作系統又並不是開源的,其擁有者不一定願意爲支持你的方案而提供代碼修改的支持。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章