間虛擬化實(shí)戰(zhàn):從虛擬定時(shí)器設(shè)計(jì)到Guest時(shí)鐘校正)
1. 為什么專門(mén)聊xvisor的時(shí)間虛擬化1.1 從一次“Guest卡死”說(shuō)起先講個(gè)我實(shí)際踩過(guò)的坑。前兩年在一款A(yù)RM嵌入式板卡上做多系統(tǒng)隔離方案選了xvisor作為T(mén)ype-1 hypervisor跑一個(gè)輕量級(jí)Linux Guest。功能驗(yàn)證基本通過(guò)但跑了一天后發(fā)現(xiàn)Guest的gettimeofday返回值跟墻鐘差了將近40秒再跑幾天直接差到幾分鐘。起初我以為只是NTP沒(méi)同步后來(lái)用串口抓日志發(fā)現(xiàn)vCPU在定時(shí)器中斷里反復(fù)進(jìn)出Guest的時(shí)鐘中斷頻率明顯不對(duì)甚至出現(xiàn)連續(xù)drop tick的情況。這個(gè)問(wèn)題帶我走進(jìn)了xvisor的時(shí)間虛擬化實(shí)現(xiàn)。說(shuō)實(shí)話虛擬化領(lǐng)域大家聊得最多的是CPU虛擬化、內(nèi)存虛擬化、中斷虛擬化時(shí)間虛擬化經(jīng)常被一筆帶過(guò)。但真正做過(guò)hypervisor的人都知道時(shí)間虛擬化是“看著不起眼、做起來(lái)頭疼”的模塊——它跟調(diào)度器、中斷控制器、物理定時(shí)器、Guest內(nèi)部時(shí)鐘源全都有耦合。Guest卡死、時(shí)鐘漂移、性能抖動(dòng)十有八九是時(shí)間虛擬化沒(méi)設(shè)計(jì)好。這篇文章就以xvisor為例掰開(kāi)揉碎講清楚時(shí)間虛擬化是怎么設(shè)計(jì)的。適合三類人看一是做嵌入式虛擬化方案選型的技術(shù)負(fù)責(zé)人想評(píng)估xvisor能不能用二是自己寫(xiě)或維護(hù)hypervisor的開(kāi)發(fā)者想?yún)⒖家惶卓陕涞氐膖imer虛擬化設(shè)計(jì)三是正在被“Guest時(shí)鐘不準(zhǔn)”“定時(shí)器中斷風(fēng)暴”之類問(wèn)題折磨的運(yùn)維和驅(qū)動(dòng)開(kāi)發(fā)同學(xué)。1.2 xvisor是誰(shuí)為什么拿它當(dāng)例子xvisor是一個(gè)開(kāi)源Type-1 hypervisor主打輕量、可移植官方支持ARM、x86、RISC-V等架構(gòu)在嵌入式領(lǐng)域常被拿來(lái)跟KVM、Xen、Xenomai這類方案做對(duì)比。它的代碼量比Linux內(nèi)核小好幾個(gè)數(shù)量級(jí)但虛擬化核心要素齊全CPU虛擬化、設(shè)備模型、中斷虛擬化、時(shí)間虛擬化都有完整實(shí)現(xiàn)。也正因?yàn)榇a規(guī)??煽胤浅_m合用來(lái)“解剖”虛擬化內(nèi)部原理。選xvisor講時(shí)間虛擬化還有一個(gè)重要原因它的設(shè)計(jì)思路很“直給”。不像大型hypervisor那樣為了兼容各種硬件而堆滿條件分支xvisor在時(shí)間子系統(tǒng)的抽象上做得很精簡(jiǎn)——核心對(duì)象少、依賴關(guān)系清晰、中斷流明確。讀懂它的時(shí)間虛擬化再回頭看KVM的kvm-clock、Xen的xen_timer會(huì)發(fā)現(xiàn)底層邏輯是相通的只是工程復(fù)雜度不同。這篇文章我不會(huì)只貼源碼也不會(huì)只講理論。我會(huì)從“Guest感知的時(shí)間到底是什么”這個(gè)根問(wèn)題出發(fā)拆解xvisor的設(shè)計(jì)取舍然后給出我在實(shí)際編譯、配置、驗(yàn)證過(guò)程中用到的操作步驟和調(diào)試命令最后把我踩過(guò)的坑整理成排查清單。保證你看完能對(duì)時(shí)間虛擬化有一個(gè)完整的、可復(fù)用的認(rèn)知框架。2. 時(shí)間虛擬化的核心難點(diǎn)Guest眼中的“時(shí)間”從哪來(lái)2.1 三條時(shí)間線Guest同時(shí)在用三套時(shí)鐘要理解時(shí)間虛擬化先得搞清楚虛擬機(jī)里的“時(shí)間”不是一個(gè)東西。實(shí)際運(yùn)行中一個(gè)Guest至少依賴三條時(shí)間線。第一是墻鐘時(shí)間wall clock也就是人類可讀的日期時(shí)間比如2025年幾月幾日幾點(diǎn)幾分。Guest里的用戶程序調(diào)用clock_gettime(CLOCK_REALTIME)拿到的基本是這條線。墻鐘時(shí)間通常是軟件維護(hù)的根節(jié)點(diǎn)是某個(gè)RTC芯片或網(wǎng)絡(luò)時(shí)間協(xié)議。第二是單調(diào)遞增時(shí)間monotonic time用來(lái)計(jì)算時(shí)間差、超時(shí)判斷比如clock_gettime(CLOCK_MONOTONIC)。這條線不能被用戶跳變只能單調(diào)往前走。內(nèi)核里的jiffies、高精度定時(shí)器hrtimer都依賴它。第三是硬件定時(shí)器時(shí)間線。CPU或者SoC內(nèi)部有周期性的硬件定時(shí)器——ARM的Generic Timer、x86的TSC/APIC timer、RISC-V的mtime等。操作系統(tǒng)用它來(lái)產(chǎn)生周期性tick中斷驅(qū)動(dòng)調(diào)度器、時(shí)間輪、延遲執(zhí)行等機(jī)制。三條時(shí)間線各有各的虛擬化難點(diǎn)。墻鐘時(shí)間主要是“怎么把宿主機(jī)的當(dāng)前時(shí)間安全地給Guest”單調(diào)時(shí)間主要涉及“offset和速率怎么折算”硬件定時(shí)器則是真正的硬骨頭——因?yàn)镚uest要直接操作硬件定時(shí)器寄存器而hypervisor必須截獲這些訪問(wèn)并進(jìn)行模擬。2.2 直接透?jìng)鞯闹旅鼏?wèn)題Guest能碰到物理中斷有人可能會(huì)問(wèn)既然ARM有Generic Timer這種虛擬化友好的硬件直接把定時(shí)器硬件給Guest用不就行了問(wèn)題沒(méi)那么簡(jiǎn)單。ARM Generic Timer確實(shí)提供了CNTVCT虛擬計(jì)數(shù)器、CNTVOFF虛擬偏移、CNTV_TVAL虛擬定時(shí)器這些機(jī)制硬件層面就已經(jīng)支持時(shí)間虛擬化。但xvisor的目標(biāo)是通過(guò)設(shè)備樹(shù)描述虛擬平臺(tái)要支持不同板卡、不同ARM實(shí)現(xiàn)不能默認(rèn)所有平臺(tái)都帶完整的虛擬化定時(shí)器擴(kuò)展。更重要的是即使硬件支持Guest對(duì)定時(shí)器的訪問(wèn)仍然會(huì)產(chǎn)生虛擬中斷hypervisor必須參與中斷路由。直接透?jìng)髯畹湫偷臑?zāi)難是Guest把定時(shí)器的觸發(fā)周期設(shè)置得極短比如每10微秒產(chǎn)生一次中斷而且沒(méi)有經(jīng)過(guò)hypervisor的rate limiting物理CPU就被中斷風(fēng)暴占滿其他vCPU完全餓死。又比如Guest在suspend時(shí)把定時(shí)器關(guān)閉但hypervisor不知道導(dǎo)致喚醒后時(shí)間完全錯(cuò)亂。所以xvisor的總體思路很明確虛擬定時(shí)器必須由hypervisor統(tǒng)一管理Guest不能直接拿到物理定時(shí)器的全部控制權(quán)。2.3 時(shí)間虛擬化的三個(gè)基本操作Offset、Dilation、Injection拋開(kāi)硬件細(xì)節(jié)時(shí)間虛擬化本質(zhì)上就是對(duì)Guest可見(jiàn)的時(shí)間軸做三個(gè)操作。第一個(gè)是Offset偏移。虛擬機(jī)的啟動(dòng)時(shí)間點(diǎn)通常不等于宿主機(jī)的時(shí)間零點(diǎn)而且多個(gè)虛擬機(jī)可能要求看到不同的“當(dāng)前時(shí)間”。hypervisor維護(hù)一個(gè)基準(zhǔn)時(shí)間點(diǎn)為每個(gè)Guest計(jì)算一個(gè)偏移量。Guest讀時(shí)間時(shí)hypervisor返回“物理時(shí)間 offset”。第二個(gè)是Dilation縮放。虛擬機(jī)的時(shí)鐘速率不一定跟物理時(shí)鐘1:1。調(diào)試場(chǎng)景里經(jīng)常需要“放慢”Guest的時(shí)間比如把Guest的時(shí)鐘減速到0.5倍速這樣跑定時(shí)器相關(guān)的競(jìng)態(tài)條件時(shí)更容易復(fù)現(xiàn)。反過(guò)來(lái)性能測(cè)試時(shí)又想讓Guest時(shí)間盡量貼近物理時(shí)間。實(shí)現(xiàn)縮放最簡(jiǎn)單的方法是修改虛擬定時(shí)器的周期——物理定時(shí)器每10ms觸發(fā)一次但告訴Guest每次都過(guò)了20msGuest感知的時(shí)間就被拉伸了兩倍。第三個(gè)是Injection注入。虛擬定時(shí)器到期后hypervisor不能直接讓物理中斷打到Guest的向量表里而是需要構(gòu)造一個(gè)虛擬中斷通過(guò)中斷控制器的虛擬化接口注入給指定的vCPU。注入的時(shí)機(jī)、優(yōu)先級(jí)、目標(biāo)vCPU選擇都有講究做不好就會(huì)出現(xiàn)中斷丟失或者重復(fù)注入。xvisor的時(shí)間虛擬化本質(zhì)就是一套把這三件事做扎實(shí)的框架。3. xvisor的時(shí)間虛擬化設(shè)計(jì)拆解3.1 兩層定時(shí)器Host Timer與Virtual Timer先區(qū)分兩個(gè)概念xvisor代碼里明確區(qū)分了“主機(jī)定時(shí)器”host timer和“虛擬定時(shí)器”virtual timer。這個(gè)概念如果不厘清代碼會(huì)越看越亂。Host Timer是物理定時(shí)器由xvisor自身使用用于產(chǎn)生hypervisor內(nèi)部的心跳tick——調(diào)度器的時(shí)間片輪轉(zhuǎn)、延遲任務(wù)、超時(shí)檢查都靠它。xvisor把物理定時(shí)器配置成固定周期比如配置CONFIG_SCHED_PERIOD相關(guān)的值每次中斷到來(lái)時(shí)xvisor的時(shí)間子系統(tǒng)統(tǒng)一處理。Virtual Timer是給每個(gè)Guest vCPU仿真的定時(shí)器。每個(gè)vCPU維護(hù)自己的虛擬定時(shí)器狀態(tài)——什么時(shí)候到期、周期多少、中斷注入給誰(shuí)。Guest在設(shè)備樹(shù)里看到的定時(shí)器節(jié)點(diǎn)就是xvisor為它創(chuàng)建的一個(gè)虛擬設(shè)備。這樣的兩層設(shè)計(jì)有一個(gè)明顯好處物理定時(shí)器只有一個(gè)中斷頻率可控可預(yù)測(cè)虛擬定時(shí)器數(shù)量可以任意多且互相隔離。Guest無(wú)論怎么折騰自己的定時(shí)器配置影響的只是它自己的虛擬定時(shí)器上下文物理中斷的穩(wěn)定節(jié)奏始終由hypervisor掌握。3.2 核心對(duì)象timer、timer_event、vcpu_timexvisor的時(shí)間虛擬化核心代碼在core/time.c和對(duì)應(yīng)架構(gòu)的arch/arm/time.cARM平臺(tái)。語(yǔ)言用的是C但我先不貼大段源碼而是把里面的關(guān)鍵對(duì)象捋清楚。第一個(gè)核心對(duì)象是struct timer代表一個(gè)“到點(diǎn)需要處理”的注冊(cè)項(xiàng)。它包含回調(diào)函數(shù)、參數(shù)、到期時(shí)間、周期標(biāo)志等。xvisor的定時(shí)器管理類似內(nèi)核的timer wheel但精簡(jiǎn)得多基本是一個(gè)按到期時(shí)間排序的單向鏈表。注冊(cè)一個(gè)定時(shí)器意味著把回調(diào)掛到鏈表上時(shí)間一到回調(diào)執(zhí)行。第二個(gè)核心對(duì)象是struct timer_event這是虛擬定時(shí)器的事件描述。每個(gè)vCPU維護(hù)一個(gè)timer_event代表該vCPU當(dāng)前正在等待的虛擬定時(shí)器到期點(diǎn)。Guest設(shè)置CNTP_TVAL或CNTV_TVAL時(shí)xvisor把它翻譯成一個(gè)絕對(duì)到期時(shí)間掛到timer_event上。第三個(gè)核心對(duì)象可以叫vcpu_time context它記錄了虛擬時(shí)間與物理時(shí)間的換算關(guān)系offset、dilation系數(shù)、上次同步點(diǎn)等。每次Guest讀取虛擬計(jì)數(shù)器時(shí)xvisor根據(jù)這個(gè)context計(jì)算出返回值。這三個(gè)對(duì)象的關(guān)系是物理tick到達(dá) → host timer鏈表中找到最近的timer_event → 判斷哪個(gè)vCPU的虛擬定時(shí)器到期 → 更新vcpu_time context → 構(gòu)造虛擬中斷注入給目標(biāo)vCPU。3.3 虛擬中斷的流轉(zhuǎn)路徑從物理中斷到Guest中斷向量中斷路徑是時(shí)間虛擬化里最容易出bug的地方。我梳理一下xvisor在這條鏈路上做了什么。首先是物理中斷入口。ARM架構(gòu)下定時(shí)器中斷是PPIPrivate Peripheral Interrupt每個(gè)CPU獨(dú)立。xvisor在啟動(dòng)階段把物理定時(shí)器中斷注冊(cè)到自己的中斷處理框架里中斷到來(lái)先進(jìn)入hypervisor的異常處理向量。然后是host timer處理。xvisor的時(shí)間子系統(tǒng)調(diào)用timer_expire之類邏輯遍歷定時(shí)器鏈表找出所有到期項(xiàng)。這里有一個(gè)實(shí)現(xiàn)細(xì)節(jié)鏈表項(xiàng)不是簡(jiǎn)單地刪除而是會(huì)先判斷是單次定時(shí)器還是周期定時(shí)器。單次定時(shí)器直接摘除周期定時(shí)器則重新計(jì)算下一次到期時(shí)間并重新入鏈。接著是虛擬到期判定。每個(gè)timer_event到期并不意味著Guest一定需要中斷。xvisor會(huì)先比較虛擬定時(shí)器的到期時(shí)間和當(dāng)前虛擬時(shí)間如果還沒(méi)有真正到期說(shuō)明是host timer提前醒了的“偽喚醒”直接跳過(guò)。只有真正的到期才進(jìn)入下一步。最后是虛擬中斷注入。xvisor調(diào)用中斷控制器的虛擬化接口把虛擬定時(shí)器中斷置為pending狀態(tài)。對(duì)于ARM GIC這通常涉及GICD_ISPENDR或者使用GIC的虛擬化擴(kuò)展GICv2/v3的list register機(jī)制。注入的目標(biāo)必須是定時(shí)器所屬vCPU當(dāng)前正在運(yùn)行的物理CPU否則還要考慮vCPU遷移帶來(lái)的中斷路由問(wèn)題。這條路徑如果捋清楚了調(diào)試時(shí)間虛擬化就有抓手。每次“Guest時(shí)間不準(zhǔn)”的問(wèn)題本質(zhì)上可以歸因到這條鏈路中的某一環(huán)offset計(jì)算錯(cuò)、虛擬到期判定錯(cuò)、中斷注入丟、物理中斷頻率抖動(dòng)。3.4 設(shè)計(jì)取舍為什么這么精簡(jiǎn)代價(jià)是什么xvisor的時(shí)間虛擬化設(shè)計(jì)主打精簡(jiǎn)這是有代價(jià)的理解取舍比記代碼更有價(jià)值。第一個(gè)取舍是用鏈表而不是紅黑樹(shù)或時(shí)間輪管理定時(shí)器。xvisor面向嵌入式場(chǎng)景虛擬機(jī)的數(shù)量、定時(shí)器數(shù)量都不會(huì)特別大鏈表在幾十個(gè)節(jié)點(diǎn)規(guī)模下表現(xiàn)足夠而且實(shí)現(xiàn)簡(jiǎn)單、便于驗(yàn)證。代價(jià)是定時(shí)器數(shù)量多時(shí)插入復(fù)雜度為O(n)但實(shí)際場(chǎng)景中很少成為瓶頸。第二個(gè)取舍是把虛擬時(shí)間映射做成線性關(guān)系offset dilation而不是像KVM那樣做復(fù)雜的時(shí)鐘源切換。線性映射的優(yōu)點(diǎn)是計(jì)算開(kāi)銷小、Guest看到的時(shí)間連續(xù)缺點(diǎn)是無(wú)法精確模擬“Guest暫停期間時(shí)間不走”這類的特殊語(yǔ)義只能靠offset調(diào)整來(lái)補(bǔ)償。第三個(gè)取舍是以tick為基本驅(qū)動(dòng)而不是完全事件驅(qū)動(dòng)。xvisor的host timer按固定周期走周期內(nèi)所有虛擬定時(shí)器到期行為都會(huì)延遲到下一個(gè)tick統(tǒng)一處理。這樣會(huì)引入最多一個(gè)tick周期的延遲但對(duì)大多數(shù)嵌入式Guest來(lái)說(shuō)完全可接受而換來(lái)的是實(shí)現(xiàn)簡(jiǎn)單、不用頻繁配置物理定時(shí)器。這三個(gè)取舍合在一起就是xvisor時(shí)間虛擬化“小而美”的底層邏輯。你要真想給別人講清楚xvisor的時(shí)間虛擬化把這幾個(gè)取舍講明白比背代碼更有說(shuō)服力。4. 實(shí)操?gòu)木幾g配置到時(shí)間正確性驗(yàn)證4.1 配置xvisor時(shí)間相關(guān)選項(xiàng)理論講完動(dòng)手環(huán)節(jié)還是要走的。我在QEMU模擬的ARM平臺(tái)用virt機(jī)器上做過(guò)完整的xvisor Linux Guest驗(yàn)證。第一步是編譯xvisor時(shí)間虛擬化主要關(guān)系到幾個(gè)配置宏。xvisor的配置在config/arm/下一般先復(fù)制默認(rèn)配置再改。時(shí)間相關(guān)的關(guān)鍵項(xiàng)大致有CONFIG_SCHED_PERIOD物理調(diào)度周期也就是host timer的tick周期。默認(rèn)值通常是1000000納秒1ms。這個(gè)值直接決定定時(shí)器中斷頻率影響虛擬定時(shí)器到期精度。CONFIG_TIMER_FREQ虛擬定時(shí)器頻率基準(zhǔn)ARM平臺(tái)往往對(duì)應(yīng)Generic Timer的頻率通常在設(shè)備樹(shù)里聲明。CONFIG_MAX_VCPUS_PER_GUEST單Guest的最大vCPU數(shù)會(huì)影響每vCPU定時(shí)器上下文的分配。我實(shí)測(cè)時(shí)把CONFIG_SCHED_PERIOD調(diào)成1000000Guest的dmesg里時(shí)鐘源是arch_sys_countertick穩(wěn)定在1000Hz左右。如果你想要更高精度的虛擬定時(shí)器可以把CONFIG_SCHED_PERIOD調(diào)小到100000100微秒但代價(jià)是hypervisor自身的中斷開(kāi)銷會(huì)明顯變大跑高負(fù)載時(shí)會(huì)看到宿主機(jī)CPU占用上升。注意xvisor的配置項(xiàng)不是每個(gè)版本都一樣源碼版本不同宏名可能略有差異。建議先查config/arm/*.conf里的實(shí)際定義再對(duì)照include/下的頭文件確認(rèn)宏作用別盲目照抄老文章的配置。編譯命令我用的經(jīng)典三連cd xvisor make ARCHarm CROSS_COMPILEaarch64-linux-gnu- defconfig # 手動(dòng)調(diào)整.config里時(shí)間相關(guān)項(xiàng) make ARCHarm CROSS_COMPILEaarch64-linux-gnu- -j8編譯產(chǎn)物是xvisor二進(jìn)制和xvisor.dtb。虛擬機(jī)鏡像和文件系統(tǒng)我是用buildroot現(xiàn)成做的這里就不展開(kāi)了。4.2 設(shè)備樹(shù)里的定時(shí)器節(jié)點(diǎn)Guest看到的虛擬定時(shí)器xvisor通過(guò)設(shè)備樹(shù)向Guest描述虛擬硬件。在xvisor的dts/目錄下你可以找到arm虛擬平臺(tái)的設(shè)備樹(shù)模板定時(shí)器節(jié)點(diǎn)大概長(zhǎng)這樣timer { compatible arm,armv8-timer; interrupts GIC_PPI 13 IRQ_TYPE_LEVEL_LOW, GIC_PPI 14 IRQ_TYPE_LEVEL_LOW, GIC_PPI 11 IRQ_TYPE_LEVEL_LOW, GIC_PPI 10 IRQ_TYPE_LEVEL_LOW; clock-frequency 62500000; };這個(gè)節(jié)點(diǎn)對(duì)Guest來(lái)說(shuō)就是它的物理定時(shí)器但實(shí)際上x(chóng)visor會(huì)把clock-frequency聲明的頻率作為虛擬定時(shí)器的頻率而不是直接透?jìng)髡鎸?shí)頻率。這里有個(gè)容易踩的坑clock-frequency的值必須跟CONFIG_TIMER_FREQ配合好。如果設(shè)備樹(shù)里聲明的是62.5MHz而xvisor內(nèi)部用的基準(zhǔn)是50MHzGuest里的clock_gettime跑一段時(shí)間后就會(huì)出現(xiàn)整數(shù)倍的漂移。建議的做法是把設(shè)備樹(shù)里的clock-frequency跟實(shí)際板卡的真實(shí)timer頻率解耦單獨(dú)定義一個(gè)對(duì)齊xvisor內(nèi)部頻率的值保證虛擬計(jì)數(shù)器換算關(guān)系閉合。Guest內(nèi)核起來(lái)后可以通過(guò)以下命令確認(rèn)它看到的定時(shí)器信息cat /sys/devices/system/clockevents/clockevent0/current_device cat /proc/interrupts | grep arch_timer cat /proc/timer_list | head -50正常情況下Guest應(yīng)該能看到arch_timer事件設(shè)備并且中斷計(jì)數(shù)會(huì)隨時(shí)間穩(wěn)定增長(zhǎng)。如果中斷計(jì)數(shù)不動(dòng)說(shuō)明虛擬定時(shí)器中斷沒(méi)有注入成功需要優(yōu)先檢查GIC的虛擬中斷配置。4.3 驗(yàn)證時(shí)間正確性的三板斧配置好之后怎么判斷時(shí)間虛擬化是“對(duì)的”我總結(jié)了三板斧。第一板斧是單調(diào)性驗(yàn)證。在Guest里寫(xiě)一個(gè)簡(jiǎn)單循環(huán)反復(fù)讀取CLOCK_MONOTONIC確認(rèn)返回值嚴(yán)格遞增不倒退。時(shí)間虛擬化做得粗糙時(shí)最容易出現(xiàn)“時(shí)間倒流”——因?yàn)樘摍C(jī)在suspend時(shí)沒(méi)做好offset補(bǔ)償恢復(fù)瞬間Guest看到的時(shí)間比暫停前還要早。我用的一個(gè)極簡(jiǎn)腳本思路大致是#!/bin/bash prev0 for i in $(seq 1 100000); do cur$(date %s%N) if [ $cur -lt $prev ]; then echo time go backwards: $prev - $cur break fi prev$cur done第二板斧是對(duì)比驗(yàn)證。同時(shí)記錄宿主機(jī)和Guest的時(shí)間換算成同一個(gè)參考系打點(diǎn)對(duì)比。不是要求完全一致而是要求偏移量在一段時(shí)間內(nèi)保持相對(duì)穩(wěn)定波動(dòng)在可接受范圍。如果Guest時(shí)間線性偏離宿主機(jī)通常是dilation參數(shù)不對(duì)如果忽快忽慢通常是虛擬定時(shí)器到期處理不穩(wěn)定。第三板斧是壓力驗(yàn)證。在Guest里跑周期性任務(wù)比如每10ms打印一個(gè)時(shí)間戳連續(xù)跑一小時(shí)。人為給宿主機(jī)加負(fù)載比如同時(shí)跑多個(gè)CPU密集型vCPU或?qū)崟r(shí)任務(wù)觀察打印間隔是否穩(wěn)定。時(shí)間虛擬化最怕的就是“鄰居干擾”——?jiǎng)e的vCPU忙的時(shí)候你的虛擬定時(shí)器到期被拖后造成Guest端的定時(shí)器延遲。xvisor在這塊的短板是它的調(diào)度器比較簡(jiǎn)單虛擬定時(shí)器到期后如果目標(biāo)vCPU不在運(yùn)行中中斷注入后要等該vCPU被調(diào)度才能被處理延遲會(huì)疊加。實(shí)測(cè)中2個(gè)vCPU的Guest在高負(fù)載下定時(shí)器周期的抖動(dòng)大概在幾個(gè)毫秒量級(jí)對(duì)于非實(shí)時(shí)場(chǎng)景能接受。4.4 用QEMU的虛擬時(shí)間做快速驗(yàn)證如果手上沒(méi)有真實(shí)板卡QEMU是一個(gè)很趁手的驗(yàn)證環(huán)境。QEMU自身支持時(shí)間虛擬化相關(guān)參數(shù)比如-rtc、-icount。在調(diào)試xvisor時(shí)間虛擬化時(shí)我通常用如下命令啟動(dòng)qemu-system-aarch64 -machine virt -cpu cortex-a57 \ -kernel xvisor -dtb xvisor.dtb -m 1024 \ -nographic \ -append consolettyAMA0啟動(dòng)后進(jìn)入xvisor的shell再加載Guest鏡像。這種環(huán)境下驗(yàn)證時(shí)間虛擬化的好處是QEMU的時(shí)鐘可控可以通過(guò)-icount控制虛擬CPU的指令執(zhí)行速度方便制造時(shí)間壓力場(chǎng)景。我曾在-icount 2大約每2條指令一個(gè)時(shí)鐘tick下跑Guest明顯看到Guest的定時(shí)器中斷被拉伸這正是驗(yàn)證dilation語(yǔ)義的好場(chǎng)景。提示QEMU環(huán)境下時(shí)間虛擬化的表現(xiàn)跟真實(shí)硬件差異很大尤其是ARM Generic Timer的虛擬化擴(kuò)展在QEMU里是模擬的性能不能代表真實(shí)板卡。它適合驗(yàn)證邏輯正確性不適合做性能評(píng)估。5. 常見(jiàn)問(wèn)題與排查實(shí)錄5.1 Guest系統(tǒng)時(shí)間漂移嚴(yán)重現(xiàn)象Guest里date顯示的時(shí)間比實(shí)際時(shí)間慢或快很多運(yùn)行越久差得越多。排查步驟先區(qū)分是墻鐘時(shí)間漂移還是單調(diào)時(shí)鐘漂移。在Guest里分別執(zhí)行date和cat /proc/uptime對(duì)比兩者的增長(zhǎng)速度。如果uptime正常而date偏慢問(wèn)題在墻鐘時(shí)間同步——NTP或者RTC虛擬化沒(méi)做好如果兩者都偏問(wèn)題更可能在虛擬計(jì)數(shù)器頻率配置不對(duì)。解決方案墻鐘問(wèn)題優(yōu)先檢查xvisor的RTC虛擬化是否工作Guest里是否有/dev/rtc。頻率問(wèn)題檢查設(shè)備樹(shù)的clock-frequency和xvisor的CONFIG_TIMER_FREQ是否對(duì)齊。我在板卡上遇到過(guò)一次典型問(wèn)題設(shè)備樹(shù)時(shí)鐘頻率寫(xiě)的是62.5MHz但xvisor內(nèi)部CONFIG_TIMER_FREQ還是默認(rèn)的50MHz導(dǎo)致Guest時(shí)間線性慢20%修改后恢復(fù)正常。5.2 vCPU卡死或中斷風(fēng)暴現(xiàn)象Guest啟動(dòng)后頻繁出現(xiàn)soft lockup/proc/interrupts里arch_timer中斷數(shù)增長(zhǎng)異??靽?yán)重時(shí)整個(gè)Guest無(wú)響應(yīng)。排查步驟先用串口進(jìn)xvisor shell查看vCPU狀態(tài)vcpu list觀察目標(biāo)vCPU是否反復(fù)進(jìn)出hypervisor。然后查host timer的實(shí)際觸發(fā)頻率確定是不是某個(gè)虛擬定時(shí)器被反復(fù)注冊(cè)成短周期。xvisor的timer命令通??梢詃ump當(dāng)前的定時(shí)器鏈表timer如果發(fā)現(xiàn)大量到期時(shí)間相等的短周期定時(shí)器基本就是Guest的定時(shí)器配置被錯(cuò)誤透?jìng)骰蛘咛摂M中斷注入后Guest沒(méi)有正確應(yīng)答導(dǎo)致中斷反復(fù)重注入。解決方案多數(shù)情況是虛擬中斷的清除語(yǔ)義沒(méi)做對(duì)。ARM Generic Timer的虛擬中斷是level-sensitive的Guest必須清除CNTV_CTL的enable位或重寫(xiě)CNTV_TVAL才能拉低中斷線。如果xvisor在注入后沒(méi)有同步更新定時(shí)器狀態(tài)Guest清了中斷但hypervisor認(rèn)為還沒(méi)清就會(huì)陷入注入-清除-再注入的循環(huán)。檢查arch/arm/time.c里的timer_event更新邏輯確保在注入虛擬中斷前已經(jīng)把該定時(shí)器標(biāo)記為“已到期并等待Guest處理”而不是“周期性重新觸發(fā)”。5.3 掛起/恢復(fù)后時(shí)間跳變現(xiàn)象Guest執(zhí)行suspend比如echo mem /sys/power/state到resume后系統(tǒng)時(shí)間要么跳到未來(lái)要么完全不動(dòng)。排查步驟這個(gè)問(wèn)題的本質(zhì)是掛起期間物理定時(shí)器停了但虛擬時(shí)間還在按物理時(shí)間累加或者反過(guò)來(lái)。xvisor時(shí)間子系統(tǒng)需要感知Guest的電源狀態(tài)變化。解決方案在xvisor里增加一個(gè)電源狀態(tài)遷移時(shí)的“時(shí)間凍結(jié)”處理Guest進(jìn)入suspend時(shí)記錄當(dāng)前虛擬時(shí)間快照Guest resume時(shí)重新校準(zhǔn)vcpu_time context的offset使Guest看到的單調(diào)時(shí)間在掛起期間保持不變或按預(yù)期補(bǔ)償。這部分的實(shí)現(xiàn)復(fù)雜度取決于虛擬平臺(tái)怎么建模電源管理。我的建議是優(yōu)先保證offset補(bǔ)償也就是suspend期間不走虛擬時(shí)間把這次掛起產(chǎn)生的物理時(shí)間差不做累加。5.4 和KVM、Xen的時(shí)間虛擬化對(duì)比聊完xvisor的排查實(shí)錄再橫向?qū)Ρ纫幌缕渌桨改芨宄visor的定位。對(duì)比維度xvisorKVMLinux內(nèi)核Xen時(shí)鐘源虛擬化方式線性映射offsetdilationkvm-clock半虛擬化 TSC/ACPI虛擬化Xen共享信息頁(yè) hypercall定時(shí)器中斷注入虛擬中斷直接注入vCPU通過(guò)KVM的irqchip vcpu定時(shí)器事件通道注入精度上限依賴host tick頻率約1ms量級(jí)可支持ns級(jí)精度kvm-clock亞毫秒級(jí)復(fù)雜度低便于閱讀和二次開(kāi)發(fā)高功能全面但代碼龐大高分域模型復(fù)雜典型場(chǎng)景嵌入式、資源受限、需要高可控性的場(chǎng)景服務(wù)器、云主機(jī)、桌面虛擬化服務(wù)器虛擬化、云基礎(chǔ)設(shè)施這個(gè)對(duì)比不是要分高下而是說(shuō)明xvisor在時(shí)間虛擬化上的設(shè)計(jì)目標(biāo)是“夠用、可控、可移植”。如果你需要納秒級(jí)精度的時(shí)間虛擬化xvisor確實(shí)不是首選但你要是想在嵌入式場(chǎng)景里搞清楚時(shí)間虛擬化是怎么回事xvisor的代碼是很好的入門(mén)教材。5.5 那些“嵌套虛擬化”報(bào)錯(cuò)其實(shí)也和時(shí)間相關(guān)熱搜詞里有一堆“vmware嵌套虛擬化失敗”“此平臺(tái)不支持虛擬化AMD-V”“模塊hv啟動(dòng)失敗”之類的報(bào)錯(cuò)很多人以為是CPU特性問(wèn)題其實(shí)有一部分和時(shí)間虛擬化脫不了干系。舉個(gè)例子VMware Workstation啟用了嵌套虛擬化后如果虛擬機(jī)里的系統(tǒng)讀到的TSC頻率跟宿主機(jī)的實(shí)際TSC頻率不一致高精度定時(shí)器就會(huì)錯(cuò)亂輕則報(bào)錯(cuò)重則直接無(wú)法啟動(dòng)。另一類“HV啟動(dòng)失敗”的場(chǎng)景里時(shí)間同步驅(qū)動(dòng)VMware Tools的time sync和hypervisor的時(shí)鐘源沖突也是常見(jiàn)原因。這類報(bào)錯(cuò)給的提示往往只有一句話但排查思路是類似的先確認(rèn)宿主機(jī)的虛擬化特性是否完整egrep -c (vmx|svm) /proc/cpuinfo再確認(rèn)虛擬機(jī)內(nèi)部的時(shí)間源、時(shí)鐘源信息是否正常最后檢查hypervisor的時(shí)間同步機(jī)制是否被安全軟件或系統(tǒng)策略關(guān)閉了。這些經(jīng)驗(yàn)跟xvisor的時(shí)間虛擬化本質(zhì)上是相通的——時(shí)間源沖突、頻率不一致、中斷注入失敗是虛擬化領(lǐng)域共通的坑。6. 調(diào)試xvisor時(shí)間子系統(tǒng)的幾個(gè)技巧6.1 用日志定位時(shí)間問(wèn)題xvisor自帶日志系統(tǒng)可以通過(guò)配置打開(kāi)精細(xì)日志。排查時(shí)間虛擬化問(wèn)題時(shí)我習(xí)慣先把這幾個(gè)日志開(kāi)關(guān)打開(kāi)# xvisor啟動(dòng)參數(shù)或config里開(kāi)啟 log-level debug重點(diǎn)關(guān)注兩類日志一類是定時(shí)器中斷處理路徑的日志能看到物理tick何時(shí)到、虛擬定時(shí)器何時(shí)到期另一類是虛擬中斷注入日志——每次給vCPU注入timer中斷時(shí)的目標(biāo)vCPU和當(dāng)前物理CPU。實(shí)際調(diào)試中我發(fā)現(xiàn)最有效的方法是在vcpu_time context的每次更新處加一條臨時(shí)打印記錄物理時(shí)間、虛擬時(shí)間、offset三個(gè)值。把一組數(shù)據(jù)抓下來(lái)數(shù)據(jù)之間的線性關(guān)系一目了然偏移量是常量還是變化量、速率是否偏差這些都能直接算出來(lái)。等到問(wèn)題確認(rèn)后再把打印關(guān)掉。6.2 構(gòu)造最小復(fù)現(xiàn)場(chǎng)景時(shí)間虛擬化的問(wèn)題往往要跑很久才暴露復(fù)現(xiàn)困難。我的經(jīng)驗(yàn)是把場(chǎng)景“時(shí)間壓縮”把虛擬定時(shí)器的周期調(diào)到極短把host tick調(diào)大人為放大時(shí)間偏差。比如正常場(chǎng)景虛擬定時(shí)器是10ms周期調(diào)試時(shí)改成1ms連續(xù)跑10秒相當(dāng)于把問(wèn)題放大了10倍。另外同時(shí)跑兩個(gè)周期相差很遠(yuǎn)的Guest也是好辦法。一個(gè)Guest跑快速tick另一個(gè)Guest跑慢速tick兩者在同一個(gè)host timer上競(jìng)爭(zhēng)很容易暴露出調(diào)度器和時(shí)間管理器之間的優(yōu)先級(jí)問(wèn)題。6.3 如果我要二次開(kāi)發(fā)從哪入手如果你讀完這篇文章想在xvisor基礎(chǔ)上改時(shí)間虛擬化我建議按這個(gè)順序入手第一步先熟悉vcpu_time context的維護(hù)邏輯動(dòng)手改offset和dilation的換算跑Guest觀察時(shí)間變化。這個(gè)改動(dòng)最小、驗(yàn)證最快能建立直觀感覺(jué)。第二步改虛擬定時(shí)器的到期處理比如把單次觸發(fā)改成周期觸發(fā)或者加入“定時(shí)器追趕”邏輯。這一步能讓你理解物理tick到虛擬中斷的完整路徑。第三步再動(dòng)中斷注入的細(xì)節(jié)比如調(diào)整虛擬中斷的優(yōu)先級(jí)、目標(biāo)vCPU選擇策略。這一步最容易引入回歸務(wù)必配合第4節(jié)的驗(yàn)證三板斧做充分測(cè)試。總的來(lái)說(shuō)xvisor的時(shí)間虛擬化雖然精簡(jiǎn)但五臟俱全。它的設(shè)計(jì)沒(méi)有太多花活卻把時(shí)間虛擬化的核心問(wèn)題——時(shí)間線管理、硬件定時(shí)器抽象、虛擬中斷注入——都表達(dá)清楚了。讀它的代碼比讀大型hypervisor里動(dòng)輒上萬(wàn)行的時(shí)鐘框架要友好得多。如果你打算深入研究虛擬化從xvisor的時(shí)間子系統(tǒng)入手再對(duì)照KVM的實(shí)現(xiàn)去看會(huì)有事半功倍的效果。