Linux Kernel, QEMU, RISC-V, and Computer Architecture
從 VirtIO 到 vDPA:封包交給硬體之後,虛擬機如何熱遷移?
⚠️ 本文經由 AI 整理並撰寫。 假設你正在替一個虛擬化平台設計網路資料路徑。起初,讓 QEMU 處理虛擬網卡的 I/O 很直覺:裝置由軟體管理,狀態也容易掌握。但當封包率提高,Host CPU 花在描述符處理、通知與封包搬運上的成本,就會逐漸變得明顯。 把這些工作交給核心、專用的使用者空間程式,甚至網卡硬體,似乎是自然的下一步。然而,雲端平台不只需要「傳得快」,還需要在主機維護、資源調度時,把正在執行的虛擬機搬到另一台主機。當裝置可以繞過 QEMU,直接改寫 Guest 記憶體,QEMU 又該如何知道哪些資料必須重新傳送?到了交接那一刻,又由誰確認封包處理到哪裡? 這正是 VirtIO、vhost、vDPA 與 Shadow Virtqueue(SVQ)串在一起的原因:它們不只是不同的加速技術,更是在重新分配資料處理工作,以及對執行狀態的控制權。 本文以 Linux Host 上的 QEMU/KVM 與 virtio-net 為例,先從一個封包如何離開 Guest 談起,再沿著硬體卸載、髒頁追蹤與熱遷移,理解這些技術各自解決了什麼問題。具體功能與限制仍須對照實際的核心、QEMU、驅動及韌體版本。 一、先看一個封包:VirtIO 統一的是介面,不是後端 Guest 裡的應用程式送出封包後,作業系統最終會把工作交給網卡驅動。在 VirtIO 架構中,這個驅動不必理解某一家實體網卡的私有佇列格式,而是使用標準的 VirtIO 介面,透過 Virtqueue(VQ) 與裝置交換工作。 這裡容易產生第一個誤會:Virtqueue 並不是一個裝滿封包內容的環形儲存區。以 Split Virtqueue 為例,它主要包含三種結構: 結構 用途 Descriptor Table 描述 buffer 的位址、長度、存取方向與鏈結關係 Available Ring 告訴裝置有哪些描述符鏈可以處理 Used Ring 由裝置回報哪些描述符鏈已完成,以及完成長度 真正的封包內容稱為 payload,通常放在描述符指向的另一塊 Guest buffer。換句話說,Virtqueue 傳遞的是「去哪裡讀寫資料」以及「工作完成了沒有」,而不等同於資料本身。 具體而言,一個 Split descriptor 的欄位配置如下;這裡列的是欄位寬度,實際讀寫還必須遵守規範的位元組序與對齊要求: 欄位 大小 意義 addr 64 bits buffer 位址 len 32 bits buffer 長度 flags 16 bits 描述符方向、是否還有下一節,以及是否為間接描述符表 next 16 bits 描述符鏈中下一節的索引 四個欄位合計 16 bytes。VRING_DESC_F_WRITE 表示裝置可以寫入 buffer,而不是 Guest 可以寫入;TX payload 通常供裝置讀取,RX buffer 則供裝置寫入。VRING_DESC_F_NEXT 用於鏈結多個描述符,VRING_DESC_F_INDIRECT 則讓描述符指向另一張描述符表。一個封包可能跨多個 buffers,不能把「一個封包」等同於「一個 16 bytes 描述符」。 ...
<超標量處理器概覽> 第 11 章 - 真實世界的例子:Alpha 21264 處理器
11.1 - 概述 DEC 的 Alpha 21264 是 superscalar CPU 的一個典範,其為 4-way out-of-order superscalar CPU,工作頻率是 466 ~ 667 MHz,benchmark:SPECint95 - 40、SPECfp95 - 86;與同時期其他的處理器 benchmark 對比: Alpha 21264 的 pipeline 最多同時支持 80 條指令,以及 80 個 checkpoints,因此對於 branch mis-prediction、exception、interrupt,Alpha 21264 都可以快速地恢復 CPU 狀態 Alpha 21264 的分支預測器實現了基於局部歷史 (Local prediction) 和基於全局歷史 (Global prediction) 兩種預測方式,並根據程式的執行情況,動態選擇預測率最高的方法,這就像是兩種分支預測方式在進行競爭一樣 對於 load/store 指令,Alpha 21264 採用了 Speculative Memory Disambiguation 和 Load hit/miss Prediction Alpha 21264 採用 7-stage pipeline,比較短的 pipeline 使 branch mis-prediction 發生時的 mis-penalty 比較低,從而提高處理器的效率: ...
<超標量處理器概覽> 第 10 章 - 提交
10.1 - 概述 在 out-of-order superscalar CPU 中,為了保持程式執行順序的一致性,一般通常都會在 pipeline 的最後增加一個 commit stage 當指令抵達 commit stage 時,會將這條指令在 ROB 中的 entry 標記為 complete 此時只代表這個指令已經計算完成,不代表可以離開 pipeline (i.e. retire) 指令進到 commit stage 並不代表它一定是正確的,有可能這條指令處在 mis-prediction 或是 exception 的路徑上,最後需要從 pipeline 中被 flushed 掉 只有當這條指令之前的指令都已經 retire 了,且這條指令已經是 complete 的狀態,才可以 retire 並離開 pipeline 在一條指令 retire 前,它的狀態都是 speculative 的,只有當這條指令真的 retire 後,才可以將它的狀態更新到 CPU 的 architecture state 對一個 N-way 的 superscalar,因為每個 cycle 最少可以 fetch N 條指令進 pipeline 中,pipeline 至少也需要 retire N 條指令,才能確保 pipeline 不會被堵塞 10.2 - ROB 10.2.1 - 一般架構 ROB 本質上是一個 FIFO,ROB 中儲存了指令的相關訊息,例如:一條指令的類型、結果、destination register 和 exception 的類型等 ...
<超標量處理器概覽> 第 9 章 - 執行
9.1 - 概述 FU 的個數決定了每個 cycle 最大可以同時執行的指令個數,也就是 issue width Execute stage 另一個重要的部份就是 bypassing network,其負責將 FU 的運算結果送到其他需要它的地方,像是 physical register file (PRF)、其他 FU 的輸入端、store buffer 等 隨著每個 cycle 可以同時執行的指令個數的增多,bypassing network 變得越來越複雜,已經是處理器中速度提昇的一個關鍵部份 在不使用 bypassing network 的情況下,指令的 operands 值可以來自於 PRF (for non-data-capture),或是 payload RAM (for data-capture),每個 FU 都是透過 select 電路將 PRF 或是 payload RAM 串連起來的 每個 select 電路和 PRF (或是 payload RAM) 的 read ports 是一一對應的,每個 FU 和 PRF (或是 payload RAM) 的 read ports 也是一一對應的 因此,PRF 總共需要的 read ports 個數與 issue width 是直接相關的;如果對處理器追求更大的 issue width,就代表著 PRF 需要更多的 read ports,反而會限制了處理器的執行速度,現代處理器多會採用 cluster 架構來解決這個矛盾 Payload RAM 由於需要與 Issue Queue 綁定在一起,使用 distributed Issue Queue 可以減少 payload RAM read ports 的需求 9.2 - FU 的類型 9.2.1 - ALU ALU (Arithmetic and Logic Unit) 通常負責:整數加減法、shift、簡單的乘除法、資料搬移 (e.g. mov、byte-swap 指令)、分支指令目標位址的計算、記憶體存取位址的計算 很多處理器在 ALU 中也實現了比較簡單的乘法功能,用來支持乘法指令 不過為了追求比較高的處理效能,高性能處理器通常選擇將乘法器單獨使用一個 FU 來實現,並在這個 FU 中實現乘累加的功能 有些處理器基於功耗和成本的考量,會將整數類型的乘法交由浮點數運算的 FU 來運算 整數轉成浮點數 → 浮點數乘法 → 浮點數轉為整數 指令的 latency 會變長,但較省面積和功耗,Intel Atom CPU 就採用了此設計 9.2.2 - AGU AGU (Address Generate Unit) 用來計算記憶體存取位址,如 load/store 指令 在一般 pipeline 處理器中,記憶體的存取位址都是交由 ALU 來計算,但是在 superscalar CPU 中,由於需要同時執行多條的指令,記憶體存取指令的執行效率直接影響了處理器的性能,所以通常會單獨使用一個 FU 來計算其位址 9.2.3 - BRU BRU (Branch Unit) 負責處理 control flow 類型的指令,如 branch、jump、call 和 return 類型的指令,BRU 負責將這些指令所攜帶的目標位址計算出來,並根據一定的條件決定是否使用這些位址;同時,在這個 FU 中還會對分支預測與否正確進行檢查,一旦發現 mis-prediction,就需要啟動相對硬的恢復機制 ARM 和 PowerPC 等處理器在指令中的編碼加上了 condition code,根據 condition code 來決定指令是否執行 優點: ...
<超標量處理器概覽> 第 8 章 - 發射 (Part 2)
8.5 - 喚醒 (Wake up) 8.5.1 - 單週期指令的喚醒 Wake up 是指被 select 電路選中的指令將其執行完後,將其 destination register 和 Issue Queue 中所有的 source registers 編號做比較,並將相同編號的 source registers 標記為 ready 的過程 因為需要將被選中指令的 destination register 編號需要傳遞到 Issue Queue 中的所有 entries,當 Issue Queue 容量比較大,或是 Issue Queue 的個數比較多時,這個編號就需要走很長的路徑 一般情況下,select 電路的個數等同於 issue width 以下是 Issue width = 4 的 wake-up 電路 (只包含一個 Issue Queue 的 entry,且所有 select 電路共用同一個 Issue Queue): SrcL /SrcR:第一、第二個 source register 的編號 ValL / ValR:指令是否存在第一、第二個 source register RdyL / RdyR:第一、第二個 source register 是否準備好 Dest:Destination register 的編號 Issued:指令是否已經被 issued 出去 當指令準備好,並被 select 電路選中時,此時該指令不一定會馬上被 issued 出去 (e.g. 一條指令使用了 load 指令的結果,即使其被 select 電路選中,也不可以馬上離開 Issue Queue),因此需要透過 Issued 記錄該指令是否已經被 issued 了 如果已經被 issued,select 電路就不會重複選到該指令 當指令準備好時,就會向 select 電路送 request 訊號,請求被仲裁 Select 電路會選擇根據是否選擇了該指令,送出 grant 訊號 四個 select 電路同時間只會有一個 grant 訊號是 valid 的 如果一條指令被 granted,那麼就會將其 Dest 送到 tag bus 上,以便 broadcast 給所有的 Issue Queue entries 實務上,為了提供速度,需要減少 Issue Queue 的容量,因此可以使用多個容量較小的 Issue Queue ...
<超標量處理器概覽> 第 8 章 - 發射 (Part 1)
8.1 - 概述 Issue 就是將符合一定條件的指令從 Issue Queue 中選出來,並送到 FU (Function Unit) 中執行的過程 Issue Queue 會依照一定的規則,選擇那些 source registers 都已經準備好的指令,將其送到 FU 中執行,這個過程就稱為:Issue Issue Queue 也被稱為 Reservation Station 對於 in-order core,指令會按照程式原始順序加進 Issue Queue 中,此時 Issue Queue 就相當於一個 FIFO 對於 out-of-order core,只有少數指令,如 store 及分支指令,才會採用 in-order 的方式執行,對其他大部分的指令,都是採用 out-of-order 的方式執行 指令到了 Issue Queue 後,只要 Issue Queue 中任一條指令的 operands 都準備好了,且滿足 issue 的條件,就可以將其送到 FU 中執行 Issue Queue 設計的好壞,會直接影響處理器的 concurrency Issue stage 的硬體比較複雜,一般它都在 critical path 上,直接影響 CPU 的 cycle time ...
<超標量處理器概覽> 第 7 章 - 暫存器重命名
7.1 - 概述 程式中不同指令的相關性 (dependency) 可以分類為: 數據相關性 (data dependency),包含以下幾種類型: WAW (Write After Write) dependency,i.e. output dependence: 兩條指令都將結果寫到同一個 destination register 中 WAR (Write After Read) dependency,i.e. anti-dependence: 一條指令的 destination register 和它前面某條指令的 source register 相同 RAW (Read After Write) dependency,i.e. true dependence: 一條指令的 source register 和它前面某條指令的 destination register 相同 只有 RAW 是真的相關性, WAR 和 RAW 可以透過 register renaming 來解決 記憶體數據相關性 (memory data dependency): Load 和 store 指令之間的相關性,代表 load 和 store 指令都存取到同一個位址 同樣也分為 WAW、WAR、RAW dependencies 控制相關性 (control dependency): ...
<超標量處理器概覽> 第 6 章 - 指令解碼
在 pipeline 中,decode stage 的任務是將指令中的資訊提取出來,CPU 使用這些資訊控制後續的 pipeline 來執行這條指令 影響 decode 的複雜度因素有: 指令集的複雜度: CISC vs. RISC 每個 cycle 可以 decode 的指令個數: 每個 cycle 可以 decode N 條指令,那就需要 N 個 decode 電路 6.1 - 指令緩存 現代處理器可以在 fetch stage 從 I-Cache 讀出大於每個 cycle 可以 decode 指令個數的指令,因此需要在 fetch stage 和 decode stage 之間加一個 buffer,用來將 I-Cache 讀出的所有指令保存起來,這個 buffer 就稱為 Instruction Buffer Fetch stage 最終會輸出兩個主要的內容給 Instruction Buffer: 從 I-Cache 讀出的 N 條指令 (並非所有指令都是有效的) 有效的指令個數 1 instruction fetch 位址不是 cache aligned 時,或是 fetch group 中包含預測為 taken 的指令,會導致 fetch stage 沒辦法寫入 N 條指令進 Instruction Buffer 此時需要告知 Instruction Buffer,有效的指令個數 現在 superscalar CPU 需要 Instruction Buffer 的原因: Superscalar CPU 可以每個 cycle 可以 fetch 的指令個數大於每個 cycle 可以 decode 的指令個數,這樣即使在發生 I-Cache miss 時,Instruction Buffer 中仍有可能還有保存尚未 decode 的指令,因此不需要 stall pipeline,可以繼續 decode 指令,增加 CPU 的性能 Superscalar CPU 中即使每個 cycle 可以 decode 的指令個數與每個 cycle 所 fetch 的指令個數相等,在 decode stage 仍會有一些特殊的指令需要處理,導致在 fetch stage 所 fetch 的指令沒有辦法全部被 decode 如 ARM 的 multiply-accumulate 指令 (UMAAL RdLo, RdHi, Rn, Rm),會有兩個 destination registers,為了減少對 register renaming 的影響,會將其拆分成兩條普通的指令,每條指令只有一個 destination register 因此,如果在 decode stage 沒有特別的處理,會導致 decode 的指令個數大於 fetch 指令的個數,但後續的 pipeline 都是依照原先的指令個數來設計的,不可能因為這些不常見的指令而增加後續 pipeline 的處理能力 (因為會增加硬體面積,且使用率也不高) 為了解決此問題,就需要加入 Instruction Buffer,讓 multiply-accumulate 後面的指令,可以等到下一個 cycle 再 decode 由於 Instruction Buffer 可以在一個 cycle 內寫入多條的指令,也可以讀出多條的指令,因此也是一個 multi-port 的 FIFIO;但在實際設計上,並不會使用真的 multi-port 的 SRAM 來實現這樣的 FIFO,而是會採用 interleaving 的方式 (參考:2.3.1 - True Multi-port),使用多個 single-port 的 SRAM 來實現,從而避免使用 multi-port SRAM 所導致的硬體速度上的限制 6.2 - 一般情況 ARM 的 CPSR,只有 4 個 bits (N、C、Z、V),但其他的暫存器都是 32 bits 的;如果將 CPSR 跟其他的暫存器統一對待,會造成很多暫存器無法有效的被利用,因為 32 bits 的暫存器,只存了 4 bits 的資料 因此,一般都是將 CPSR 單獨處理,對 CPSR 單獨使用一套 register renaming 的流程,這樣就可以根據 CPSR 的特性來訂製 register renaming 的流程 且考慮到條件執行的指令只是少部份,所以所使用的 register file 可以很小,例如只需只用 16 個 physical registers 就足夠了 指令所攜帶的 source registers 和 destination registers 的個數直接決定了 register renaming 電路在實現上的難易度: 像是 register renaming mapping tables 的 ports 數、指令間相關性檢查電路的複雜度 由於 RISC 架構的指令比較整齊劃一,很容易解析出指令中的 opcode 和 operands,在 decode stage 產生的 pipeline 控制訊號也比較少,因此 RISC 架構的 instruction decoding 通常都可以在 1 個 cycle 完成 一般情況下,RISC 處理器在 decode stage 完成的任務可以概括為: What type:例如指令是算術指令還是分支指令 What operation:例如當指令是算術指令時,是進行什麼運算;是分支指令時,它的跳轉條件是什麼樣的 What resource:例如對算術指令時來說,其 source 和 destination registers 是哪些,有沒有 immediate value 6.3 - 特殊情況 即使在 RISC 指令集中,也存在一些特殊的指令;這些指令不能按照一般的方法處理 例如:ARM 的 LDM / STM 指令,需要多個 cycles 才能完成;而且它們的 source 和 destination registers 有多個,如果在 superscalar CPU 中對它們跟普通指令一樣來處理的話,會需要增加 register renaming mapping table、issue queue 和 ROB 所需的 ports 數,增加硬體的面積,並降低處理效能 因此在 superscalar CPU 中,並不會直接處理 LDM / STM 這樣的指令,而是會將其轉換為多條普通的指令 (µops),每條普通的指令就是一般的 load / store 指令,這樣就可以用普通指令的方式來處理 6.3.1 - 分支指令的處理 先前提到,採用 checkpoint 的方式對 mis-prediction 的分支指令恢復 CPU 的狀態,為了減少分支指令編號分配電路的複雜度,需要限制每個 cycle 能 decode 的分支指令個數,例如每個 cycle 只能 decode 一條分支指令 (參考:4.4 - 分支預測失敗時的恢復) 但是,每個 cycle 從 Instruction Buffer 讀取的指令中,有可能存在多條的分支指令,需要在 decode stage 做特別的處理 簡單的作法: ...
<超標量處理器概覽> 第 4 章 - 分支預測 (Part 2)
4.3 - 分支指令的目標位址預測 分支指令的目標位址可以分為兩種: 直接跳轉 (direct): 對於直接跳轉指令 (e.g. RISC-V 的 beq、jal、lui 指令),它的跳轉 offset 是以 immediate value 的方式 encode 在 opcode 中 (有可能是 PC-relative,也有可能不是),所以它的目標位址是固定的,只要記錄這條分支指令目標位址即可 當再次遇到這條分支指令時,如果分支預測結果是 taken,那麼目標位址就可以直接使用先前所記錄的值 間接跳轉 (indirect): 對於間接跳轉指令 (e.g. RISC-V 的 jalr 指令),由於它的目標位址來自於暫存器,而暫存器的值是有可能一直變化的,所以對間接跳轉指令來說,要預測目標位址並不是一件容易的事情 然而慶幸的是,程式中大部份的間接跳轉指令都是用來呼叫函式的,也就是 call 和 return;這種目標位址是有規律的,因此可以對其進行預測 4.3.1 - 直接跳轉類型的分支預測 對於直接跳轉類型的分支指令 (假設是 PC-relative),其目標位址有兩種情況: 當分支指令 not taken 時: 目標位址 = 當前分支指令的 PC 值 + sizeof(fetch group) e.g. PC + 4 當分支指令 taken 時: 目標位址 = 當前分支指令的 PC 值 + signed extended offset e.g. PC + offset ...
<超標量處理器概覽> 第 4 章 - 分支預測 (Part 1)
4.1 - 概述 分支預測和 Cache 左右著 CPU 的效能,對於 superscalar CPU 來說,準確度高的分支預測更為重要 在一般的 RISC 指令集中,分支指令包含兩個要素: 方向: 分支指令的方向只有兩種:跳轉 (taken),或是不跳轉 (not taken) 有些跳轉指令是無條件執行的 (e.g. jmp 指令),有些跳轉指令則是需要根據指令中攜帶的條件是否成立來決定是否發生跳轉 (e.g. beq 指令) 目標位址: 對於一般的 RISC 指令集,目標位址在指令中有兩種形式: 直接跳轉 (direct): 目標位址 = 當前分支指令 (或分支指令的下一條指令) 的 PC 值 + immediate PC offset 由於指令通常只有 32/64 bits,因此 immediate PC offset 的值範圍也就有限,因此也限制了直接跳轉能夠跳轉的範圍 因為 immediate PC offset 值是 encode 在分支指令中,因此在 pipeline 的 decode stage 就可以直接將 immediate PC offset 給解析出來,因此這種類型的指令是很容易進行分支預測的 這種跳轉指令的執行效能也比較好 間接跳轉 (indirect): 目標位址是來自於 register 的內容,由於 register 是 32/64 bits,因此可以跳轉到任何位址 由於跳轉位址來自於 register,因此此類的分支指令的目標位址需要等待一段時間才能獲得 (e.g. 要等到 pipeline 的 execute stage) 在確認目標位址前進到 pipeline 的指令,有可能都會是無效的 (if taken),因此增大了 mis-prediction penalty 且由於 register 的內容是會動態變化的,因此這類的分支指令很難對目標位址進行預測 不過慶幸的是,這類指令通常都是 call 或是 return 類型的指令,這類指令都有很強的規律性,是容易被預測的 MIPS R3000 只使用了 5 級的 pipeline,在第二個階段的 decode stage,就可以得到分支指令跳轉的目標位址,即使發生了跳轉 (taken),也只需要丟棄一道仍在 fetch stage 的指令,misprediction penalty 並不高 ...