)
ARMv8/v9 Generic Timer虛擬化架構(gòu)拆解做虛擬化平臺的老哥們應該都有同感時間不對一切白給。無論是虛擬機的時鐘漂移、線程調(diào)度延遲還是容器網(wǎng)絡的超時重傳底層都是定時器在撐著。而ARM平臺上這個定時器地基就是Generic Timer。今天這篇不聊泛泛的架構(gòu)概念直接進到ARMv8/v9 Generic Timer在虛擬化場景下的內(nèi)部機制從硬件視圖、軟件分層到KVM落地實現(xiàn)一整套拆開揉碎講清楚。先說清楚這文章解決什么問題如果你正在做KVM虛擬化開發(fā)、BSP適配、或者排查虛擬機時鐘異常類問題這篇文章能幫你建立起從硬件定時器到KVM虛擬中斷的完整鏈路認知。如果你只是聽說過Generic Timer想入門跟著走一遍也能搞清楚它的核心機制后面遇到問題至少知道去哪一層排查。標題里V-15和A-40是我們內(nèi)部項目對虛擬化和架構(gòu)兩塊內(nèi)容的編號不必糾結(jié)具體含義下面直接進正題。1. 定時器虛擬化的三個核心難題1.1 時間在虛擬化世界里為什么這么難虛擬化最容易被低估的復雜度就是時間。CPU、內(nèi)存、設備都可以通過硬件虛擬化擴展來加速唯獨時間這個東西它的設備是每顆CPU核心自帶的而且它的讀取頻率極高——操作系統(tǒng)的時鐘節(jié)拍、調(diào)度器的tick、協(xié)議棧的超時計算全都在高頻讀時間。想象一下你有一個物理鬧鐘現(xiàn)在要把它分給10個人用每個人都想在自己的時間線上設置鬧鈴、讀取當前時間而且互相不能干擾。更麻煩的是每個人虛擬機還希望自己可以隨意撥快撥慢手表修改系統(tǒng)時間但不能影響別人。這就是定時器虛擬化的本質(zhì)難題。具體拆解成三個問題來看時間來源一致性所有虛擬機看到的當前時間必須有一個統(tǒng)一的基準不能每顆CPU各說各話否則遷移和多核場景直接崩。虛擬時間隔離每臺虛擬機需要自己的時間線這個時間線的起點可以不同比如虛擬機剛啟動時時間是2010年宿主機已經(jīng)是2024年而且虛擬機修改自己的時間不能影響宿主機和其他虛擬機。定時中斷路由每臺虛擬機要能設置自己的鬧鐘時間到了要正確觸發(fā)對應的虛擬中斷并且要精確到微秒級別不能靠軟件輪詢。這三個問題ARM Generic Timer的虛擬化架構(gòu)就是專門為它們設計的。1.2 ARM沒有給定時器單獨開一條虛擬化捷徑這里要說一個容易誤解的點。ARM在虛擬化上做了很多硬件加速比如GIC (Generic Interrupt Controller) 對虛擬中斷的直接注入MMU有Stage-2地址轉(zhuǎn)換。但定時器這個模塊ARM并沒有提供一個虛擬定時器設備讓你直接操作。它采用的方式是提供一組精心設計的硬件機制讓Hypervisor用trap-and-emulate的方式來實現(xiàn)虛擬定時器但是把這個過程的開銷壓縮到極低。為什么不像PCIe直通那樣直接把定時器直通給虛擬機因為定時器不是獨立設備它是CPU核心的一部分而且多個虛擬機共享同一顆物理CPU。直通等于讓一個虛擬機霸占硬件資源其他虛擬機就沒法用定時器了。所以說ARM Generic Timer的虛擬化架構(gòu)本質(zhì)上是一個硬件輔助的軟件虛擬化方案。理解這一點后面看KVM的代碼邏輯就會順很多。2. Generic Timer硬件機制全景拆解2.1 系統(tǒng)計數(shù)器整個時間世界的基準ARM Generic Timer的頂層是一套系統(tǒng)級的計數(shù)器叫System Counter。這個計數(shù)器是唯一的、全局的、單調(diào)遞增的整個SoC上所有核心看到的值都是一樣的。它通常由主板上的晶振驅(qū)動頻率一般在1MHz到50MHz之間。System Counter在軟件側(cè)體現(xiàn)為兩個寄存器視圖CNTPCT物理計數(shù)器和CNTVCT虛擬計數(shù)器。這兩個寄存器都是64位的單位是tick。但是注意ARM的spec規(guī)定軟件不能直接讀System Counter本身必須通過每個核心上的CNTPCT/CNTVCT寄存器來讀。為什么要區(qū)分物理和虛擬兩套計數(shù)器視圖答案很簡單物理計數(shù)器給宿主機用虛擬計數(shù)器給虛擬機用。虛擬計數(shù)器 物理計數(shù)器 - Offset這個Offset由Hypervisor設置。這就是前面說的虛擬時間線的實現(xiàn)基礎。2.2 每個核心上的定時器組件結(jié)構(gòu)每個ARM核心上有兩組重要的定時器組件一組是給非安全世界的一組是給安全世界的。我們做虛擬化主要關注非安全世界也就是EL1和EL2這兩層下面的結(jié)構(gòu)以這套為主。每個核心上有四種定時器物理定時器 (Physical Timer)通常用于EL1/EL0非安全世界中斷號是PPI 13。虛擬定時器 (Virtual Timer)通常用于給虛擬機提供定時中斷中斷號是PPI 14。EL2物理定時器 (EL2 Physical Timer)給Hypervisor自己用的定時器中斷是PPI 26。安全物理定時器 (Secure Physical Timer)給TrustZone安全世界用的虛擬化場景一般用不到。每一種定時器都有自己的一組寄存器CompareValue比較值、Control控制、Status狀態(tài)。定時器的原理很簡單當前計數(shù)值 CompareValue 的時候觸發(fā)一次中斷。2.3 關鍵寄存器與作用域劃分把寄存器按訪問權(quán)限和虛擬化角色拆開看落實到代碼和調(diào)試上會更有操作性寄存器訪問層級虛擬化角色CNTPCT_EL0EL0/EL1可讀物理計數(shù)器Hypervisor讀物理時間用CNTVCT_EL0EL0/EL1可讀虛擬計數(shù)器虛擬機讀當前時間CNTVOFF_EL2僅EL2可寫虛擬計數(shù)器偏移量虛擬機時間線原點CNTP_TVAL_EL0EL0/EL1可寫物理定時器的遞減計數(shù)值CNTP_CTL_EL0EL0/EL1可寫物理定時器控制使能、屏蔽、狀態(tài)CNTP_CVAL_EL0EL0/EL1可寫物理定時器比較值CNTV_TVAL_EL0EL0/EL1可寫虛擬定時器的遞減計數(shù)值CNTV_CTL_EL0EL0/EL1可寫虛擬定時器控制CNTV_CVAL_EL0EL0/EL1可寫虛擬定時器比較值CNTHP_TVAL_EL2僅EL2EL2物理定時器CNTHP_CTL_EL2僅EL2EL2物理定時器控制CNTHP_CVAL_EL2僅EL2EL2物理定時器比較值CNTHCTL_EL2僅EL2控制EL0對計數(shù)器和定時器的訪問權(quán)限CNTKCTL_EL1EL1可寫控制EL0對內(nèi)核定時器寄存器的訪問這張表建議存一下排問題的時候會反復用到。特別是CNTVOFF_EL2和CNTHCTL_EL2這兩個是整個虛擬化的樞紐。2.4 計數(shù)器讀路徑為什么虛擬機讀時間也要被攔截這里有一個很多初學者沒注意到的設計點虛擬機執(zhí)行CNTVCT_EL0讀取時間在KVM的默認配置下是不需要trap到EL2的。ARM通過硬件機制讓CNTVCT_EL0直接讀出來一個已經(jīng)減去了CNTVOFF_EL2的值整個過程發(fā)生在硬件層面虛擬機根本不知道偏移的存在。但是有一個例外當虛擬機運行在32位模式而Hypervisor需要讀取CNTPCT的時候情況就變得復雜了。32位guest訪問CNTPCT會拆成兩次32位load高32位和低32位這中間可能發(fā)生高低位不一致的問題。KVM內(nèi)核對這個問題有專門的patch處理路徑后面在常見問題部分會講到。3. KVM定時器虛擬化架構(gòu)設計3.1 分層模型Host Timer和Guest TimerKVM在arch/arm64/kvm/arch_timer.c中實現(xiàn)了完整的定時器虛擬化邏輯。整套設計圍繞兩條時間線展開Host時間線基于CNTPCT物理計數(shù)器KVM用它來調(diào)度vCPU的執(zhí)行、計算虛擬機的運行時間。Guest時間線基于CNTVCT虛擬計數(shù)器虛擬機里的操作系統(tǒng)看到的時間。兩條時間線之間的橋梁就是CNTVOFF_EL2。KVM在加載vCPU到物理CPU上的時候?qū)懭脒@個寄存器vCPU切走的時候不需要恢復因為下個vCPU加載時會重新寫這個操作在關鍵路徑上只有一次MSR寫開銷極低。定時器的虛擬化用到的核心結(jié)構(gòu)體在KVM中分成兩層struct arch_timer_cpu { struct arch_timer_context timers[NR_KTIMERS]; struct hrtimer hrtimer; bool is_timer_running; }; struct arch_timer_context { struct kvm_vcpu *vcpu; enum kvm_arch_timers timer; u64 cnt_cval; u64 cnt_ctl; bool loaded; bool ready; };第一層是per-CPU結(jié)構(gòu)的每個vCPU一組第二層是具體的物理定時器和虛擬定時器各自獨立的上下文。從結(jié)構(gòu)體上就能看出設計意圖物理和虛擬兩個定時器分別模擬各自維護比較值和控制位kvm根據(jù)guest的配置決定用哪個。3.2 Virtual Timer和Physical Timer的分工策略這是KVM定時器虛擬化設計里最精彩的部分。為什么要有兩套定時器給guest用表面上看有虛擬定時器就夠了guest設鬧鐘就用CNTV_CVAL_EL0時間到了觸發(fā)PPI 14中斷不就行了嗎真實原因是某些guest操作系統(tǒng)會直接操作物理定時器而不是虛擬定時器。比如Linux內(nèi)核早期的arch timer驅(qū)動它在某些配置下直接使用物理timer。還有32位的ARM guest內(nèi)核某些版本的代碼路徑上會讀取CNTPCT來校準時間。如果guest運行在EL1虛擬機里它訪問CNTP_TVAL_EL0和CNTP_CTL_EL0是會直接透過到硬件物理定時器的在trap沒有配置的情況下它會在沒有Hypervisor掌控的情況下直接產(chǎn)生物理中斷這個中斷會直接送進guest卻沒有任何虛擬化層的管理成為一顆無法關閉的定時炸彈。所以KVM的做法是分兩層guest的物理定時器和虛擬定時器操作都必須經(jīng)過KVM的接管和控制。它最終映射到Host側(cè)的實現(xiàn)是用Host的一個真正的物理定時器來backing guest的某個定時器再通過vGIC把中斷以虛擬中斷的形式注入回guest。具體到實現(xiàn)上KVM的策略是虛擬定時器用虛擬計數(shù)器CNTVCT作為時間基準backing hrtimer基于host的CLOCK_MONOTONIC實際讀取cntpct換算中斷走PPI 14通過vgic注入。物理定時器用物理計數(shù)器CNTPCT作為時間基準backing hrtimer同樣基于host時間中斷走PPI 13通過vgic注入。兩個timer的backing hrtimer在host上共用同一個hrtimer實體arch_timer_cpu-hrtimer通過hrtimer_start重新指定到期時間來實現(xiàn)兩個timer的切換和共享。3.3 中斷的虛擬化路徑從硬件中斷到Guest IRQ定時器中斷是整個虛擬化鏈路里最長的路徑之一值得完整走一遍Host物理定時器超時觸發(fā)host的timer中斷handler。KVM的timer handlerkvm_timer_irq_handler被調(diào)用。這個handler識別中斷來源是backing timer到期。KVM檢查對應的arch_timer_context確認是否應該向guest注入中斷。如果要注入調(diào)用kvm_timer_update_irq通過irqchipvgic設置對應的虛擬中斷為pending狀態(tài)。vCPU在下一次進入guest模式時vgic會把pending中斷通過list register注入guest在EL1收到中斷。這里面有一個容易被忽略的關鍵細節(jié)KVM在判斷是否應該向guest注入中斷的時候不是簡單看hrtimer到期就注入而是要對比當前的虛擬計數(shù)器值和guest設置的定時器比較值。因為hrtimer的精度和實際timer的精度存在微小偏差KVM需要做一次校準static bool kvm_timer_irq_can_fire(struct arch_timer_context *timer_ctx) { u64 cnt; u64 cval, ctl; cval timer_ctx-cnt_cval; ctl timer_ctx-cnt_ctl; if (kvm_timer_should_fire(timer_ctx)) return false; cnt kvm_phys_timer_read(); if (cnt cval) return false; return (ctl ARCH_TIMER_CTRL_ENABLE) !(ctl ARCH_TIMER_CTRL_IT_MASK); }注意最后兩個條件ENABLE位和IT_MASK位。這說明KVM嚴格遵循ARM定時器的硬件語義即使hrtimer到點了如果guest屏蔽了中斷或者禁用了定時器也不能硬注入。3.4 KVM如何處理Guest屏蔽定時器中斷這個場景在真實運行中經(jīng)常出現(xiàn)而且坑特別多。Guest的Linux內(nèi)核在處理定時器中斷時會先屏蔽中斷設置IMASK位清掉pending狀態(tài)處理完再重新使能。如果KVM在guest屏蔽中斷期間直接把hrtimer停了guest重新使能時會發(fā)現(xiàn)定時器已經(jīng)沒有在走時間直接卡住。KVM的處理方式是即使guest屏蔽了中斷hrtimer也要繼續(xù)跑因為hrtimer的到期只是一個信號真正的是否注入中斷還要看guest的屏蔽狀態(tài)。而且KVM在hrtimer到期時如果發(fā)現(xiàn)guest屏蔽了中斷會通過編程硬件定時器的比較值讓它在下一個預期的到期點再次觸發(fā)形成輪詢等待的效果。這個繼續(xù)跑、不注入的策略保證了guest重新使能定時器后馬上就能收到一個pending的中斷時間線不會斷。但是代價是host側(cè)會多次觸發(fā)定時器中斷增加一定的CPU開銷。這塊在性能敏感場景值得做profile看看是不是有大量的spurious wakeup。4. 定時器的生命周期管理與vCPU調(diào)度聯(lián)動4.1 vCPU加載與卸載時定時器狀態(tài)保存KVM在vCPU切入和切出時對定時器有明確的狀態(tài)管理。切出的場景是vCPU被搶占、運行時間片到期或者需要退出到用戶空間處理IO。切入的時候KVM做這么幾件事void kvm_timer_vcpu_load(struct kvm_vcpu *vcpu) { struct arch_timer_cpu *timer vcpu_to_timer(vcpu); struct arch_timer_context *vtimer vcpu_vtimer(vcpu); struct arch_timer_context *ptimer vcpu_ptimer(vcpu); kvm_timer_update_state(vcpu); if (timer-is_timer_running) return; timer-is_timer_running true; /* Set the offset for the virtual timer */ timer_set_voffset(vcpu-kvm, vcpu-arch.timer_irq.offset); kvm_timer_vcpu_load_nogic(vcpu); kvm_timer_unblock(vcpu); }關鍵在于寫入CNTVOFF_EL2把這條vCPU的虛擬時間線切到自己的坐標。更新vtimer和ptimer的硬件寄存器讓guest看到的定時器狀態(tài)連續(xù)。如果backing hrtimer已經(jīng)啟動過切回來時不需要重新啟動hrtimer它一直在跑只是到期事件可能因為vCPU不在而錯過了。這里有一個補課機制后面講。切出的時候邏輯對稱但要額外注意void kvm_timer_vcpu_put(struct kvm_vcpu *vcpu) { struct arch_timer_cpu *timer vcpu_to_timer(vcpu); struct arch_timer_context *vtimer vcpu_vtimer(vcpu); /* If the timer has expired, inject the interrupt */ if (kvm_timer_should_fire(vtimer)) kvm_timer_update_irq(vcpu, true, vtimer); if (!timer-is_timer_running) return; timer-is_timer_running false; timer_save_state(vcpu); timer-hrtimer.cancel(cancel_phys_timer); }這里有一個設計亮點即使vCPU被切出hrtimer也不是被cancel掉取消而是保留。如果guest設置的定時器到期時間還沒到hrtimer留在host的timer wheel里到期時照樣觸發(fā)如果guest設置的到期時間已經(jīng)過了那么在切出的時候KVM會立刻把虛擬中斷的pending狀態(tài)置上等vCPU下一次進入時直接處理。這就是為什么虛擬機的定時器即使在高負載宿主上也不會出現(xiàn)明顯漂移——因為KVM用的是host的硬件定時器來保證到期精度而不是依賴vCPU的調(diào)度。4.2 補課機制Guest錯過定時器中斷怎么辦展開講一下上面提到的補課機制。場景是這樣的guest的定時器在10ms后到期但是host負載很高vCPU在8ms的時候被切出了物理CPU直到20ms后才重新被調(diào)度回來。如果沒有補課機制這10ms的定時器事件就丟了guest醒來后發(fā)現(xiàn)時間已經(jīng)過去了20ms但它的定時器中斷一個都沒收到所有依賴定時器的邏輯全都錯亂。KVM的補課邏輯在kvm_timer_vcpu_load里static void kvm_timer_update_state(struct kvm_vcpu *vcpu) { struct arch_timer_cpu *timer vcpu_to_timer(vcpu); struct arch_timer_context *vtimer vcpu_vtimer(vcpu); struct arch_timer_context *ptimer vcpu_ptimer(vcpu); if (kvm_timer_should_fire(vtimer) ! vtimer-irq.level) kvm_timer_update_irq(vcpu, !vtimer-irq.level, vtimer); if (kvm_timer_should_fire(ptimer) ! ptimer-irq.level) kvm_timer_update_irq(vcpu, !ptimer-irq.level, ptimer); timer-hrtimer_active false; }kvm_timer_should_fire會做一次當前虛擬計數(shù)器和比較值的大小判斷。如果vCPU回來時發(fā)現(xiàn)當前時間已經(jīng)超過了guest設置的值就把中斷狀態(tài)更新為pending。因為這段時間guest的定時器中斷本質(zhì)上一直在pendingKVM通過這此判斷來模擬這個狀態(tài)。這個機制背后反映的設計哲學是定時器中斷本質(zhì)上是一個事件攜帶的機制只要最終狀態(tài)正確中間的丟失可以用狀態(tài)同步來彌補不需要逐個事件追溯。這與中斷控制器虛擬化中l(wèi)evel trigger的處理思路一脈相承。4.3 32位Guest的特殊處理與CNTPCT陷阱談32位guest之前先解釋一下為什么會有trap問題。當虛擬機運行在32位模式AArch32ARM架構(gòu)規(guī)定訪問某些64位寄存器的行為會primary變化。比如32位guest訪問CNTPCT_EL0會被trap到EL2。為什么因為32位guest無法一次完成64位load只能拆成兩次32位load兩次之間的高32位值可能已經(jīng)變了。如果不做trap處理guest會讀到撕裂的時間值。KVM的處理是接受這個trap然后模擬讀取用一次原子的64位讀拿到CNTPCT再拆成高32位和低32位返回給guest。所以這里多出來的開銷是必然的不是KVM實現(xiàn)的問題。但這里有一個嚴重的性能隱患如果guest的Linux內(nèi)核在時鐘校準里頻繁讀CNTPCT每次都會有trap整個系統(tǒng)時間校準路徑會變得特別慢。實際的KVM在arch/arm64/kvm/hyp/hyp-entry.S中有對應的handlertableel0_32_pc: ... el0_32_cntpct: ...它做了這樣的處理在hyp階段直接讀出CNTPCT拆成兩個32位值設置好返回寄存器后直接eret回guest全程不退出到host kernel。這個路徑把多次trap的開銷降到了單次trap一次內(nèi)存讀算是在架構(gòu)限制下的最優(yōu)解。我記得還有一處是CNTVCT的訪問在ARMv8.0的某個勘誤表里有提到32位guest讀CNTVCT_EL0也可能產(chǎn)生詭異的撕裂值個別內(nèi)核版本會在head.S里做一次重讀校準這里就不再展開了。5. KVM定時器實驗復現(xiàn)搭建一個最小觀測環(huán)境5.1 環(huán)境準備與內(nèi)核配置要點紙上談兵沒用。我自己的調(diào)試環(huán)境是QEMU KVM跑ARM64 guesthost是樹莓派的Ubuntu Server內(nèi)核5.15和另外一臺鯤鵬920服務器兩邊邏輯一致只是性能差很多。要復現(xiàn)本文講的定時器虛擬化行為需要確認以下配置項CONFIG_KVMyCONFIG_KVM_ARM_HOSTy這個不用多說。CONFIG_ARM_ARCH_TIMERyARM的arch timer驅(qū)動。CONFIG_ARM_GIC_V3y中斷控制器的v3版本。CONFIG_HZ_250或者CONFIG_HZ_1000guest里配置的時鐘頻率會影響定時器中斷頻率方便觀察差異。調(diào)試輸出方面建議打開tracefs的timer相關事件mount -t tracefs tracefs /sys/kernel/tracing echo 1 /sys/kernel/tracing/events/kvm/kvm_timer_update_irq/enable echo 1 /sys/kernel/tracing/events/kvm/kvm_hrtimer_start/enable echo 1 /sys/kernel/tracing/events/kvm/kvm_hrtimer_cancel/enable cat /sys/kernel/tracing/trace_pipe這三個trace event能完整還原KVM定時器的生命周期全貌從hrtimer啟動到中斷注入全鏈路可見。實測里面kvm_timer_update_irq的觸發(fā)頻率和guest的HZ配置強相關可以直觀看到虛擬定時器的tick節(jié)奏。5.2 一個觀測腳本確認CNTVOFF在不同VM間隔離要驗證CNTVOFF_EL2確實起到了虛擬機時間隔離的作用可以這樣操作在宿主機寫一個內(nèi)核模塊讀取CNTVCT_EL0和CNTPCT_EL0的差值不過這需要root權(quán)限和內(nèi)核模塊不是每個環(huán)境都方便更輕量的方式是跑一個只讀虛擬計數(shù)器的小程序#include stdint.h #include stdio.h #include time.h static inline uint64_t cntvct_read(void) { uint64_t val; asm volatile(mrs %0, cntvct_el0 : r (val)); return val; } static inline uint64_t cntpct_read(void) { uint64_t val; asm volatile(mrs %0, cntpct_el0 : r (val)); return val; } int main(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, ts); uint64_t v cntvct_read(); uint64_t p cntpct_read(); printf(CNTVCT%lu CNTPCT%lu diff%ld tick\n, v, p, p - v); return 0; }在宿主機上編譯運行diff會是一個固定值0或者某個常數(shù)取決于kernel啟動時是否設置了CNTVOFF為0在虛擬機里編譯運行同一個程序diff就是KVM設置的CNTVOFF_EL2值。兩個VM看到的diff不同這就能非常直觀地驗證虛擬時間線的隔離。實測中diff值通常是一個非常大的隨機偏移這就是KVM為每個VM分配的獨立時間原點。5.3 用ftrace驗證hrtimer與注入的因果關系更深入地驗證中斷注入路徑可以同時打開kvm和timer的trace事件echo 1 /sys/kernel/tracing/events/kvm/enable echo 1 /sys/kernel/tracing/events/timer/enable cat /sys/kernel/tracing/trace在guest里跑一個高頻率的定時器程序比如nanosleep 1msguest產(chǎn)生的定時器中斷會導致host側(cè)對應的backing hrtimer反復到期。trace里可以看到hrtimer_expire_entry到kvm_timer_update_irq之間通常只有幾微秒的延遲這說明中斷注入路徑是硬實時鏈路沒有經(jīng)過workqueue之類的延遲機制。如果發(fā)現(xiàn)hrtimer到期到中斷注入之間出現(xiàn)了超過100微秒的間隔那就要懷疑是不是host上有其他高優(yōu)先級中斷搶占了CPU或者vCPU沒有在物理CPU上運行導致hrtimer回調(diào)無法及時執(zhí)行。這個時候要改成per-cpu的hrtimerHRTIMER_MODE_ABS_HARD來保證硬中斷上下文中處理后面調(diào)優(yōu)部分會講怎么改。6. 高頻問題排查與性能調(diào)優(yōu)實錄6.1 虛擬機時鐘漂移最快排查清單虛擬機上最常被人吐槽的問題就是時鐘漂移本來以為是網(wǎng)絡問題查半天發(fā)現(xiàn)是時間不對導致TCP時間戳錯亂。遇到時鐘漂移按以下順序排查現(xiàn)象可能原因排查方法guest時間比host慢CNTVOFF配置異常host讀CNTVCT與guest讀CNTVCT對比guest時間跳變遷移后CNTVOFF未正確恢復檢查migration代碼路徑確認寫入CNTVOFF_EL2guest時間完全不動CNTFRQ未設置或異常dmesg看arch_timer驅(qū)動初始化日志定時中斷抖動大hrtimer模式不是HARD查/sys/kernel/debug/tracing看hrtimer回調(diào)上下文32位guest時間異常CNTPCT trap路徑問題strace guest內(nèi)讀時間函數(shù)看是否有頻繁trap最坑的一次經(jīng)歷是某個guest內(nèi)核版本里CNTKCTL_EL1的配置有bug導致guest無法在EL0訪問CNTVCT_EL0結(jié)果guest的vDSO里的時鐘讀取全部走了系統(tǒng)調(diào)用性能掉了一個量級。這種問題靠監(jiān)測是看不出來的要學會用perf trace去看guest內(nèi)時間系統(tǒng)調(diào)用的頻率是否異常。6.2 中斷風暴問題hrtimer空轉(zhuǎn)與RESCHED經(jīng)常有老哥反饋宿主機負載不高但CPU0的中斷占用率很高irqtop里看arch_timer中斷每秒觸發(fā)幾萬次。大概率是guest里的定時器配置了過高的頻率比如1000Hz而且guest處于idle狀態(tài)不斷進入WFI。KVM對guest的WFI處理會直接退出到host讓vCPU線程睡在hrtimer上等到下次虛擬定時器到期再喚醒。如果guest設置了非常短的定時器間隔vCPU就會不停地醒來再睡每次醒來都要走一遍完整的vCPU加載流程開銷非常大。調(diào)優(yōu)方向有兩個在guest里調(diào)低HZ從1000降到250能減少3/4的中斷觸發(fā)頻率絕大多數(shù)服務器場景完全夠用。在host側(cè)確認hrtimer的HRTIMER_MODE_ABS_HARD配置是否正確。KVM默認對timer hrtimer用的是hard模式在硬中斷上下文執(zhí)行hrtimer回調(diào)如果降級成了soft模式回調(diào)會進softirq延遲會明顯增加。同時要檢查KVM是否開啟了irqchip in-kernel模式。如果使用userspace irqchip比如老的kvmtool或者某些嵌入式場景虛擬中斷要通過eventfd通知到用戶空間再寫回每次注入都是兩次ioctl的開銷性能會差幾十倍。6.3 遷移場景下的定時器連續(xù)性保障vCPU熱遷移時定時器狀態(tài)遷移是一塊特別容易出bug的地方。KVM的遷移相關代碼在arch/arm64/kvm/arch_timer.c里有如下關鍵函數(shù)int kvm_timer_get_state(struct kvm_vcpu *vcpu) int kvm_timer_set_state(struct kvm_vcpu *vcpu)這兩個函數(shù)負責把當前vCPU的定時器狀態(tài)比較值、控制位保存到kvm_timer_context里然后在目標機上恢復。正常情況下遷移流程是源端暫停vCPU。kvm_timer_get_state保存vtimer和ptimer的cnt_cval、cnt_ctl。用戶空間QEMU/kvmtool把這些狀態(tài)傳給目標端。目標端kvm_timer_set_state恢復這些狀態(tài)。目標端vCPU啟動后在kvm_timer_vcpu_load時把cval寫入硬件寄存器。這里的坑在于目標機的CNTVCT值和源機的CNTVCT值不一樣因為兩臺機器的CNTVOFF_EL2不同?;謴蜖顟B(tài)時必須把源機的cval換算成目標機的cval換算公式是new_cval old_cval (new_vo - old_vo)。KVM在kvm_timer_set_state里會調(diào)用timer_set_voffset重新設置新VM的偏移然后根據(jù)偏移差調(diào)整cval。如果這一步做錯了guest遷移后定時器要么立即觸發(fā)一次虛假中斷要么延后好久才觸發(fā)。調(diào)試這個問題的辦法是在遷移前后各打印一下kvm_timer_get_state和kvm_timer_set_state出的cval和offsetecho 1 /sys/kernel/tracing/events/kvm/kvm_timer_get_state/enable echo 1 /sys/kernel/tracing/events/kvm/kvm_timer_set_state/enable對比兩份日志里的cnt_cval差值應該和兩邊的CNTVOFF差值完全一致如果不一致就是換算邏輯出了問題。6.4 從KVM向pKVM/槍口式虛擬化演進的影響ARMv9開始力推的CCAConfidential Compute Architecture和pKVMprotected KVM對定時器虛擬化的架構(gòu)影響值得提前關注。pKVM里Hypervisor本身被拆成了兩個世界root world高特權(quán)控制所有硬件和protected world受限的VMM運行環(huán)境。這意味著原來運行在EL2的KVM代碼被一分為二其中一部分降級到EL1。定時器虛擬化里最重要的CNTVOFF_EL2寫入操作在pKVM下由root world完成protected world的VMM不能再直接操作這個寄存器。整個中斷注入路徑也變了定時器中斷需要經(jīng)過root world的中轉(zhuǎn)才能到達protected world的VMM再到guest。這對系統(tǒng)軟件開發(fā)者意味著什么以前在KVM里直接操作timer寄存器做優(yōu)化的代碼路徑需要重新審視。比如原來用戶態(tài)通過KVM_CAP_ARM_TIMER控制定時器行為的方式在pKVM里可能不再可用因為那個狀態(tài)不在protected world的掌控范圍內(nèi)。好在ARM在硬件層面考慮了這一點EL2物理定時器PPI 26就是給hypervisor自己用的它不參與guest的定時器虛擬化所以hypervisor可以在不干擾guest虛擬定時器的情況下做自己的調(diào)度計時。pKVM也延續(xù)了這套設計用EL2物理定時器做自己的時間基準不和guest的時間線混在一起。7. 寫在最后的個人經(jīng)驗定時器虛擬化這塊看起來只是虛擬化體系里的一小塊但它牽扯到硬件架構(gòu)、中斷子系統(tǒng)、調(diào)度器、遷移等多個方向的交叉知識。串起來理解之后再去看其他設備的虛擬化比如vgic中斷虛擬化、PMU虛擬化很多設計思路都是相通的。我個人在做這套東西的過程中的幾個體會第一別上來就看KVM源碼先把ARM ARMARM Architecture Reference Manual里Generic Timer那章過一遍。很多KVM代碼里的常量和不直觀的位操作都是直接從寄存器語義翻譯過來的。第二排查時間相關問題的時候建立時間線世界觀特別有用。時刻問自己我現(xiàn)在看的是host時間線還是guest時間線CNTVOFF在中間扮演了什么角色一旦把坐標系分清楚至少70%的問題都能定位到正確方向。第三性能調(diào)優(yōu)不要憑空想。裝好ftrace和perf實測幾組數(shù)據(jù)再動代碼。很多時候你以為瓶頸在中斷注入的路徑上實際測出來的結(jié)果可能完全相反。沒有測量就沒有優(yōu)化。最后說一個調(diào)試小技巧如果你需要快速確認一個guest當前用的是vtimer還是ptimer只需要在guest里執(zhí)行cat /proc/interrupts | grep -E arch_timer|timer看中斷號。如果你看到的是一個外設中斷號非13/14說明guest代碼里做了自定義處理這時候就要懷疑它是不是在EL1直接操作物理定時器繞過KVM的虛擬化了。這種情況在改裝過內(nèi)核的安卓環(huán)境里尤其常見值得多留個心眼。