這篇文章可有點叼了。。。廢話很少說,看圖!前端
引用以下:Amit N, Wei M. The design and implementation of hyperupcalls[C]//2018 {USENIX} Annual Technical Conference ({USENIX}{ATC} 18). 2018: 97-112.程序員
而後照樣是長篇的翻譯。。固然,主力是谷歌,我負責人工審覈一波。。PPT我也有,若是有須要的能夠不去麻煩做者了,(做者若是沒人提醒估計是不會回你的。。我仍是找了他學生纔拿到的,不過貌似有個網站,他把PPT都丟了上去,不過我忘了)後端
虛擬機抽象提供了各類各樣的好處,無能否認地支持了雲計算。 然而,虛擬機是雙重的 一把雙刃劍,由於他們運行於其上的Hypervisor必須將他們視爲黑盒,這限制了他們之間可以交換的信息。 在本文中,咱們介紹了一種新機制hyperupcalls的設計和實現,它使Hypervisor可以安全地執行Guest虛擬機提供的驗證代碼,以便傳輸信息。 Hyperupcalls是用C語言編寫的,能夠徹底訪問Guest數據結構,例如頁表。 咱們提供了一個完整的框架,能夠從hyperupcall中輕鬆訪問熟悉的內核函數。 與最早進的半虛擬化技術和虛擬機內省相比,Hyperupcalls更靈活,更少侵入。 咱們證實,hyperupcalls不只能夠用於將某些操做的Guest性能提升多達2倍,並且hyperupcalls也能夠用做強大的調試和安全工具。緩存
硬件虛擬化引入了虛擬機(VM)的抽象,使得稱爲Hypervisor的主機可以同時運行稱爲Guest機的多個操做系統(OS),每一個操做系統都假設它們在本身的物理機器上運行。 這是經過暴露模擬真實物理硬件的硬件接口來實現的。 這種簡單抽象的引入致使了現代數據中心和雲的興起,正如咱們今天所知道的那樣。 不幸的是,虛擬化並不是沒有缺點。 雖然虛擬化的目標是爲VM和Hypervisor彼此分開,這種分離使得雙方沒法理解在另外一側作出的決定,被稱爲語義間隙問題 。安全
【The semantic gap characterizes the difference between two descriptions of an object by different linguistic representations, for instance languages or symbols. According to Hein, the semantic gap can be defined as "the difference in meaning between constructs formed within different representation systems". 語義間隙經過不一樣的語言表示(例如語言或符號)來表徵對象的兩個描述之間的差別。 根據Hein的說法,語義差距能夠定義爲「在不一樣表示系統中造成的構造之間的意義差別」。】服務器
解決語義差距對性能相當重要。 若是沒有關於Guest決策的信息,Hypervisor可能會次優地分配資源。 例如,Hypervisor沒法在不瞭解其內部操做系統狀態的狀況下知道guest虛擬機中的哪些內存空閒,從而破壞了VM抽象。 現在,最早進的Hypervisor一般經過半虛擬化[11,58]彌合語義鴻溝,這使得Guest瞭解Hypervisor。 半虛擬化使Guest免受物理硬件接口的限制,並容許與Hypervisor直接信息交換,經過使Hypervisor可以作出更好的資源分配決策來提升總體性能。網絡
然而,半虛擬化涉及在Hypervisor和Guest機的上下文中執行代碼。 Hypercalls要求Guest發出一個在Hypervisor中執行的請求,就像系統調用同樣,而且upcalls要求Hypervisor發出請求在Guest中執行。 這種設計帶來了許多缺點。 首先,半虛擬機制在Hypervisor和Guest機之間引入了上下文切換,若是須要Guest機和Hypervisor之間的頻繁交互,這多是實質性的[7]。 其次,半虛擬機制的請求者必須等待它在另外一個可能正忙的上下文中服務,或者若是它是空閒的則喚醒該Guest。 最後,半虛擬機制將Hypervisor和Guest機的設計結合起來:須要爲每一個Guest機和Hypervisor實現半虛擬機制,從而增長複雜性[46]並妨礙可維護性[77]。 添加準虛擬功能須要使用新接口更新guest虛擬機和Hypervisor[69],而且有可能引入錯誤和攻擊面[47,75]。數據結構
【每一段程序都有不少外部變量。只有像Add這種簡單的函數纔是沒有外部變量的。一旦你的一段程序有了外部變量,這段程序就不完整,不能獨立運行。你爲了使他們運行,就要給全部的外部變量一個一個寫一些值進去。這些值的集合就叫上下文。譬如說在C++的lambda表達是裏面,[寫在這裏的就是上下文](int a, int b){ ... }。】多線程
一種不一樣的技術,VM內省(VMI)[25]和反向,Hypervisor內省(HVI)[72]旨在經過內省其餘上下文來解決半虛擬化的一些缺點,實現無需上下文切換或先前協調的通訊傳輸。 然而,這些技術是脆弱的:數據結構,行爲甚至安全增強的微小變化[31]可能會打破內省機制,或者更糟糕的是,引入安全漏洞。 所以,內省一般被納入入侵檢測系統(IDS)領域,該系統可檢測惡意軟件或行爲不當的應用程序。架構
【VMI tools may be located inside or outside the virtual machine and act by tracking the events (interrupts, memory writes, and so on) or sending the requests to the virtual machine. Virtual machine monitor usually provides low-level information like raw bytes of the memory. Converting this low-level view into something meaningful for the user is known as the semantic gap problem. Solving this problem requires analysis and understanding of the systems being monitored. VMI工具能夠位於虛擬機內部或外部,並經過跟蹤事件(中斷,內存寫入等)或將請求發送到虛擬機來執行操做。虛擬機監視器一般提供低級信息,如內存的原始字節。將此低級視圖轉換爲對用戶有意義的內容稱爲語義缺口問題。解決這個問題須要分析和理解被監控的系統。 VMI:即Virtual Machine Introspection,VMI是一種用於在外部監測系統級虛擬機運行狀態的技術。監測器可置於另外一個虛擬機,在VMM內部或在虛擬化架構的任何其餘部分。在VMI過程當中,VM的運行狀態可被廣義地定義爲包括處理器寄存器、內存、磁盤、網絡及任何硬件級事件。】
在本文中,咱們描述了hyperupcalls 1的設計和實現,這種技術使Hypervisor可以與Guest進行通訊,如upcalls,但沒有這樣的上下文切換,像VMI。 這是經過使用通過驗證的代碼實現的,該代碼使Guest可以以靈活的方式與Hypervisor進行通訊,同時確保Guest不能提供行爲不當或惡意代碼。 一旦Guest註冊了一個hyperupcall,Hypervisor就能夠執行它來執行諸如定位空閒Guest頁面或運行Guest中斷處理程序而不切換到Guest的操做。
Hyperupcalls易於構建:它們是用C語言等高級語言編寫的,咱們提供了一個框架,容許hyperupcalls共享相同的代碼庫並構建系統,由於Linux內核能夠推廣到其餘操做系統。 編譯內核時,工具鏈會將hyperupcall轉換爲可驗證的字節碼。 這樣能夠輕鬆維護hyperupcalls。 在引導時,guest虛擬機向hypervisor註冊hyperupcalls,Hypervisor驗證字節碼並將其編譯回本機代碼以得到性能。 一旦從新編譯,Hypervisor能夠隨時調用hyperupcall。
咱們代表,使用hyperupcalls能夠經過容許Hypervisor主動分配資源來顯着提升性能,而不是等待guest虛擬機經過現有機制作出反應。 咱們構建用於內存回收和處理內部處理器中斷(IPI)的hyperupcalls,並顯示高達2 x的性能提高。除了提升性能以外,hyperupcall還能夠加強虛擬環境中系統的安全性和可調試性。 咱們開發了一個hyperupcall,使Guest可以在不使用專用硬件的狀況下對內存頁面進行寫保護,另外一個使ftrace [57]可以在統一的跟蹤中捕獲Guest和Hypervisor事件,從而使咱們可以在虛擬化環境中得到新的性能視野。
【write-protect寫保護是硬件設備或軟件程序阻止寫入新信息或更改舊信息的能力。 一般,這意味着您能夠讀取數據,但不能寫入。 圖中是SD卡上的寫保護開關示例,用於打開和關閉該卡上的寫保護。 Ftrace是一個直接內置於Linux內核的跟蹤實用程序。 許多發行版在其最新版本中已經啓用了各類Ftrace配置。 Ftrace爲Linux帶來的好處之一是可以查看內核中發生的事情。】
本文作出如下貢獻:
咱們創建了一種機制分類,用於彌合Hypervisor和Guest之間的語義鴻溝,並在該分類中放置Hyperupcalls
咱們用如下內容描述和實現hyperupcalls(§3):
咱們對hyperupcalls進行原型設計和評估,並代表hyperupcalls能夠提升性能(§4.3,§4.2),安全性(§4.5)和可調試性(§4.4)。
如今人們廣泛認爲,爲了從虛擬化中提取最大的性能和實用性,Hypervisor及其Guest須要彼此瞭解。 爲此,存在許多促進Hypervisor和Guest之間通訊的機制。 表1總結了這些機制,這些機制能夠由請求者,執行者以及機制是否要求Hypervisor和Guest提早協調來普遍地表徵。
在下一節中,咱們將討論這些機制並描述hyperupcall如何知足通訊機制的需求,其中Hypervisor在沒有上下文切換的狀況下製做並執行其本身的請求。 咱們首先介紹當今使用的最早進的半虛擬機制。
超級呼叫和上行呼叫。 現在,大多數Hypervisor都利用半虛擬化來跨語義鴻溝進行通訊。 目前普遍使用的兩種機制是超級調用,它容許Guest調用Hypervisor提供的服務和upcalls,這使得Hypervisor能夠向Guest發出請求。 半虛擬化意味着這些機制的接口在Hypervisor和Guest之間提早協調[11]。
上行呼叫和超級呼叫的主要缺點之一是它們須要上下文切換,由於兩種機制都在請求的相反側執行。 所以,必須當心調用這些機制。 過於頻繁地調用hypercall或upcalls會致使高延遲和計算資源浪費[3]。
upcalls的另外一個缺點,特別是請求由多是忙於處理其餘任務的Guest處理。 若是Guest忙碌或Guest閒置,則會由於等待Guest機有空閒或者喚醒而產生而外的懲罰。 這可能須要無限的時間,而且Hypervisor可能不得不依賴懲罰系統來確保Guest在合理的時間內作出響應。
最後,經過增長Hypervisor與其Guest之間的耦合,半虛擬機制可能難以維持。 每一個Hypervisor都有本身的半虛擬接口,每一個guest虛擬機必須實現每一個Hypervisor的接口。 半虛擬接口並不薄:微軟的半虛擬接口規範長達300頁[46]。 Linux提供了各類準虛擬鉤子,Hypervisor可使用它們與VM進行通訊[78]。儘管努力使半虛擬化接口標準化 ,但它們彼此不兼容,並隨着時間的推移而發展,添加功能甚至刪除一些功能(例如,MicrosoftHypervisor事件跟蹤)。 所以,大多數Hypervisor並不徹底支持標準化接口的工做,而專業操做系統則尋找替代解決方案[45,54]。
預虛擬化。 預虛擬化[42]是Guest從Hypervisor請求服務的另外一種機制,但請求是在Guest自身的上下文中提供的。 這是經過代碼注入實現的:Guest端留下存根,Hypervisor用Hypervisor代碼填充。 預虛擬化提供了對hypercalls的改進,由於它們在Guest和Hypervisor之間提供了更靈活的接口。 能夠說,預虛擬化存在一個基本限制:在guest虛擬機中運行的代碼是剝離的,沒法執行敏感操做,例如,訪問共享I/O設備。 所以,在預虛擬化中,在Guest端中運行的超級代碼仍然須要使用hypercalls與特權Hypervisor代碼進行通訊。
當Hypervisor或Guest嘗試從其餘上下文中推斷信息而不直接與其進行通訊時,就會發生自省。 經過內省,無需任何接口或協調。 例如,Hypervisor可能僅僅經過其存儲器訪問模式來嘗試推斷徹底未知的Guest的狀態。 內省和半虛擬化之間的另外一個區別是沒有發生上下文切換:執行內省的全部代碼都在請求者中執行。
虛擬機內省(VMI)。 當Hypervisor對Guest進行內省時,它被稱爲VMI [25]。 首次引入VMI是爲了經過從特權主機提供入侵檢測(IDS)和內核完整性檢查來加強VM安全性[10,24,25]。 VMI還應用於檢查點和重複數據刪除VM狀態[1],以及監控和實施Hypervisor策略[55]。 這些機制的範圍從簡單地觀察VM的內存和I/O訪問模式[36]到訪問VM OS數據結構[16],而且在最末端它們能夠修改VM狀態甚至直接將進程注入其中[26,19]。 VMI的主要好處是Hypervisor能夠在沒有上下文切換的狀況下直接調用VMI,而且guest虛擬機無需「意識到」檢查VMI是否正常運行。 可是,VMI很脆弱:VM OS中的一個無害的變化,例如爲數據結構添加額外字段的修補程序可能會致使VMI沒法正常工做[8]。 所以,VMI每每是一種「盡力而爲」的機制。
HVI。 在較小程度上,Guest可能會檢討它正在運行於其上的Hypervisor,稱爲Hypervisor內省(HVI)[72,61]。 HVI一般用於保護VM免受不受信任的Hypervisor[62]或惡意軟件以繞過Hypervisor安全[59,48]。
雖然Hypervisor提供了固定的界面,但OS研究代表,多年來靈活的操做系統界面能夠在不犧牲安全性的狀況下提升性能。 Exokernel提供了低級原語,並容許應用程序實現高級抽象,例如內存管理[22]。 SPIN容許擴展內核功能以提供特定於應用程序的服務,例如專門的進程間通訊[13]。 使這些擴展可以在不影響安全性的狀況下運行良好的關鍵特性是使用簡單的字節代碼來表達應用程序需求,並在與內核相同的保護環上運行此代碼。 咱們的工做受到這些研究的啓發,咱們的目標是在Hypervisor和Guest之間設計一個靈活的界面,以彌合語義鴻溝。
本文介紹了hyperupcalls,它知足了Hypervisor與guest虛擬機通訊的機制的需求,該機制是協調的(與VMI不一樣),由Hypervisor自己執行(與upcalls不一樣)而且不須要上下文切換(與hypercalls不一樣)。經過hyperupcalls,VM經過註冊可驗證代碼與Hypervisor進行協調。 而後,Hypervisor響應於事件(例如內存壓力或VM進入/退出)執行該代碼。 在某種程度上,hyperupcalls能夠被認爲是由Hypervisor執行的upcalls。
與VMI相比,訪問VM狀態的代碼由guest提供,所以hyperupcalls徹底瞭解guest虛擬機內部數據結構 - 實際上,hyperupcalls是使用guest虛擬機操做系統代碼庫構建的,並共享相同的代碼,從而簡化了維護,同時提供了操做系統具備表達機制來向底層Hypervisor描述其狀態。
與Hypervisor向guest虛擬機發出異步請求的upcalls相比,Hypervisor能夠隨時執行hyperupcall,即便guest虛擬機未運行也是如此。 經過upcalls,Hypervisor受到Guest的支配,這可能會延遲upcalls[6]。 此外,因爲upcalls的操做相似於遠程請求,所以upcalls可能會被迫以不一樣的方式實現OS功能。 例如,當刷新用於識別空閒Guest內存的規範技術時的ballooning中的遠程頁面[71]時,Guest使用虛擬進程來釋放內存壓力以釋放頁面。 經過hyperupcall,Hypervisor能夠像Guest內核線程同樣,直接掃描Guest的空閒頁面。
【Blooning 在虛擬機上安裝的VMtools就包括了ballooningdriver。它告訴Hypervisor哪些不活動的內存頁面能夠被收回。這對虛擬機上應用的性能是沒有任何影響的。】
Hyperupcalls相似於預虛擬化,由於代碼是跨語義間隙傳輸的。 傳輸代碼不只能夠實現更具表現力的通訊,還能夠將請求的執行移至間隙的另外一端,從而加強性能和功能。 與預虛擬化不一樣,Hypervisor不能信任虛擬機提供的代碼,而且Hypervisor必須確保高調用的執行環境在調用之間保持一致。
Hyperupcalls是由guest虛擬機提供給Hypervisor的簡短可驗證程序,用於提升性能或提供其餘功能。 guest虛擬機經過啓動時的註冊過程向Hypervisor提供hyperupcall,容許Hypervisor訪問guest虛擬機操做系統狀態,並在驗證後經過執行它們來提供服務。 Hypervisor運行hyperupcalls以響應事件或什麼時候須要查詢guest狀態。 hyperupcalls的體系結構和咱們爲利用它們而構建的系統如圖1所示。
咱們的目標是使hyperupcalls儘量簡單地構建。 爲此,咱們提供了一個完整的框架,容許程序員使用Guest操做系統代碼庫編寫hyperupcalls。 這極大地簡化了hyperupcalls的開發和維護。 該框架將此代碼編譯爲可驗證的代碼,Guest向Hypervisor註冊。 在下一節中,咱們將描述OS開發人員如何使用咱們的框架編寫hyperupcall。
Guest操做系統開發人員爲他們但願處理的每一個Hypervisor事件編寫hyperupcall。 Hypervisor和Guest贊成這些事件,例如VM進入/退出,頁面映射或虛擬CPU(VCPU)搶佔。 每一個hyperupcall都由預約義的標識符標識,很是相似於UNIX系統調用接口[56]。 表2給出了hyperupcall能夠處理的事件的示例。
hyperupcalls的一個關鍵屬性是必須保證代碼不會破壞Hypervisor。 爲了使hyperupcall安全,它必須只能訪問由Hypervisor指示的受限內存區域,運行一段有限的時間而不會阻塞,休眠或鎖定,而且只能使用明確容許的Hypervisor服務。
因爲Guest不受信任,Hypervisor必須創建一個保證這些安全屬性的安全機制。 咱們能夠選擇許多解決方案:軟件故障隔離(SFI)[70],攜帶證據的代碼[51]或安全語言,如Rust。 爲了實現hyperupcalls,咱們選擇了加強型Berkeley Packet Filter(eBPF)VM。
咱們選擇eBPF有幾個緣由。 首先,eBPF相對成熟:BPF是在20多年前引入的,而且在整個Linux內核中普遍使用,最初用於包過濾,但擴展到支持其餘用例,如沙盒系統調用(seccomp)和內核事件跟蹤[34] 。 eBPF受到普遍採用,並獲得各類運行時的支持[14,19]。 其次,能夠證實eBPF具備咱們所需的安全屬性,而且Linux附帶驗證器和JIT,用於驗證和有效執行eBPF代碼[74]。 最後,eBPF有一個LLVM編譯器後端,它使用編譯器前端(Clang)從高級語言生成eBPF字節碼。 因爲操做系統一般用C語言編寫,所以eBPF LLVM後端爲咱們提供了一種簡單的機制,可將不安全的Guest操做系統源代碼轉換爲可驗證的安全eBPF字節碼。
不幸的是,寫一個hyperupcall並不像在eBPF字節碼中從新編譯OS代碼那麼簡單。 可是,咱們的框架旨在使編寫hyperupcalls的過程儘量簡單和可維護。 該框架提供了三個關鍵功能,簡化了hyperupcalls的編寫。 首先,框架負責處理Guest地址轉換問題,所以guest OS符號可用於hyperupcall。 其次,該框架解決了eBPF對C代碼施加了很大的限制的侷限性。 最後,框架定義了一個簡單的接口,它爲 hyperupcall 提供了數據,所以能夠高效,安全地執行。
Guest操做系統符號和內存。 即便hyperupcalls能夠訪問guest虛擬機的整個物理內存,訪問guest虛擬機操做系統數據結構也須要知道它們駐留的位置。 操做系統一般使用內核地址空間佈局隨機化(KASLR)來隨機化OS符號的虛擬偏移,使其在編譯期間未知。 咱們的框架經過使用地址空間屬性關聯指針並注入代碼來調整指針,從而在運行時解析OS符號偏移。 當註冊hyperupcall時,guest虛擬機提供實際的符號偏移,使hyperupcall開發人員可以在C代碼中引用OS符號(變量和數據結構),就像它們被內核線程訪問同樣。
全局/本地Hyperupcalls。 並不是全部的hyperupcall都須要及時執行。 例如,通知Guest機Hypervisor事件(例如VM進入/退出或中斷注入)的通知僅影響Guest機而不影響Hypervisor。 咱們指的是隻影響將其註冊爲guest虛擬機的本地的hyperupcalls,以及影響整個Hypervisor做爲全局的hyperupcalls。 若是將超級調用註冊爲本地,咱們放寬時序要求並容許超級調用阻塞和休眠。 本地hyperupcalls在Guest的VCPU時間中與捕獲相似,所以行爲不端的超級調用會對本身進行懲罰。
可是,全局超級調用必須及時完成執行。 咱們確保對於Guest操做系統,全局hyperupcalls請求的頁面在超級調用期間被固定,並將可訪問的內存限制爲Guest總物理內存的2%(可配置)。 因爲本地hyperupcalls可能會阻塞,所以他們使用的內存不須要固定,容許本地hyperupcalls來解決全部·Guest內存。
解決eBPF限制。雖然eBPF具備表現力,但eBPF字節碼的安全保證意味着它不是圖靈完備且有限的,所以只有一部分C代碼能夠編譯成eBPF。 eBPF的主要限制是它不支持循環,ISA不包含原子,不能使用自修改代碼,函數指針,靜態變量,本機彙編代碼,而且不能太長且複雜而沒法驗證。
這些限制的後果之一是hyperupcall開發人員必須意識到hyperupcall的代碼複雜性,由於複雜的代碼將使驗證者失敗。 雖然這彷佛是一個不直觀的限制,但其餘使用BPF的Linux開發人員面臨一樣的限制,咱們在框架中提供了一個輔助函數來下降複雜性,例如memset和memcpy,以及執行本機原子操做的函數,如CMPXCHG。 表3中顯示了這些輔助函數的選擇。此外,咱們的框架掩蓋了內存訪問( 第 3.4 章 ),這大大下降了驗證的複雜性。 在實踐中,只要咱們當心地展開循環,咱們在使用 4096指令的設置和1024的堆棧深度 開發( 第 4 章 )中 的用例時沒有遇到驗證者問題 。
Hyperupcall接口。 當Hypervisor調用hyperupcall時,它會填充一個上下文數據結構,如表4所示. hyperupupall接收一個事件數據結構,它指示調用回調的緣由,以及一個指向guest虛擬機的指針(在Hypervisor的地址空間中,正在執行hyperupcall)。 當hyperupcall完成時,它能夠返回一個值,該值能夠由Hypervisor使用。
編寫hyperupcall。 藉助咱們的框架,操做系統開發人員編寫C代碼,能夠訪問操做系統變量和數據結構,並輔以框架的輔助功能。 典型的hyperupcall將讀取事件字段,讀取或更新OS數據結構並可能將數據返回到Hypervisor。 因爲hyperupcall是操做系統的一部分,開發人員能夠引用操做系統自己使用的相同數據結構 - 例如,經過頭文件。 這大大增長了hyperupcalls的可維護性,由於數據佈局更改在OS源和hyperupcall源之間同步。
值得注意的是,hyperupcall不能直接調用guest虛擬機操做系統函數,由於該代碼還沒有受到框架的保護。 可是,OS功能能夠編譯爲hyperupcalls並集成在通過驗證的代碼中。
一旦寫入了Hyperupcall,就須要將其編譯成eBPF字節碼,而後Guest才能將其註冊到Hypervisor。 咱們的框架經過Clang和eBPF LLUM後端運行hyperupcall C代碼,生成此字節碼做爲Guest操做系統構建過程的一部分,並進行一些修改以協助地址轉換和驗證:
Guest內存訪問。 爲了訪問Guest內存,咱們使用eBPF的直接數據包訪問(DPA)功能,該功能旨在容許程序在不使用輔助功能的狀況下安全有效地訪問網絡數據包。 咱們不是傳遞網絡數據包,而是將Guest端視爲「數據包」。 以這種方式使用DPA須要對eBPF LLUM後端進行錯誤修復[2],由於它是在假設數據包大小爲G64KB的狀況下編寫的。
地址翻譯。 Hyperupcalls容許Hypervisor無縫地使用Guest虛擬地址(GVA),這使得它看起來好像是在guest虛擬機中運行了hyperupcall。 可是,代碼其實是由Hypervisor執行的,其中使用了主機虛擬地址(HVAs),使得Guest機指針無效。 爲了容許在主機上下文中透明地使用Guest指針,所以須要將這些指針從GVA轉換爲HVAs。 咱們使用編譯器進行這些翻譯。
爲簡化此轉換,Hypervisor將GVA範圍連續映射到HVA空間,所以能夠經過調整基址輕鬆完成地址轉換。 因爲guest虛擬機可能須要hyperupcall來訪問多個連續的GVA範圍 - 例如,一個用於guest 1:1直接映射和OS文本部分[37] - 因此框架使用其各自的「地址空間」屬性來註釋每一個指針。 咱們擴展LLUM編譯器以使用此信息來注入eBPF代碼,該代碼經過簡單的減法操做將每一個指針從GVA轉換爲HVA。 應當注意,生成的代碼安全性不是由Hypervisor承擔的,而且在註冊Hyperupcall時被驗證。
綁定檢查。 驗證者拒絕直接內存訪問的代碼,除非它能夠確保內存訪問在「數據包」(在咱們的例子中是Guest內存)邊界內。 咱們不能期望hyperupcall程序員執行所需的檢查,由於添加它們的負擔很大。 所以,咱們加強編譯器以自動添加在每次 內存訪問 以前執行綁定檢查的代碼 ,從而容許驗證經過。 正如咱們在3.4節中所述,邊界檢查是使用屏蔽完成的,而不是分支以簡化驗證。
上下文緩存。 咱們的編譯器擴展引入了內在函數來獲取指向上下文的指針或讀取其數據。 在回調中常常須要上下文來調用輔助函數和轉換GVA。將上下文做爲函數參數須要進行侵入式更改,而且能夠防止guest虛擬機與其Hyperupcall之間共享代碼。 相反,咱們使用編譯器將上下文指針緩存在其中一個寄存器中,並在須要時檢索它。
將hyperupcall編譯成eBPF字節碼後,就能夠註冊了。 guest能夠隨時註冊hyperupcalls,但大多數hyperupcalls都是在Guest引導時註冊的。 guest提供hyperupcall事件ID,hyperupcall字節碼和hyperupcall將使用的虛擬內存。 每一個參數以下所述:
Hypervisor驗證每一個hyperupcall在註冊時是否安全。 咱們的驗證程序基於Linux eBPF驗證程序,並檢查hyperupcall的三個屬性:內存訪問,運行時指令數和使用的輔助函數。
理想狀況下,驗證是合理的,確保只有安全的代碼才能經過驗證,而且可以完整、成功驗證任何安全程序。 雖然健全性不會由於可能危及系統安全而受到損害,但許多驗證系統(包括eBPF)都會犧牲完整性來保證驗證者的簡單性。 在實踐中,驗證者要求以某種方式編寫程序以經過驗證 [66] ,即便這樣,驗證也可能因爲路徑爆炸而失敗。 這些限制與咱們使hyperupcalls易於構建的目標不一致。
咱們將討論下面的驗證程序檢查的屬性,以及咱們如何簡化這些檢查以使驗證儘量簡單。
有界運行時指令。 對於全局hyperupcalls,eBPF驗證程序確保hyperupcall的任何可能執行都包含有限數量的指令,這些指令由Hypervisor設置(默認爲4096)。 這能夠確保Hypervisor能夠及時執行hyperupcall,而且沒有無限循環能夠致使hyperupcall不退出。
內存訪問驗證。 驗證器確保存儲器訪問僅發生在由「分組」限定的區域中,該分組在超級調用中是在註冊期間提供的虛擬存儲器區域。 如前所述,咱們加強編譯器以自動添加代碼,證實每一個內存訪問都是安全的驗證者。
可是,天真地添加這樣的代碼會致使頻繁的驗證失敗。 當前的Linux eBPF驗證程序在驗證內存訪問安全性方面的能力很是有限,由於它要求它們以前會有比較和分支指令,以防止出現綁定訪問。 驗證者探索可能的執行路徑並確保其安全性。 雖然驗證者採用各類優化來修剪分支並避免走在每一個可能的分支上,但驗證一般耗盡可用資源而且由於咱們和其餘人經歷過而失敗[65]。
所以,咱們的加強編譯器不是使用compare和branch來確保內存訪問安全,而是添加了掩蓋每一個範圍內的內存訪問偏移的代碼,從而防止了越界內存訪問。 咱們加強驗證程序以將此屏蔽識別爲安全。 應用此加強功能後,咱們編寫的全部程序都經過了驗證。
輔助功能安全。 Hyperupcalls能夠調用輔助函數來提升性能並幫助限制運行時指令的數量。 輔助函數是標準的eBPF特性,驗證者強制執行能夠調用的輔助函數,這些函數可能因事件而異,具體取決於Hypervisor策略。 例如,Hypervisor可能在內存回收期間不容許使用flush_tlb_vcpu,由於它可能會阻塞系統一段延長的時間。
驗證程序檢查以確保輔助函數的輸入是安全的,確保輔助函數僅訪問容許訪問的內存。 雖然能夠在輔助函數中完成這些檢查,但新的eBPF擴展容許驗證程序靜態驗證輔助函數輸入。 此外,Hypervisor還能夠基於每一個事件設置輸入策略(例如,全局超級調用的內存大小)。
輔助函數的數量和複雜性也應該受到限制,由於它們成爲可信計算基礎的一部分。所以,咱們僅介紹簡單的輔助函數,這些函數主要依賴於guest虛擬機能夠直接或間接觸發的代碼,例如中斷注入。
eBPF安全性。最近發現的「幽靈」硬件漏洞[38,30]的兩個概念驗證漏洞攻擊目標是eBPF,這可能會引起對eBPF和高呼叫安全性的擔心。若是攻擊者能夠在特權上下文中運行非特權代碼,那麼利用這些漏洞就更容易了,就像hyperupcalls那樣,能夠防止發現的攻擊[63]。實際上,這些安全漏洞可能會使hyperupcall更具吸引力,由於當使用傳統的半虛擬機制(如upcalls和hypercalls)進行上下文切換時,它們的緩解技術(例如,返回堆棧緩衝區填充[33])會產生額外的開銷。
已驗證的hyperupcalls安裝在每一個guest虛擬機hyperupcall表中。一旦註冊並驗證了超級調用,Hypervisor就會響應事件執行hyperupcall。
Hyperupcall補丁。爲了不測試hyperupcall是否已註冊的開銷,Hypervisor使用代碼修補技術,在Linux中稱爲「靜態密鑰」[12]:只有當hyperupcalls是已註冊的狀態時,纔會在每一個Hypervisor上的Hyperupcall調用代碼上設置一個無操做指令。
訪問遠程VCPU狀態。一些hyperupcalls讀取或修改遠程VCPU的狀態。這些VCPU可能沒有運行,或者它們的狀態可能被Hypervisor的不一樣線程訪問。即便遠程VCPU被搶佔,Hypervisor也可能已經讀取了一些寄存器,而且在VCPU恢復執行以前不會指望它們發生變化。若是hyperupcall寫入遠程VCPU寄存器,它可能會破壞Hypervisor的常量甚至引入安全問題。
【vCPU表明虛擬中央處理單元。 將一個或多個vCPU分配給雲環境中的每一個虛擬機(VM)。 VM的操做系統將每一個vCPU視爲單個物理CPU核心。 若是主機具備多個CPU核心,那麼vCPU實際上由全部可用核心上的多個時隙組成,從而容許多個VM託管在較少數量的物理核心上。】
此外,讀取遠程VCPU寄存器會致使高開銷,由於VCPU狀態的一部分可能被緩存在另外一個CPU中,而且若是要讀取VCPU狀態,則必須首先將其寫回存儲器。更重要的是,在Intel CPU中,VCPU狀態不能經過公共指令訪問,而且必須首先「加載」VCPU,而後才能使用特殊指令(VMREAD和VMWRITE)訪問其狀態。切換加載的VCPU會產生很大的開銷,咱們的系統大約須要1800個週期。
爲了提升性能,咱們定義了一般搶佔Hypervisor的同步點,而且已知訪問VCPU狀態是安全的。在這些點上,咱們從VMCS「解密」VCPU寄存器並將它們寫入存儲器,以便hyperupcall能夠讀取它們。超級調用寫入遠程VCPU寄存器並更新已分解的值以標記Hypervisor,以在恢復該VCPU以前將寄存器值從新加載到VMCS中。訪問遠程VCPU的Hyperupcalls以盡力而爲的方式執行,僅在VCPU處於同步點時運行。在hyperupcall運行時,防止遠程VCPU恢復執行。
使用Guest操做系統鎖。一些OS數據結構受鎖保護。須要一致的Guest操做系統數據結構視圖的Hyperupcalls應遵照Guest操做系統規定的同步方案。然而,Hyperupcall只能機會性地獲取鎖,由於VCPU可能在持有鎖時被搶佔。可能須要調整鎖實現以支持外部實體的鎖定,而不是任何VCPU。釋放鎖可能須要相對較大的代碼來處理慢速路徑,這可能會阻止及時驗證超級調用。
雖然可能會提出各類臨時解決方案,但彷佛完整的解決方案要求Guest操做系統鎖定具備高上報功能。它還須要支持從eBPF代碼調用eBPF函數,以免可能致使驗證失敗的代碼大小膨脹。因爲最近添加了此支持,咱們的實現不包括鎖支持。
咱們的評估 由如下問題指導:
測試平臺。咱們的測試平臺包括一個帶有Intel ES-2670 CPU的48核雙插槽Dell PowerEdge 8630服務器,一個希捷ST1200磁盤,它運行帶有Linux內核v4.8的Ubuntu 17.04。基準測試適用於具備16個VCPU和8GB RAM的guest虛擬機。每次測量進行5次,並報告平均結果。
Hyperupcall原型。咱們在Linux v4.8和KVM上實現了一個用於hyperupcall支持的原型,這是一個集成在Linux中的Hypervisor。 Hyperupcalls經過修補的LLVM 4進行編譯,並經過Linux內核eBPF驗證程序使用咱們在第3章中描述的補丁進行驗證。咱們啓用Linux eBPF「JIT」引擎, 它在驗證後將eBPF代碼編譯爲本機機器代碼。已經研究了BPF JIT引擎的正確性而且能夠驗證[74]。
用例。咱們評估了表5中列出的四個hyperupcall用例。每一個用例演示了在不一樣的hypervisor事件中使用hyperupcalls,並使用不一樣複雜度的hyperupcalls。
咱們經過將hyperupcall與本機代碼的運行時間與相同函數進行比較來評估使用通過驗證的代碼來管理Hypervisor請求的開銷(表5)。總的來講,咱們發現驗證代碼相對於原生的絕對開銷很小(<250個循環)。對於處理TLB擊落到非活動核心的TLB用例,咱們的hyperupcall運行速度比本機代碼快,由於TLB刷新被推遲。驗證hyperupcall的開銷很小。對於最長的hyperupcall(跟蹤),驗證花了67ms。
雖然一般能夠有效地完成向VCPU的中斷傳送,可是若是目標VCPU沒有運行則會有很大的損失。若是CPU過載,而且調度目標VCPU須要搶佔另外一個VCPU,則會發生這種狀況。對於同步處理器間中斷(IPI),發送方僅在接收方指示IPI已交付和已接通後才恢復執行,從而致使太高的開銷。
在轉換後備緩衝器(TLB)擊落的狀況下,IPI傳遞的開銷最爲顯着,這是一種軟件協議,OS用於保持虛擬到物理地址映射相關的TLB緩存。因爲常見的CPU架構(例如,x86)不能使TLB在硬件中保持一致,所以修改映射的OS線程會將IPI發送到可能緩存映射的其餘CPU,而後這些CPU會刷新其TLB。 【TLB(轉換後備緩衝區)是從虛擬存儲器地址到物理存儲器地址的轉換緩存。當處理器更改地址的虛擬到物理映射時,它須要告訴其餘處理器使其緩存中的映射無效。 這個過程被稱爲「TLBshootdown】
咱們使用hyperupcalls來處理這種狀況,方法是註冊一個hyperupcall,當中斷傳遞給VCPU時,它會處理TLB擊落。Hypervisor在確保其處於靜止狀態後,使用中斷向量和目標VCPU提供超級調用。咱們的hyperupcall檢查此向量是不是「遠程函數調用」向量以及函數指針是否等於OS TLB刷新函數。若是是這樣,它運行此函數時只須要不多的修改:(1) 不使用本機指令刷新TLB,而是使用輔助函數執行TLB刷新,將其推遲到下一個VCPU重入; (2)即便禁用VCPU中斷也會執行TLB刷新,由於實驗上它能夠提升性能。
不能否認,還有另外一種解決方案:引入一個將TLB刷新委託給Hypervisor的超級調用[52]。雖然這種解決方案能夠防止TLB刷新,但它須要不一樣的代碼路徑,這可能會引入隱藏的錯誤[43],使與OS代碼的集成變得複雜或引入額外的開銷[44]。此解決方案也僅限於TLB刷新,而且沒法處理其餘中斷,例如,從新安排IPI。
評估。咱們使用默認的mpm_event模塊在guest虛擬機中運行Apache Web服務器[23],該模塊運行多線程工做程序來處理傳入的請求。爲了衡量性能,咱們使用ApacheBench,Apache HTTP服務器基準測試工具,使用16個鏈接生成10k請求,並測量請求延遲。結果顯示在圖2中,顯示hyperupcalls將延遲減小了1.3 x。即便物理CPU沒有超額訂閱,性能也會提升,這彷佛使人驚訝。可是,因爲VCPU在此基準測試中一般暫時處於空閒狀態,所以它們也能夠觸發對Hypervisor的退出。
根據定義,可用內存不包含任何所需數據,能夠丟棄。若是Hypervisor知道guest虛擬機中有哪些內存空閒,它能夠在內存回收,快照,實時遷移或鎖定步驟執行期間丟棄它[20]並避免I/O操做以保存和恢復其內容。然而,關於哪些存儲器頁面是空閒的信息由Guest持有,而且因爲語義上的差別而不可用於Hypervisor。
多年來,已經提出了幾種機制來通知Hypervisor哪些存儲器頁面使用半虛擬化是空閒的。然而,這些解決方案要麼將Guest端與Hypervisor耦合[60];因爲頻繁的超級調用引發的開銷[41]或僅限於實時遷移[73]。全部這些機制都受到固有的限制:沒有耦合Guest機和Hypervisor,Guest機須要與Hypervisor通訊哪些頁面是空閒的。
相反,支持hyperupcalls的Hypervisor不須要通知有關空閒頁面的信息。相反,guest虛擬機設置了一個hyperupcall,它根據頁面元數據(Linux的結構頁面)描述頁面是否可丟棄,而且基於在Linux中的is_free_buddy_page函數。當Hypervisor執行能夠從丟棄空閒Guest存儲器頁面(例如回收頁面)中受益的操做時,Hypervisor調用該超級調用來檢查該頁面是不是可丟棄的。當頁面已經被取消映射時,也會調用hyperupcall,以防止在再也不空閒時丟棄它的競賽。
檢查是否能夠丟棄頁面必須經過全局超級調用來完成,由於必須在有限且短期內提供答案。結果,guest虛擬機只能註冊其部份內存以供hyperupcall使用,由於該內存從不被分頁以確保及時執行hyperupcall。咱們的Linux guest虛擬機註冊了頁面元數據的內存 約佔Guest物理內存的1.6%。評價。爲了評估「內存丟棄」hyperupcall的性能,咱們測量其對因內存壓力而被回收內存的guest虛擬機的影響。當內存不足時,Hypervisor能夠執行「不合做的交換」 - 直接回收Guest機內存並將其交換到磁盤。然而,這種方法一般會致使次優的回收決策。或者,Hypervisor可使用memory ballooning,這是一種半虛擬機制,其中Guest模塊被告知主機內存壓力並致使Guest直接回收內存[71]。而後Guest能夠作出知識淵博的回收決定並丟棄空閒頁面。雖然內存膨脹一般表現良好,可是當內存須要忽然回收時性能會受到影響[4,6]或當Guest盤設置在網絡附加存儲[68]上時,所以不在高內存壓力下使用[21]。
爲了評估內存膨脹,不合做的交換和使用hyperupcalls進行交換,咱們運行了一個場景,其中須要忽然回收內存和物理CPU,以便容納新的guest虛擬機。在Guest,咱們開始並退出「memhog」,使4GB可用於在Guest回收。接下來,咱們讓Guest忙運行與低內存佔用CPU密集型任務 一 的sysbench的CPU性能測試,它計算使用的全部虛擬處理器[39]素數。
如今,在系統繁忙的狀況下,咱們經過增長內存和CPU過量使用來模擬回收資源以啓動新guest虛擬機的需求。咱們下降了guest虛擬機可用的物理CPU數量,並將其限制爲僅1GB內存。咱們根據爲guest虛擬機分配的物理CPU數來衡量回收內存所需的時間(圖3a)。這模擬了一個新的Guest開始。而後,咱們中止增長內存壓力,並使用4GB的SysBench文件讀取基準測量運行具備大內存佔用量的Guest應用程序的時間(圖3b)。這模擬了guest虛擬機重用由Hypervisor回收的頁面。
當物理CPU過量使用時,Ballooning會緩慢地回收內存(最多110秒),由於內存回收操做會在CPU時間與CPU密集型任務競爭。不合做的交換(交換基礎)能夠更快地回收(32秒),但因爲它不知道內存頁是不是空閒的,它會致使更高的Guest空閒頁面的開銷。相反,當使用hyperupcalls時,Hypervisor能夠促進空閒頁面的回收並丟棄它們,從而回收內存的速度比Ballooning快8倍,而內存只有10%的減速。
固然,CPU過量使用並非Ballooning無響應或沒法使用的惟一狀況。當內存壓力很是高時,Hypervisor會避免膨脹,而變成使用主機級交換[67]。 hyperupcalls能夠與Ballooning協同運行:Hypervisor能夠正常使用Ballooning,並在資源壓力高或Ballooning沒有響應時使用hyperupcalls。
事件跟蹤是調試正確性和性能問題的重要工具。可是,收集虛擬化工做負載的跟蹤有些限制。在guest虛擬機內收集的跟蹤不會顯示Hypervisor事件,例如強制VM退出時,這會對其產生重大影響 性能。對於在Hypervisor中收集的跟蹤信息,他們須要有關Guest操做系統符號的知識[15]。沒法在雲環境中收集此類跟蹤。此外,每一個跟蹤僅收集部分事件,並不顯示guest虛擬機和虛擬機監控程序事件如何交錯。
爲了解決這個問題,咱們在hyperupcall中運行Linux內核跟蹤工具ftrace [57]。 Ftrace很是適合在hyperupcall中運行。它簡單,無鎖,而且能夠在多個上下文中啓用併發跟蹤:不可屏蔽中斷(NMI),硬件和軟件中斷處理程序以及用戶進程。所以,它很容易適應與Guest事件同時跟蹤Hypervisor事件。使用ftrace hyperupcall,guest虛擬機能夠在一個統一日誌中跟蹤Hypervisor和Guest事件,從而簡化調試。因爲跟蹤全部事件僅使用Guest邏輯,所以新的OS版本能夠更改跟蹤邏輯,而無需更改Hypervisor。評價。跟蹤是有效的,儘管超級調用複雜度(3308 eBPF指令),由於大多數代碼處理不常見的事件,處理跟蹤頁面填滿的狀況。使用hyperupcalls跟蹤比使用本機代碼232個週期要慢,這仍然比Hypervisor和Guest端之間的上下文切換時間短得多。 .
跟蹤是性能調試的有用工具,能夠暴露各類開銷[79]。例如,經過在VM-exit事件上註冊ftrace,咱們看到許多進程(包括短時間進程)因爲CPUID指令的執行而觸發多個VM退出,這些指令枚舉CPU功能而且必須由Hypervisor。咱們發現大多數Linux應用程序使用的GNU C庫使用CPUID來肯定支持的CPU功能。經過擴展Linux虛擬動態共享對象(vDSO)以便應用程序查詢支持的CPU功能而不觸發退出,能夠防止這種開銷。
操做系統採用的一種常見安全加固機制是「自我保護」:操做系統代碼和不可變數據寫保護。可是,這種保護是使用頁表完成的,容許惡意軟件經過修改頁表條目來規避它。爲防止此類攻擊,建議使用嵌套頁表,由於這些表沒法從guest虛擬機中訪問[50]。
可是,嵌套只能提供有限數量的策略,例如,不能將容許訪問受保護內存的Guest代碼列入白名單。 Hyperupcalls更具表現力,容許Guest以靈活的方式指定內存保護。
咱們使用hyperupcalls來提供Hypervisor級別的Guest內核自我保護,能夠輕鬆修改它以適應複雜的策略。在咱們的實現中,guest設置了一個標記受保護頁面的位圖,並在退出事件上註冊hyperupcall,它檢查退出緣由,是否發生了內存訪問以及guest虛擬機是否嘗試根據位圖寫入受保護的內存。若是嘗試訪問受保護的內存,則會觸發VM關閉。 guest虛擬機在「頁面映射」事件上設置了另外一個超級調用,該事件查詢Guest頁面幀所需的保護。此超級調用可防止虛擬機監控程序主動預先取消Guest機內存。
評價。這種超級調用代碼很簡單,但每次退出會產生43個週期的開銷。能夠說,只有已經經歷過大量上下文切換的工做負載纔會受到額外開銷的影響。現代CPU能夠防止這種頻繁的切換。
彌合語義差距是關鍵性能,而且Hypervisor能夠爲Guest提供高級服務。如今使用Hypercalls和upcalls來彌補差距,但它們有幾個缺點:Hypercalls不能由Hypervisor啓動,upcalls沒有有限的運行時,而且都會致使上下文切換的懲罰。內省,避免上下文切換的替代方案多是不可靠的,由於它依賴於觀察而不是顯式接口。 Hyperupcalls經過容許guest虛擬機將其邏輯暴露給Hypervisor來克服這些限制,經過使hyperupcall可以直接安全地執行guest虛擬機邏輯來避免上下文切換。
老師上課點評的時候,認同了我所理解的第三方公證監視器的思想提取,真是amazing~~!!語義鴻溝問題是虛擬機的一個很經典的問題,他必須存在可是又對性能有極大的影響。那麼爲何不像是內省同樣在虛擬機裏面放一個監視器呢?由於這樣會破壞虛擬機的封裝性。虛擬機必需要自我欺騙成我是一個完整的機器,而不是別的物理機的一個時隙,因此若是發現本身體內還存在一個監視器,會做何感想?!第三方的話,虛擬機徹底能夠考慮成是對外的通信。