<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Virtualization on 0xc0de</title>
    <link>https://0xc0de.xyz/categories/virtualization/</link>
    <description>Recent content in Virtualization on 0xc0de</description>
    <image>
      <title>0xc0de</title>
      <url>https://0xc0de.xyz/%3Clink%20or%20path%20of%20image%20for%20opengraph,%20twitter-cards%3E</url>
      <link>https://0xc0de.xyz/%3Clink%20or%20path%20of%20image%20for%20opengraph,%20twitter-cards%3E</link>
    </image>
    <generator>Hugo</generator>
    <language>zh-tw</language>
    <lastBuildDate>Wed, 21 Oct 2026 22:39:29 +0800</lastBuildDate>
    <atom:link href="https://0xc0de.xyz/categories/virtualization/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>從 VirtIO 到 vDPA：封包交給硬體之後，虛擬機如何熱遷移?</title>
      <link>https://0xc0de.xyz/posts/virtio-vhost-vdpa/</link>
      <pubDate>Wed, 21 Oct 2026 22:39:29 +0800</pubDate>
      <guid>https://0xc0de.xyz/posts/virtio-vhost-vdpa/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚠️ 本文經由 AI 整理並撰寫。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;假設你正在替一個虛擬化平台設計網路資料路徑。起初，讓 QEMU 處理虛擬網卡的 I/O 很直覺：裝置由軟體管理，狀態也容易掌握。但當封包率提高，Host CPU 花在描述符處理、通知與封包搬運上的成本，就會逐漸變得明顯。&lt;/p&gt;
&lt;p&gt;把這些工作交給核心、專用的使用者空間程式，甚至網卡硬體，似乎是自然的下一步。然而，雲端平台不只需要「傳得快」，還需要在主機維護、資源調度時，把正在執行的虛擬機搬到另一台主機。當裝置可以繞過 QEMU，直接改寫 Guest 記憶體，QEMU 又該如何知道哪些資料必須重新傳送?到了交接那一刻，又由誰確認封包處理到哪裡?&lt;/p&gt;
&lt;p&gt;這正是 VirtIO、vhost、vDPA 與 Shadow Virtqueue（SVQ）串在一起的原因：&lt;strong&gt;它們不只是不同的加速技術，更是在重新分配資料處理工作，以及對執行狀態的控制權。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;本文以 Linux Host 上的 QEMU/KVM 與 virtio-net 為例，先從一個封包如何離開 Guest 談起，再沿著硬體卸載、髒頁追蹤與熱遷移，理解這些技術各自解決了什麼問題。具體功能與限制仍須對照實際的核心、QEMU、驅動及韌體版本。&lt;/p&gt;
&lt;h2 id=&#34;一先看一個封包virtio-統一的是介面不是後端&#34;&gt;一、先看一個封包：VirtIO 統一的是介面，不是後端&lt;/h2&gt;
&lt;p&gt;Guest 裡的應用程式送出封包後，作業系統最終會把工作交給網卡驅動。在 VirtIO 架構中，這個驅動不必理解某一家實體網卡的私有佇列格式，而是使用標準的 VirtIO 介面，透過 &lt;strong&gt;Virtqueue（VQ）&lt;/strong&gt; 與裝置交換工作。&lt;/p&gt;
&lt;p&gt;這裡容易產生第一個誤會：Virtqueue 並不是一個裝滿封包內容的環形儲存區。以 Split Virtqueue 為例，它主要包含三種結構：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;結構&lt;/th&gt;
					&lt;th&gt;用途&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Descriptor Table&lt;/td&gt;
					&lt;td&gt;描述 buffer 的位址、長度、存取方向與鏈結關係&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Available Ring&lt;/td&gt;
					&lt;td&gt;告訴裝置有哪些描述符鏈可以處理&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Used Ring&lt;/td&gt;
					&lt;td&gt;由裝置回報哪些描述符鏈已完成，以及完成長度&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;真正的封包內容稱為 &lt;strong&gt;payload&lt;/strong&gt;，通常放在描述符指向的另一塊 Guest buffer。換句話說，Virtqueue 傳遞的是「去哪裡讀寫資料」以及「工作完成了沒有」，而不等同於資料本身。&lt;/p&gt;
&lt;p&gt;具體而言，一個 Split descriptor 的欄位配置如下；這裡列的是欄位寬度，實際讀寫還必須遵守規範的位元組序與對齊要求：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;欄位&lt;/th&gt;
					&lt;th&gt;大小&lt;/th&gt;
					&lt;th&gt;意義&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;addr&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;64 bits&lt;/td&gt;
					&lt;td&gt;buffer 位址&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;len&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;32 bits&lt;/td&gt;
					&lt;td&gt;buffer 長度&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;flags&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;16 bits&lt;/td&gt;
					&lt;td&gt;描述符方向、是否還有下一節，以及是否為間接描述符表&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;next&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;16 bits&lt;/td&gt;
					&lt;td&gt;描述符鏈中下一節的索引&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;四個欄位合計 16 bytes。&lt;code&gt;VRING_DESC_F_WRITE&lt;/code&gt; 表示&lt;strong&gt;裝置可以寫入&lt;/strong&gt; buffer，而不是 Guest 可以寫入；TX payload 通常供裝置讀取，RX buffer 則供裝置寫入。&lt;code&gt;VRING_DESC_F_NEXT&lt;/code&gt; 用於鏈結多個描述符，&lt;code&gt;VRING_DESC_F_INDIRECT&lt;/code&gt; 則讓描述符指向另一張描述符表。一個封包可能跨多個 buffers，不能把「一個封包」等同於「一個 16 bytes 描述符」。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>⚠️ 本文經由 AI 整理並撰寫。</p>
</blockquote>
<p>假設你正在替一個虛擬化平台設計網路資料路徑。起初，讓 QEMU 處理虛擬網卡的 I/O 很直覺：裝置由軟體管理，狀態也容易掌握。但當封包率提高，Host CPU 花在描述符處理、通知與封包搬運上的成本，就會逐漸變得明顯。</p>
<p>把這些工作交給核心、專用的使用者空間程式，甚至網卡硬體，似乎是自然的下一步。然而，雲端平台不只需要「傳得快」，還需要在主機維護、資源調度時，把正在執行的虛擬機搬到另一台主機。當裝置可以繞過 QEMU，直接改寫 Guest 記憶體，QEMU 又該如何知道哪些資料必須重新傳送?到了交接那一刻，又由誰確認封包處理到哪裡?</p>
<p>這正是 VirtIO、vhost、vDPA 與 Shadow Virtqueue（SVQ）串在一起的原因：<strong>它們不只是不同的加速技術，更是在重新分配資料處理工作，以及對執行狀態的控制權。</strong></p>
<p>本文以 Linux Host 上的 QEMU/KVM 與 virtio-net 為例，先從一個封包如何離開 Guest 談起，再沿著硬體卸載、髒頁追蹤與熱遷移，理解這些技術各自解決了什麼問題。具體功能與限制仍須對照實際的核心、QEMU、驅動及韌體版本。</p>
<h2 id="一先看一個封包virtio-統一的是介面不是後端">一、先看一個封包：VirtIO 統一的是介面，不是後端</h2>
<p>Guest 裡的應用程式送出封包後，作業系統最終會把工作交給網卡驅動。在 VirtIO 架構中，這個驅動不必理解某一家實體網卡的私有佇列格式，而是使用標準的 VirtIO 介面，透過 <strong>Virtqueue（VQ）</strong> 與裝置交換工作。</p>
<p>這裡容易產生第一個誤會：Virtqueue 並不是一個裝滿封包內容的環形儲存區。以 Split Virtqueue 為例，它主要包含三種結構：</p>
<table>
	<thead>
			<tr>
					<th>結構</th>
					<th>用途</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Descriptor Table</td>
					<td>描述 buffer 的位址、長度、存取方向與鏈結關係</td>
			</tr>
			<tr>
					<td>Available Ring</td>
					<td>告訴裝置有哪些描述符鏈可以處理</td>
			</tr>
			<tr>
					<td>Used Ring</td>
					<td>由裝置回報哪些描述符鏈已完成，以及完成長度</td>
			</tr>
	</tbody>
</table>
<p>真正的封包內容稱為 <strong>payload</strong>，通常放在描述符指向的另一塊 Guest buffer。換句話說，Virtqueue 傳遞的是「去哪裡讀寫資料」以及「工作完成了沒有」，而不等同於資料本身。</p>
<p>具體而言，一個 Split descriptor 的欄位配置如下；這裡列的是欄位寬度，實際讀寫還必須遵守規範的位元組序與對齊要求：</p>
<table>
	<thead>
			<tr>
					<th>欄位</th>
					<th>大小</th>
					<th>意義</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>addr</code></td>
					<td>64 bits</td>
					<td>buffer 位址</td>
			</tr>
			<tr>
					<td><code>len</code></td>
					<td>32 bits</td>
					<td>buffer 長度</td>
			</tr>
			<tr>
					<td><code>flags</code></td>
					<td>16 bits</td>
					<td>描述符方向、是否還有下一節，以及是否為間接描述符表</td>
			</tr>
			<tr>
					<td><code>next</code></td>
					<td>16 bits</td>
					<td>描述符鏈中下一節的索引</td>
			</tr>
	</tbody>
</table>
<p>四個欄位合計 16 bytes。<code>VRING_DESC_F_WRITE</code> 表示<strong>裝置可以寫入</strong> buffer，而不是 Guest 可以寫入；TX payload 通常供裝置讀取，RX buffer 則供裝置寫入。<code>VRING_DESC_F_NEXT</code> 用於鏈結多個描述符，<code>VRING_DESC_F_INDIRECT</code> 則讓描述符指向另一張描述符表。一個封包可能跨多個 buffers，不能把「一個封包」等同於「一個 16 bytes 描述符」。</p>
<p>Guest 在 available ring 放入描述符鏈的 head index，再推進 <code>avail-&gt;idx</code>；裝置消費後，在 used ring 回報 head ID 與寫入長度，再推進 <code>used-&gt;idx</code>。這些索引描述的是發布與完成進度，並不是 payload 的位址。像 <code>last_avail_idx</code>、<code>last_used_idx</code> 這類軟體或後端狀態，則用來記住各自已處理到哪裡；它們的語意不能只靠名稱推斷，更不能在兩組不同佇列之間直接照抄。</p>
<p>發送時，Guest 驅動準備好 payload 與描述符，依 VirtIO 的記憶體排序規則發布工作，必要時再通知後端有新工作，這個通知稱為 <strong>Kick</strong>。後端處理完後更新完成資訊，必要時通知 Guest，這個方向的通知通常稱為 <strong>Call</strong>。通知可以被抑制或批次處理，因此不能把每個封包都想成固定經歷一次 VM-exit 與一次中斷。</p>
<p>這裡的記憶體排序並非枝節：必須先讓描述符與資料可見，再發布新的 available index；讀取完成資訊時，也必須確保裝置先前寫入的內容已可見。即使 Guest、QEMU 與裝置共享同一份 RAM，也不代表可以省略 memory barrier。Packed Virtqueue 會把描述符及 available / used 狀態放進不同於 Split Ring 的配置，後續談到 SVQ 時，必須另外核對它支援的 ring 格式。</p>
<p>如果後端由 QEMU 實作，那麼裝置模型與主要的 VirtIO 後端資料處理都在 QEMU 使用者空間。但 <strong>VirtIO 並沒有要求後端一定是 QEMU</strong>：只要維持 Guest 所看到的介面與語意，後端就能換成另一種實作。</p>
<p>這個前後端分離的特性，讓我們不必先改 Guest 驅動，就能開始調整 Host 的 I/O 路徑。</p>
<h2 id="二把工作移出-qemuvhost-解決了什麼">二、把工作移出 QEMU：vhost 解決了什麼?</h2>
<p>要理解 vhost，先把虛擬裝置的工作分成兩類。**控制面（Control Plane）**負責裝置建立、功能協商、佇列設定、記憶體映射，以及停止、重設與恢復；**資料面（Data Plane）**則持續讀取描述符、處理 payload、更新完成狀態並發送通知。</p>
<p>前者關心「這個裝置如何運作」，後者關心「這一批工作如何完成」。vhost 的核心價值，就是讓後者不必都由 QEMU 執行。</p>
<p>最常見的例子是 <strong>vhost-net</strong>。QEMU 仍然向 Guest 呈現 VirtIO 網卡、完成必要設定，但主要的 VirtIO 資料處理移到 Host 核心，由核心後端對接 TAP 等網路路徑。這減少了資料路徑對 QEMU 使用者空間的依賴，不代表整條核心網路路徑從此沒有 CPU 或資料複製成本。</p>
<p>在 Linux 中，這條路徑由 <code>vhost_net</code> 模組與 vhost worker 等核心機制支援，相關實作可從 <code>drivers/vhost/net.c</code> 追蹤。Guest 的通知可以經核心註冊的 eventfd 路徑交給後端，不必每次先喚醒 QEMU。這仍是軟體資料面：核心要執行描述符處理及相應網路工作，並非網卡自動理解了 VirtIO。</p>
<p>另一種選擇是 <strong>vhost-user</strong>：QEMU 透過 Unix domain socket 與獨立的使用者空間後端交換控制訊息，設定共享記憶體、佇列及通知通道。真正的資料處理則由外部程式執行，例如 OVS-DPDK；儲存場景也有 SPDK 等應用。</p>
<p>Socket 在這裡主要傳遞控制訊息與檔案描述符，不是逐包傳送 payload。後端透過可共享的記憶體 backing，例如適當配置的 memfd 或檔案映射，存取 Guest RAM，並使用協定設定的 kick / call file descriptors 協調工作。控制通道與資料所在的共享記憶體分開，才是它能避免讓 QEMU 成為逐包中繼站的原因。</p>
<p>兩者可以用同一張圖理解：Guest 端不變，改變的是誰消費 Virtqueue。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-0-1"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-0-1">1</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-0-2"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-0-2">2</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-0-3"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-0-3">3</a>
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>QEMU 軟體後端：Guest driver &lt;-&gt; Guest VQ &lt;-&gt; QEMU backend
</span></span><span style="display:flex;"><span>vhost-net：    Guest driver &lt;-&gt; Guest VQ &lt;-&gt; Host kernel backend
</span></span><span style="display:flex;"><span>vhost-user：   Guest driver &lt;-&gt; Guest VQ &lt;-&gt; External userspace backend
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="dpdk-很快為什麼還需要硬體卸載">DPDK 很快，為什麼還需要硬體卸載?</h3>
<p>vhost-user 經常與 DPDK 一起出現，但兩者不是同義詞。vhost-user 是後端整合協定；DPDK 是使用者空間資料面開發套件，其 Poll Mode Driver（PMD）讓應用程式直接管理網卡資料面佇列，通常繞過一般核心網路協定棧，以輪詢與批次處理降低開銷。vhost-user 本身不強制使用 DPDK，也不強制輪詢。</p>
<p>這種設計把 CPU 使用方式從「等待通知後處理」轉成「主動檢查工作」。Busy polling 能在高負載下提供良好延遲與吞吐量，但通常也需要保留專用核心。DPDK 另有中斷、節能與混合模式；反過來，核心網路路徑也有 NAPI、批次處理與 offload，不能只用「輪詢對中斷」判斷兩者效能。</p>
<p>此外，繞過一般核心網路協定棧不等於全鏈路零複製。實體網卡的 DPDK buffer 與 Guest buffer 可能是兩套記憶體，後端仍可能需要搬運封包；是否能避免複製，要看 buffer 管理、offload 與實際資料路徑。</p>
<p>以 OVS-DPDK 的接收路徑為例，可以把工作拆成以下幾段：</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-1-1"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-1-1">1</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-1-2"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-1-2">2</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-1-3"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-1-3">3</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-1-4"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-1-4">4</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-1-5"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-1-5">5</a>
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>實體網卡
</span></span><span style="display:flex;"><span>  -&gt; DMA 到 PMD 管理的接收 buffer
</span></span><span style="display:flex;"><span>  -&gt; OVS-DPDK 解析、查表與轉送
</span></span><span style="display:flex;"><span>  -&gt; vhost-user 後端填入 Guest 提供的 RX buffer
</span></span><span style="display:flex;"><span>  -&gt; 更新 Guest used ring，必要時通知 Guest
</span></span></code></pre></td></tr></table>
</div>
</div><p>其中「PMD buffer → Guest RX buffer」是否需要 memcpy，是獨立於「網卡能不能 DMA」的另一個問題。PMD 也不一定完全排除核心參與：有些裝置透過 VFIO 綁定，有些採核心與使用者空間協作的驅動模式。Hugepages 常用來配置資料面記憶體，但實際需求仍取決於 PMD 與記憶體模式。</p>
<p>因此，即使軟體方案已經足夠快，平台仍可能希望把資料處理交給硬體，以回收 Host CPU。假設一台 64 核心主機保留 8 核心給軟體資料面，就占了核心數的 12.5%。這只是容量計算示例，卻指出一個重要問題：<strong>達到相同吞吐量，需要付出多少可供工作負載使用的 CPU?</strong></p>
<p>vDPA 就是在這個問題上，往硬體卸載再走一步。</p>
<h3 id="dpu-的價值不只在吞吐量">DPU 的價值不只在吞吐量</h3>
<p>網路從 25G 提升到 100G、200G 或 400G 後，Host 面臨的可能不只是頻寬，還有每秒封包數、快取污染、記憶體頻寬及每包處理成本。相同頻寬下，小封包通常需要更多次描述符與轉送處理；因此不能只用鏈路速度推算需要保留多少 CPU，也不能替軟體資料面設定一個通用的吞吐量上限。</p>
<p>DPU / SmartNIC 可以把支援的網路、儲存、安全或管理工作移到獨立的處理核心與加速引擎。這除了回收 Host CPU，也可能讓基礎設施控制與租戶工作負載位於不同的執行域，並支援裸金屬服務的管理架構。但實際隔離邊界取決於平台設計，不能只因為用了 DPU，就認定所有功能都已硬體化或安全問題都已消失。</p>
<p>因此，DPU 是較廣的基礎設施卸載平台，vDPA 則是本文用來接上 VirtIO 相容資料面的機制。兩者可以搭配，卻不能畫上等號。</p>
<h2 id="三vdpa保留-virtio-介面把資料面交給相容裝置">三、vDPA：保留 VirtIO 介面，把資料面交給相容裝置</h2>
<p>**vDPA（vhost Data Path Acceleration）**提供管理 VirtIO 相容資料面裝置的抽象框架。在硬體 vDPA 的情境下，裝置能處理 VirtIO 相容佇列，並透過 DMA 直接存取 Guest buffer，不必由 Host 軟體逐一搬運 payload。</p>
<p>不過，vDPA 與 <code>vhost-vdpa</code> 必須分開看。前者是裝置框架，後者是 QEMU 等使用者空間程式接入該框架的一種方式。典型 Linux 路徑如下：</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-2-1"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-2-1"> 1</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-2-2"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-2-2"> 2</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-2-3"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-2-3"> 3</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-2-4"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-2-4"> 4</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-2-5"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-2-5"> 5</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-2-6"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-2-6"> 6</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-2-7"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-2-7"> 7</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-2-8"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-2-8"> 8</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-2-9"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-2-9"> 9</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-2-10"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-2-10">10</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-2-11"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-2-11">11</a>
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Guest：virtio-net 驅動
</span></span><span style="display:flex;"><span>          |
</span></span><span style="display:flex;"><span>QEMU：虛擬 VirtIO 裝置、設定與生命週期管理
</span></span><span style="display:flex;"><span>          |
</span></span><span style="display:flex;"><span>vhost-vdpa：/dev/vhost-vdpa-X
</span></span><span style="display:flex;"><span>          |
</span></span><span style="display:flex;"><span>Linux vDPA framework
</span></span><span style="display:flex;"><span>          |
</span></span><span style="display:flex;"><span>Vendor vDPA driver
</span></span><span style="display:flex;"><span>          |
</span></span><span style="display:flex;"><span>VirtIO 相容資料面裝置
</span></span></code></pre></td></tr></table>
</div>
</div><p>這個分層把廠商差異留在 Host。QEMU 使用通用後端介面，廠商驅動再把設定轉換成硬體所需的操作。Linux 的 <code>drivers/vhost/vdpa.c</code> 實作 vhost-vdpa 接口，vDPA 框架與廠商驅動則另位於 <code>drivers/vdpa/</code>，所以不宜單純把 vDPA 當成 vhost 的子集。</p>
<p>廠商驅動要銜接的並不只有封包收發，還包括取得及設定 feature bits、queue size、ring 位址、queue ready 狀態、通知與記憶體映射，以及後端所支援的佇列狀態保存 / 恢復。硬體資料面可由 ASIC、FPGA 或可程式化引擎實作；判斷重點是它能否提供所需的 VirtIO 語意，而不是產品是否被冠上 SmartNIC 或 DPU 的名稱。</p>
<p>這也不是唯一的整合方式：vDPA 裝置可以透過 <code>virtio-vdpa</code> 接入 Host 的 VirtIO 子系統；DPDK 也有使用者空間 vDPA 驅動，可經 vhost-user 與 QEMU 整合。Linux 甚至提供軟體 vDPA 模擬裝置。因此，下文所說的「硬體直接處理」，特指硬體 vDPA，而非 vDPA 框架必然具有的屬性。</p>
<h3 id="封包不經-qemu為什麼還需要-qemu">封包不經 QEMU，為什麼還需要 QEMU?</h3>
<p>因為「處理封包」與「管理虛擬裝置」是兩件事。</p>
<p>在本文的 QEMU/KVM 架構中，Guest 看到的仍是 QEMU 呈現的標準 VirtIO 裝置，例如 <code>virtio-net-pci</code>。QEMU 協調功能協商、佇列數量、Guest 記憶體註冊、DMA 映射與通知通道，也負責 reset、停止及遷移等生命週期事件。部分控制操作由核心、後端或硬體執行，但仍需要有人把它們協調成一個完整的虛擬裝置。</p>
<p>所以，vDPA 省去的是穩態資料路徑中的部分軟體工作，不是把裝置管理一併拿掉。概念上也不一定要由 QEMU 扮演這個角色，其他 VMM 可以使用相應介面。</p>
<h3 id="這與-sr-iov-vf-直通有什麼不同">這與 SR-IOV VF 直通有什麼不同?</h3>
<p>SR-IOV 是把 PCIe 裝置資源劃分為 Physical Function（PF）與多個 Virtual Function（VF）的硬體能力，不是遷移協定。傳統 VF 經 VFIO 指派給 Guest 時，Guest 通常看到廠商特定的 PCIe 裝置介面，因此需要相應的 VF 驅動；vDPA 則把廠商特定控制留在 Host，Guest 使用 VirtIO 驅動。</p>
<table>
	<thead>
			<tr>
					<th>比較項目</th>
					<th>傳統 VF 經 VFIO 直通</th>
					<th>QEMU + 硬體 vDPA</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Guest 看到的介面</td>
					<td>通常是廠商特定 VF 介面</td>
					<td>標準 VirtIO 介面</td>
			</tr>
			<tr>
					<td>Guest 驅動</td>
					<td>對應 VF 的驅動</td>
					<td>例如 virtio-net</td>
			</tr>
			<tr>
					<td>Host 端支援</td>
					<td>PF 管理驅動、VFIO 與平台支援</td>
					<td>vDPA 廠商驅動及相應後端</td>
			</tr>
			<tr>
					<td>遷移條件</td>
					<td>VFIO migration、裝置與驅動能力</td>
					<td>vDPA、QEMU 與裝置狀態恢復能力</td>
			</tr>
	</tbody>
</table>
<p>這裡的重點是<strong>驅動與介面的邊界</strong>，不是開源與私有之分：不少廠商 VF 驅動已在 Linux 核心中。vDPA 的廠商驅動，例如 <code>mlx5_vdpa</code>，裝在 Host；Guest 仍須具備對應的 VirtIO 驅動，Windows 映像也不一定預裝 NetKVM。</p>
<p>兩種技術並不互斥，有些 vDPA 實作本身就使用 VF 提供資源。現代 VFIO 裝置也可能支援遷移。VirtIO 的標準介面有助於降低 Guest 與硬體的耦合，卻不代表兩張都支援 vDPA 的網卡，就能任意互相遷移。</p>
<p>要知道這個限制從何而來，接下來必須看看硬體究竟如何碰到 Guest 記憶體。</p>
<h2 id="四硬體如何直接存取-guest-記憶體">四、硬體如何直接存取 Guest 記憶體?</h2>
<p>Guest RAM 實際上由 Host 記憶體承載。同一塊 buffer 在不同角色眼中，會有不同的位址：Guest 使用 Guest Physical Address（GPA），QEMU 透過 Host Virtual Address（HVA）存取它，底層對應 Host Physical Address（HPA）；裝置執行 DMA 時，則使用 I/O Virtual Address（IOVA）。</p>
<p>這些名稱描述的是不同位址空間，而不是一條必然固定的轉換鏈。沒有 Guest vIOMMU 等額外轉換時，IOVA 可能採用 GPA；加入不同的記憶體與後端配置後，IOVA 就不一定等於 GPA。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-3-1"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-3-1">1</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-3-2"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-3-2">2</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-3-3"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-3-3">3</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-3-4"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-3-4">4</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-3-5"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-3-5">5</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-3-6"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-3-6">6</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-3-7"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-3-7">7</a>
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>裝置從描述符取得 DMA 位址（IOVA）
</span></span><span style="display:flex;"><span>                 |
</span></span><span style="display:flex;"><span>                 v
</span></span><span style="display:flex;"><span>       平台 IOMMU / 裝置位址轉換
</span></span><span style="display:flex;"><span>                 |
</span></span><span style="display:flex;"><span>                 v
</span></span><span style="display:flex;"><span>       承載 Guest buffer 的 Host RAM
</span></span></code></pre></td></tr></table>
</div>
</div><p>建立這條路徑時，QEMU 提供記憶體區域資訊，核心與驅動依所選後端建立、維護映射及存取權限。映射必須涵蓋 ring 與必要的 payload buffers，而不只是 Virtqueue metadata。核心 vhost-vdpa 與 VFIO / IOMMUFD 是不同的整合路徑，不能把一套介面的能力直接套用到另一套。</p>
<p>在 QEMU 的 vhost-vdpa 實作中，可以從 <code>vhost_vdpa_dma_map()</code> 看見這個交接：QEMU 透過 vhost-vdpa file descriptor 傳送 IOTLB 更新訊息，內容包含 IOVA、對應的 HVA、長度及讀寫權限。這不是把 HVA 直接放上 PCIe 匯流排，而是讓核心與裝置驅動建立正確的 DMA 映射。撤銷時則要送出 invalidation，並確保裝置不會再使用舊映射。</p>
<p>QEMU 透過 <code>MemoryListener</code> 的 <code>region_add</code> / <code>region_del</code> 等回呼掌握位址空間變更；若涉及 Guest vIOMMU，還需要處理對應的映射通知。因此，映射不是只在 VM 啟動時「一次寫好就永遠不變」。另外，QEMU 中的軟體 IOTLB 訊息、裝置的位址轉換快取與平台 IOMMU 雖然相關，並不是同一個物件。</p>
<p>映射建立後，硬體就能讀取描述符，直接讀出 TX buffer，或把收到的封包寫入 RX buffer。相關頁面的生命週期通常必須依後端要求固定，避免裝置繼續使用失效的映射，這就是 memory pinning 所要解決的問題。Hugepages 能降低頁表與 IOTLB（I/O 位址轉換快取）壓力，但不是所有 vDPA 實作的強制要求；<code>ulimit -l</code> 設定的是記憶體鎖定資源限制，不是 QEMU 用來執行 pinning 的系統呼叫。</p>
<p>以 64 GiB 記憶體為例，若分別用 4 KiB、2 MiB、1 GiB 的粒度表示，數量依序是 16,777,216、32,768 與 64。這說明大頁為什麼可能減少轉換壓力，但實際能否使用大粒度 DMA 映射，仍受實體配置、對齊、IOMMU 與驅動支援影響；使用 Hugepages 不保證所有映射都能合併，也不保證 IOTLB 能容納整台 VM 的工作集合。</p>
<h3 id="dma-之外通知仍有成本">DMA 之外，通知仍有成本</h3>
<p>資料不經 CPU 逐位元組搬運，並不表示所有通知也自動消失。Guest 的 Kick、裝置的 Call，以及 Host 排程與管理路徑，仍可能需要軟體參與。</p>
<p>這也是 MSI / MSI-X 與 Interrupt Posting 容易被混淆的地方。**MSI / MSI-X 是 PCIe 裝置發出中斷的機制，Interrupt Posting 則是虛擬化平台加速遞送中斷的機制。**以 Intel 平台為例，裝置發出 MSI / MSI-X 記憶體寫入訊息後，中斷重映射硬體可依路由設定更新目標 vCPU 的 Posted-Interrupt Descriptor，再配合 CPU 虛擬中斷能力，減少軟體介入。</p>
<p>把這段再往下拆，可以看到三種不同層次的工作：</p>
<ol>
<li><strong>裝置發出訊息。</strong> MSI / MSI-X 使用已設定的 address / data 發出 PCIe 記憶體寫入。MSI-X 提供多個可個別設定及遮蔽的向量，適合多佇列裝置，但不代表一個向量必然對應一個 queue。</li>
<li><strong>平台決定路由。</strong> 中斷重映射硬體依裝置身分與路由表判斷投遞目標；在 posted-interrupt 路徑中，可以更新記憶體中 Posted-Interrupt Descriptor 的 PIR（Pending Interrupt Requests）位元，並在必要時產生通知事件。</li>
<li><strong>CPU 遞送虛擬中斷。</strong> 配合 APIC virtualization / virtual interrupt delivery 等能力，在受支援的執行條件下，CPU 可以把 pending interrupt 遞送給 Guest，減少傳統軟體注入所需的 VM-exit。</li>
</ol>
<p>裝置仍使用設定好的 MSI / MSI-X 訊息，不必自行理解 vCPU 的 PIR，也不能把這描述成網卡必須產生另一種通用的「Posted MSI」封包格式。上述是 Intel 平台的說明，不能直接套用到所有 CPU 架構。</p>
<p>而在軟體通知路徑中，<code>ioeventfd</code> 讓 KVM 在匹配到 Guest 的 I/O 寫入時通知 eventfd，後端可據此得知有新工作；<code>irqfd</code> 則讓 eventfd 事件觸發 Guest 中斷注入。它們能減少 QEMU 逐次處理通知的需求，但 eventfd 不是一條必然直通硬體 doorbell 的實體線，也不等於 posted interrupt。</p>
<p>但這需要整條平台與後端路徑支援；vCPU 未執行、需要被喚醒，或通知走 eventfd / irqfd 路徑時，仍可能需要 Host 軟體。Kick 也不是裝置向 Guest 發出的硬體中斷，不能與 MSI-X 畫上等號。</p>
<p>因此，硬體 vDPA 比較準確的效益是「降低 Host 資料面 CPU 成本」，而不是「Host CPU 使用率歸零」；Guest vCPU 本身也仍然使用實體 CPU。</p>
<p>到這裡，穩態傳輸的架構已經完整：軟體先設定，硬體再自主處理。但這種自主性，也正是遷移時的新問題。</p>
<h2 id="五熱遷移的難題誰改寫了正在搬運的記憶體">五、熱遷移的難題：誰改寫了正在搬運的記憶體?</h2>
<p>如果 VM 已經停止，複製 RAM 相對單純。<strong>Pre-copy</strong> 則希望 VM 在大部分搬運期間繼續執行：先複製一輪 RAM，再把這段期間重新被修改的頁面補送，反覆進行，最後才停止來源端，傳送剩餘資料與 CPU / 裝置狀態。</p>
<p>因此，遷移系統不只要知道「哪些頁面已送過」，還要知道「送過之後，哪些頁面又變了」。這就是 <strong>Dirty Page Tracking（髒頁追蹤）</strong>。</p>
<p>關鍵不在於只選一種資料結構，而是先找出所有寫入者：</p>
<table>
	<thead>
			<tr>
					<th>寫入來源</th>
					<th>典型追蹤路徑</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Guest vCPU</td>
					<td>KVM dirty logging，例如 bitmap 或 dirty ring</td>
			</tr>
			<tr>
					<td>QEMU 裝置模型</td>
					<td>QEMU 記憶體 API 與顯式 dirty marking</td>
			</tr>
			<tr>
					<td>外部軟體後端或裝置 DMA</td>
					<td>後端 dirty log、SVQ，或受支援的裝置 / IOMMU dirty tracking</td>
			</tr>
	</tbody>
</table>
<p>這三條路徑最後必須整合成遷移所需的記憶體變更資訊。<strong>Guest vCPU 的髒頁追蹤，不會自動涵蓋 QEMU 或裝置對 Guest RAM 的寫入。</strong></p>
<h3 id="bitmap-與-dirty-ring記錄格式不等於收集機制">Bitmap 與 dirty ring：記錄格式不等於收集機制</h3>
<p>最直覺的記錄方式是 dirty bitmap：一個 bit 對應一個追蹤粒度的頁面，設為 <code>1</code> 表示需要重送。以 64 GiB RAM、4 KiB 頁面為例，共有 16,777,216 頁，bitmap 大約只需要 2 MiB。</p>
<p>KVM 可以透過 <code>KVM_MEM_LOG_DIRTY_PAGES</code> 啟用追蹤，再由 <code>KVM_GET_DIRTY_LOG</code> 等介面同步。底層如何發現寫入，則可能依靠寫保護 fault、硬體 dirty bit 或 Intel PML（Page Modification Logging）等機制。Bitmap 是回報格式，並不代表底層只能採用某一種攔截方式。</p>
<p>例如，以寫保護為基礎的追蹤，可以理解成以下循環：</p>
<ol>
<li>QEMU 為對應的 KVM memory slot 啟用 dirty logging。</li>
<li>KVM 在適當的粒度與時機，讓尚未記錄的頁面以寫保護方式接受追蹤；在 x86 上，這涉及 EPT / NPT 等第二階段位址轉換機制。</li>
<li>Guest vCPU 嘗試寫入時觸發 fault，由 KVM 標記對應頁面為 dirty，並按追蹤策略允許後續寫入繼續。</li>
<li>QEMU 收集 dirty bitmap，把這些頁面加入自己的遷移待送集合。</li>
<li>KVM 依所使用的 API 與能力清除記錄、重新建立保護，開始下一輪追蹤。</li>
</ol>
<p>Bitmap 通常可用 <code>unsigned long</code> 陣列等形式儲存，但 API 是以 memory slot 為單位回報，不應把它想成所有 vCPU 每次寫入都必須持有同一把全域鎖。<code>KVM_GET_DIRTY_LOG</code> 的清除行為也會受 manual dirty-log protect 能力影響；使用相應模式時，需要配合 <code>KVM_CLEAR_DIRTY_LOG</code>，不能一律說「讀取 bitmap 就完成全部重新保護」。</p>
<p>如果使用 Intel PML，硬體會把相關 EPT dirty 狀態變化所對應的 Guest 實體頁位址，記錄到每個 vCPU 使用的 PML buffer。這個硬體 buffer 可容納 512 筆位址，滿時會觸發 VM-exit，由 KVM 收集紀錄。因此，PML 能降低逐頁寫保護攔截的頻率，卻不是整個追蹤期間完全沒有 VM-exit；PML buffer 也不等於 QEMU 可直接讀取的 KVM dirty ring。</p>
<p>另一種介面是 <strong>KVM dirty ring</strong>。它以每個 vCPU 的 <code>struct kvm_dirty_gfn</code> 陣列回報髒頁，其中 <code>slot</code> 識別記憶體 slot，<code>offset</code> 是相對該 slot 的頁面偏移，<code>flags</code> 則表示 DIRTY、RESET 等條目狀態。KVM 產生條目，使用者空間收集後依協定標記，再透過 <code>KVM_RESET_DIRTY_RINGS</code> 回收並重新建立追蹤。</p>
<p>其 UAPI 結構如下，單筆為 16 bytes：</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-4-1"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-4-1">1</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-4-2"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-4-2">2</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-4-3"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-4-3">3</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-4-4"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-4-4">4</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-4-5"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-4-5">5</a>
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#66d9ef">struct</span> kvm_dirty_gfn {
</span></span><span style="display:flex;"><span>    __u32 flags;
</span></span><span style="display:flex;"><span>    __u32 slot;
</span></span><span style="display:flex;"><span>    __u64 offset;
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></td></tr></table>
</div>
</div><p>在這裡，<code>slot</code> 按 API 編碼包含 address-space / slot 識別；<code>offset</code> 不是任意全域 GPA，而是相對該 slot 起點的頁面偏移。例如，某個 slot 起於 GPA <code>0x40000000</code>，頁面大小為 4 KiB，<code>offset = 3</code> 就對應該 slot 中 GPA <code>0x40003000</code> 的頁面。</p>
<p>使用者空間先查詢 <code>KVM_CAP_DIRTY_LOG_RING</code> 或 <code>KVM_CAP_DIRTY_LOG_RING_ACQ_REL</code> 等能力，再於建立 vCPU 之前透過 <code>KVM_ENABLE_CAP</code> 啟用。Ring 大小以 bytes 指定，必須符合 API 要求；例如 64 KiB 可以容納 4096 筆條目。建立 vCPU 後，使用者空間透過 mmap 存取對應的 per-vCPU ring，並為需要追蹤的 memory slots 設定 <code>KVM_MEM_LOG_DIRTY_PAGES</code>。</p>
<p>條目的生命週期可以畫成：</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-5-1"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-5-1">1</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-5-2"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-5-2">2</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-5-3"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-5-3">3</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-5-4"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-5-4">4</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-5-5"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-5-5">5</a>
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>未使用（flags = 00）
</span></span><span style="display:flex;"><span>  -&gt; KVM 記錄髒頁（01，DIRTY）
</span></span><span style="display:flex;"><span>  -&gt; 使用者空間依序收集，設 RESET bit（1X，HARVESTED）
</span></span><span style="display:flex;"><span>  -&gt; KVM_RESET_DIRTY_RINGS：重新保護頁面並回收條目
</span></span><span style="display:flex;"><span>  -&gt; 未使用（00）
</span></span></code></pre></td></tr></table>
</div>
</div><p>「收集」是把頁面編號納入待遷移集合，不是等 RAM 傳完才設 RESET。依 KVM API，必須先呼叫 <code>KVM_RESET_DIRTY_RINGS</code> 重新建立追蹤，再讀取相關頁面內容；否則頁面在複製期間再次被寫入時，可能漏掉下一輪變更。條目也必須依序收集，不能跳過中間一筆直接處理後面一筆。</p>
<p>Per-vCPU 設計可降低共享資料競爭，但生產者與消費者仍需遵守原子操作及記憶體排序規則。Ring 滿時，KVM 可用 <code>KVM_EXIT_DIRTY_RING_FULL</code> 要求使用者空間收集與回收；這不等於自動替遷移限速並保證收斂。收集一筆條目，也不代表 QEMU 當下就把那一頁傳出去。</p>
<p>尤其在弱記憶體排序架構上，讀取及更新 <code>flags</code> 必須遵守 acquire / release 或對應 memory barrier 規則。要取得最後完整的髒頁集合，還必須確保 vCPU 已退出 <code>KVM_RUN</code>，讓尚留在處理器 dirty buffers 裡的記錄同步到 ring，而不是只掃一次 mmap 區域就認定追蹤已結束。</p>
<p>同樣地，dirty ring 與 PML 分屬回報介面及硬體收集機制，兩者不是同一件事，也不互為必要條件。</p>
<table>
	<thead>
			<tr>
					<th>比較項目</th>
					<th>Dirty bitmap</th>
					<th>KVM dirty ring</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>主要記錄單位</td>
					<td>每頁一個 bit，依 memory slot 組織</td>
					<td>每筆一個 slot / offset 與狀態</td>
			</tr>
			<tr>
					<td>使用者空間收集</td>
					<td>取得並處理 bitmap</td>
					<td>依序收集已產生的條目</td>
			</tr>
			<tr>
					<td>空間與工作量</td>
					<td>隨追蹤頁數增加，掃描成本與收集方式有關</td>
					<td>Ring 容量隨 vCPU 數與設定大小增加，處理成本也受髒化事件數影響</td>
			</tr>
			<tr>
					<td>壓力來源</td>
					<td>大記憶體範圍的同步與掃描</td>
					<td>Ring 滿造成的退出、收集與重設</td>
			</tr>
			<tr>
					<td>是否保證遷移收斂</td>
					<td>否</td>
					<td>否</td>
			</tr>
	</tbody>
</table>
<p>兩者不必然完全互斥。部分架構可透過 <code>KVM_CAP_DIRTY_LOG_RING_WITH_BITMAP</code>，另外用 bitmap 補足不在 vCPU / ring context 中產生的 KVM 髒頁；這仍不是自動攔截任意 QEMU 寫入或外部裝置 DMA 的機制。</p>
<h3 id="從髒頁回報到-qemu-遷移">從髒頁回報到 QEMU 遷移</h3>
<p>KVM 的紀錄只是來源之一。QEMU 還要把 KVM、裝置模型與後端回報整合到自己的 RAM 遷移追蹤。<code>MemoryListener</code> 可用來協調記憶體區域及 dirty logging 的啟停、同步等事件，相關回呼包含 <code>log_start</code>、<code>log_stop</code>、<code>log_sync</code>；但它不是對每次 Host 記憶體寫入都自動觸發的監聽器。</p>
<p>對 QEMU 裝置模型而言，必須透過正確的記憶體 API 或顯式 dirty marking 回報寫入；外部 vhost 後端也需要使用約定的 dirty-log 機制。這些紀錄同步後，遷移執行緒才知道哪些 RAM 頁需要排入下一輪傳輸。</p>
<p>Pre-copy 能否收斂，則取決於髒化速度與有效傳輸速度。如果 VM 持續修改記憶體的速度接近或超過遷移通道能補送的速度，換成 dirty ring 也不會自動解決；平台仍需調整頻寬、停頓目標、節流或遷移策略。</p>
<h3 id="dma-留下的缺口">DMA 留下的缺口</h3>
<p>當 vDPA 網卡直接把封包寫入 Guest RX buffer，寫入者不是 Guest vCPU，不能期待 KVM 的 vCPU dirty logging 自動捕捉它。Host CPU 由 QEMU 寫入 Guest RAM，也不會因此自動進入 Guest 的 PML 或寫保護追蹤路徑。</p>
<p>裝置若能回報 DMA 修改過的頁面，或者整條軟硬體路徑支援 IOMMU dirty tracking，就能補上這個缺口。但「有 IOMMU」、「使用 VFIO」或「使用 IOMMUFD」都不是 DMA dirty tracking 已可用的保證，更不能推論 vhost-vdpa 能直接使用其他後端的能力。</p>
<p>如果沒有適用的硬體追蹤能力，還有一個思路：既然 QEMU 不必搬運封包本身，那麼能不能只讓它掌握「哪些 buffer 交給裝置，以及何時完成」?</p>
<p>這就是 SVQ 的切入點。</p>
<h2 id="六shadow-virtqueue接回描述符不必接回整個封包">六、Shadow Virtqueue：接回描述符，不必接回整個封包</h2>
<p>**Shadow Virtqueue（SVQ）**是 QEMU 管理的影子佇列。啟用後，硬體不再直接以 Guest VQ 作為 ring metadata 的處理對象，而改為處理 QEMU 提供的 SVQ。QEMU 則負責解析 Guest 提交的工作、建立後端描述符，並把完成結果對應回 Guest。</p>
<p>乍看之下，這像是把整條資料路徑拉回 QEMU。實際上，必須把描述符與 payload 分開畫：</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-6-1"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-6-1">1</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-6-2"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-6-2">2</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-6-3"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-6-3">3</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-6-4"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-6-4">4</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-6-5"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-6-5">5</a>
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>描述符與完成回報：
</span></span><span style="display:flex;"><span>Guest VQ &lt;-&gt; QEMU（解析、轉換、維護對照）&lt;-&gt; SVQ &lt;-&gt; vDPA 裝置
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>封包內容：
</span></span><span style="display:flex;"><span>Guest payload buffer &lt;============= DMA =============&gt; vDPA 裝置
</span></span></code></pre></td></tr></table>
</div>
</div><p>**硬體改讀寫 SVQ 的 ring metadata，不等於封包先寫進 SVQ，再由 QEMU 複製到 Guest。**一般資料佇列中，SVQ 的描述符仍可指向 Guest 原本的 payload buffer，硬體依然直接 DMA 讀寫那塊記憶體。</p>
<h3 id="用一次-tx-與-rx-看清楚兩條路徑">用一次 TX 與 RX 看清楚兩條路徑</h3>
<p>假設 Guest 要送出一個 1500 bytes 的封包。Guest 驅動把封包放在自己的 TX buffer，再把描述符提交到 Guest VQ。QEMU 解析這個工作，為 SVQ 建立對應描述符，必要時轉換成後端使用的 IOVA，並記錄 shadow descriptor 與 Guest element 的對照。硬體從 SVQ 得知工作位置後，直接 DMA 讀取 Guest TX buffer；完成時更新 SVQ，再由 QEMU 回報給 Guest。</p>
<p>接收方向的原理相同，只是 buffer 的方向相反：Guest 先提供可供裝置寫入的 RX buffer，QEMU 將對應描述符提交給 SVQ，硬體收到封包後直接 DMA 寫入 Guest RX buffer，再更新 SVQ 的完成資訊。</p>
<table>
	<thead>
			<tr>
					<th>階段</th>
					<th>TX：Guest → 網路</th>
					<th>RX：網路 → Guest</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Guest 提交</td>
					<td>已有內容、供裝置讀取的 buffer</td>
					<td>供裝置寫入的 buffer</td>
			</tr>
			<tr>
					<td>QEMU 中介</td>
					<td>建立 shadow 描述符與對照</td>
					<td>建立 shadow 描述符與對照</td>
			</tr>
			<tr>
					<td>硬體處理 payload</td>
					<td>DMA 讀取 Guest buffer</td>
					<td>DMA 寫入 Guest buffer</td>
			</tr>
			<tr>
					<td>QEMU 收尾</td>
					<td>回報 Guest 完成狀態</td>
					<td>標記可寫入 buffer 的髒頁，再回報完成</td>
			</tr>
	</tbody>
</table>
<p>所以，TX 與 RX 都可以避免 <strong>QEMU 額外的 payload memcpy</strong>。這項性質限定在這條資料路徑，並不是宣稱 Guest 網路協定棧或整條網路從未複製資料；控制佇列及特殊配置也可能採取不同處理。</p>
<p>SVQ 的工作也不只是「複製一個指針」。單個 Split descriptor 雖然是 16 bytes，但一個封包可能跨多個描述符、間接表及 buffers；QEMU 還要處理描述符重建、位址轉換與完成狀態。Packed Ring 的結構不同，其 SVQ 相容性須按版本確認。因此，即使不複製 payload，中介仍有 CPU 與延遲成本。</p>
<h3 id="描述符轉換與完成回收在程式裡如何對應">描述符轉換與完成回收，在程式裡如何對應?</h3>
<p>以 QEMU <code>hw/virtio/vhost-shadow-virtqueue.c</code> 的 Split Ring 路徑為例，可以把提交過程拆成四個動作：</p>
<ol>
<li><strong>取得 Guest 工作。</strong> QEMU 透過 <code>virtqueue_pop()</code> 解析描述符鏈，建立 <code>VirtQueueElement</code>。其中 <code>out_sg</code> 是裝置可讀的 scatter-gather buffers，<code>in_sg</code> 是裝置可寫的 buffers；方向以裝置為準。</li>
<li><strong>轉換位址。</strong> <code>vhost_svq_add()</code> 及其內部路徑查找 <code>VhostIOVATree</code> 維護的映射，把 Guest backing 或 QEMU buffer 對應成裝置可用的 IOVA，並檢查範圍是否合法。</li>
<li><strong>重建 shadow 描述符。</strong> QEMU 在 SVQ 配置自己的描述符鏈，記錄 <code>desc_state[head].elem</code> 與佔用的描述符數量，再以正確的 memory barrier 發布 available index，必要時 Kick 裝置。</li>
<li><strong>依完成 ID 找回原工作。</strong> 裝置更新 SVQ used ring 後，<code>vhost_svq_get_buf()</code> 讀取完成 ID 與長度，透過 <code>desc_state</code> 找回 Guest element，回收 shadow 描述符；後續由 <code>virtqueue_fill()</code> / <code>virtqueue_flush()</code> 等路徑更新 Guest 完成狀態。</li>
</ol>
<p>因此，Guest 的 descriptor head ID 與 SVQ 的 head ID 不必相同，兩側的 available / used index 也不必有相同數值。QEMU 靠明確的對照與各自的進度管理連接兩者，而不是假設兩張表可以逐格原樣複製。</p>
<p>例如，Guest RX buffer 位於 GPA <code>0x12345000</code>，SVQ 為它配置的 IOVA 可能是 <code>0x40000000</code>。硬體讀到的是後者，再經映射抵達同一塊 Guest backing memory；完成時回報的則是 shadow 描述符 ID，由 QEMU 轉回原本的 Guest element。這三種資訊——Guest 位址、裝置 DMA 位址與描述符 ID——各有用途，不能混為同一個「指針」。</p>
<p>此外，GPA 連續不保證 QEMU 的 HVA scatter-gather 區段也連續，同一個 Guest 工作可能需要較多 shadow 描述符。SVQ 若暫時沒有足夠空間，<code>vhost_svq_add()</code> 可以回報 <code>-ENOSPC</code>，QEMU 保留待提交工作，待裝置釋放描述符後再繼續。這也是不複製 payload 仍可能遇到背壓與排程成本的原因。</p>
<p>通知路徑則由原本的 Guest / 後端兩端互動，改成四個邏輯端點：</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-7-1"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-7-1">1</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-7-2"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-7-2">2</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-7-3"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-7-3">3</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-7-4"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-7-4">4</a>
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Guest Kick -&gt; QEMU 的 svq_kick
</span></span><span style="display:flex;"><span>QEMU 發布工作 -&gt; hdev_kick -&gt; vDPA 後端
</span></span><span style="display:flex;"><span>vDPA 完成工作 -&gt; hdev_call -&gt; QEMU
</span></span><span style="display:flex;"><span>QEMU 回報 Guest -&gt; svq_call -&gt; Guest 通知路徑
</span></span></code></pre></td></tr></table>
</div>
</div><p>這些名稱對應 QEMU 中的 event notifiers，不代表每個工作都會產生四次系統呼叫；通知仍可批次或抑制。SVQ 也不必然使用專屬的 busy-poll worker：程式可透過事件處理與完成回收運作，不能直接把它等同於 DPDK 的輪詢模型。</p>
<h3 id="不碰-payload怎麼知道哪一頁髒了">不碰 payload，怎麼知道哪一頁髒了?</h3>
<p>答案在 QEMU 維護的對照關係。它知道哪些 Guest 記憶體範圍以可寫入 buffer 的形式交給後端；當硬體回報 SVQ 工作完成，QEMU 就能找回對應範圍，透過 VirtIO / 記憶體處理路徑顯式標記髒頁，再更新 Guest used 狀態。</p>
<p>這不是 DMA 觸發了 KVM dirty ring，也不是 QEMU 把 DMA 寫入「變成」vCPU 寫入，而是<strong>軟體根據工作語意與完成資訊，補上遷移系統原本看不到的變更</strong>。</p>
<p>這種追蹤可能依 buffer 或頁面範圍保守標記，不保證精確到每個實際寫入的 byte。另外，TX payload 的 DMA read 雖然不會弄髒記憶體，但 TX 完成時更新 Guest used ring 仍然是寫入，同樣必須追蹤。</p>
<p>例如，Guest 提供一塊 4 KiB RX buffer，裝置只寫入其中約 1500 bytes，遷移仍以頁面為單位記錄；若可寫範圍跨頁，則可能涉及不只一頁。完成長度與實作的 unmap / dirty-marking 語意會影響標記範圍，不能由封包長度直接推論「恰好只髒一頁」。</p>
<p>更重要的是，尚未完成的 RX 工作可能已開始 DMA，但 QEMU 尚未收到 used 回報。最終切換時，必須先讓裝置停止在受支援的邊界、處理剩餘工作與必要的 dirty marking，才能宣告最後的 RAM 狀態完整；只收集已收到的 Call，並不足以證明沒有未結算的 DMA。</p>
<p>SVQ 因而取得一個有用的平衡：讓硬體繼續搬運大量 payload，讓軟體重新掌握工作與記憶體變更的關係。然而，「看得到完成」還不夠；真正交接時，尚未完成的工作也必須有明確歸屬。</p>
<h2 id="七從追蹤到交接熱遷移如何安全切換">七、從追蹤到交接：熱遷移如何安全切換?</h2>
<p>把前面的機制放回 pre-copy 流程，就能看到 SVQ 所在的位置：</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-8-1"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-8-1">1</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-8-2"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-8-2">2</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-8-3"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-8-3">3</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-8-4"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-8-4">4</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-8-5"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-8-5">5</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-8-6"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-8-6">6</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-8-7"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-8-7">7</a>
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>來源端正常執行
</span></span><span style="display:flex;"><span>  -&gt; 啟用髒頁追蹤，必要時啟用 SVQ
</span></span><span style="display:flex;"><span>  -&gt; 背景複製 RAM，反覆補送髒頁
</span></span><span style="display:flex;"><span>  -&gt; Stop-and-copy：停止來源端 vCPU，安全停止裝置資料面
</span></span><span style="display:flex;"><span>  -&gt; 結算完成資訊，傳送最後髒頁與 CPU / 裝置狀態
</span></span><span style="display:flex;"><span>  -&gt; 目的端載入狀態，建立本地後端映射與通知路徑
</span></span><span style="display:flex;"><span>  -&gt; 目的端接管執行，網路轉送位置隨之更新
</span></span></code></pre></td></tr></table>
</div>
</div><p>SVQ 若用來補足遷移期間的 DMA 髒頁追蹤，就必須在需要追蹤的期間發揮作用，而不是等到最後幾毫秒才介入。實際上，SVQ 可能動態啟用，也可能常駐；目的端是否先使用 SVQ、何時切回直接資料面，同樣取決於版本、後端能力與配置。</p>
<h3 id="切換不是只改一個地址">切換不是只改一個地址</h3>
<p>不論從直接資料面切到 SVQ，或反向切回，都需要讓舊路徑停止在可理解的邊界，再讓新路徑接續。概念上必須滿足四個條件：</p>
<ol>
<li><strong>穩定舊路徑。</strong> 停止提交新工作，依後端能力 suspend / stop，確認裝置不會再存取即將撤銷的映射。</li>
<li><strong>結算既有工作。</strong> 收集已完成結果，釐清未消費與 in-flight 描述符的所有權及進度，避免遺漏或重複處理。</li>
<li><strong>建立新路徑。</strong> 配置 ring 位址、佇列狀態、DMA 映射，以及 Kick / Call 通道。</li>
<li><strong>驗證後恢復。</strong> 確認設定成功，再恢復後端，處理切換期間累積的工作與通知。</li>
</ol>
<p>相關介面可能包括 <code>VHOST_GET_VRING_BASE</code>、<code>VHOST_SET_VRING_ADDR</code>、<code>VHOST_SET_VRING_BASE</code> 與受支援的 <code>VHOST_VDPA_SUSPEND</code>。但這不是所有版本通用的固定 ioctl 腳本，<code>vring_base</code> 也不是涵蓋完整裝置狀態的單一計數器。</p>
<p>把各介面的責任列開，就能看見為什麼「重新映射」本身不足以完成切換：</p>
<table>
	<thead>
			<tr>
					<th>介面或狀態</th>
					<th>主要用途</th>
					<th>不能由此推論的事</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>VHOST_VDPA_SUSPEND</code></td>
					<td>在後端支援時，讓裝置資料面進入可停止的狀態</td>
					<td>不代表所有已發布工作都已完成</td>
			</tr>
			<tr>
					<td><code>VHOST_GET_VRING_BASE</code></td>
					<td>依後端契約取得佇列處理進度</td>
					<td>不等於取得全部裝置狀態，也不能盲目信任未穩定的進度</td>
			</tr>
			<tr>
					<td><code>VHOST_SET_VRING_ADDR</code></td>
					<td>設定 descriptor / available / used 區域的位址</td>
					<td>不會自動建立所有 DMA 映射或同步佇列進度</td>
			</tr>
			<tr>
					<td><code>VHOST_SET_VRING_BASE</code></td>
					<td>設定後端使用的佇列起始進度</td>
					<td>不是把整條 ring 或 payload 寫入晶片</td>
			</tr>
			<tr>
					<td><code>VHOST_SET_VRING_KICK</code> / <code>VHOST_SET_VRING_CALL</code></td>
					<td>綁定通知 file descriptors</td>
					<td>不等於啟用端到端硬體中斷直通</td>
			</tr>
			<tr>
					<td><code>VHOST_VDPA_SET_STATUS</code>、queue enable 等設定</td>
					<td>依裝置狀態機完成重設、啟動等生命週期操作</td>
					<td>不是任意順序設定後都能安全恢復</td>
			</tr>
	</tbody>
</table>
<p>在 QEMU 的 SVQ 啟動路徑中，<code>vhost_svq_start()</code> 建立影子 ring 及描述符對照，<code>vhost_vdpa_svq_map_rings()</code> 再替這些記憶體配置裝置可用的 IOVA。裝置需要讀取的 descriptor / available 區域可以用唯讀映射，裝置要更新的 used 區域則需要可寫映射；Guest payload 的合法 DMA 映射仍然保留。最後設定到後端的 ring 位址，才會指向這組 shadow metadata。</p>
<p>這裡的 <code>VhostIOVATree</code> 是 QEMU 用來管理與查找映射的軟體資料結構，不是直接替代硬體 IOMMU 的頁表。SVQ 自己的 ring 可從零開始計數，Guest 的工作進度則由 QEMU 的 VirtQueue 狀態維持；因此不必把硬體先前的所有 index 原樣複製到新 SVQ。</p>
<p>反向停用時，程式需要把通知重新接回直接資料面。在支援動態切換的實作中，<code>shadow_vqs_enabled</code> 與 <code>SVQ_TSTATE_DISABLING</code> 等狀態可協助區分過渡期，例如在旗標尚未完全清除前，先把 Call 的目的地切回 Guest notifier。但這些是版本相關的內部狀態，不應被當成穩定的使用者 API。</p>
<p>尤其要注意，<strong>停止裝置後，不能假設所有未完成工作還會自行完成</strong>，所以安全收尾不等於「掛起後一直等到 ring 全空」。同樣地，遮蔽 Kick 只是改變通知，不會自動阻止 Guest 修改記憶體，也不一定阻止後端消費已發布的工作；只改 IOMMU 映射，也無法取代佇列進度、描述符對照與通知狀態的同步。</p>
<p>例如，QEMU 的網路 SVQ 停止路徑可先轉交已完成的 used entries，再對尚未完成的 Guest elements 執行 <code>virtqueue_unpop()</code> 等處理，把工作退回 Guest 佇列的處理邊界，而不是要求每個 RX buffer 都必須先收到封包。這類處理依賴裝置已安全停止及網路後端的語意，不能直接套用到任意儲存或其他 VirtIO 裝置。</p>
<p>可以用下面三種狀態檢查切換是否完整：</p>
<table>
	<thead>
			<tr>
					<th>工作所在狀態</th>
					<th>切換時需要釐清的事</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>已發布在 Guest VQ，尚未交給後端</td>
					<td>保留為待處理工作，避免新路徑漏掉已發布但未通知的項目</td>
			</tr>
			<tr>
					<td>已交給後端，尚未回報完成</td>
					<td>確認是否已消費、是否可能有 DMA 或外部副作用，按契約保存、退回或接續</td>
			</tr>
			<tr>
					<td>後端已完成，Guest 尚未看到 used 回報</td>
					<td>完成 dirty marking 與 used 狀態同步，避免新路徑重做</td>
			</tr>
	</tbody>
</table>
<p>這說明「新 SVQ 是空的，所以不用處理舊工作」也不成立：要切進一組空佇列，仍然必須結算舊資料面的所有權。</p>
<h3 id="來源端與目的端不是鏡像操作">來源端與目的端不是鏡像操作</h3>
<p>來源端原本正在執行，必須主動停止並穩定狀態；目的端通常已在 incoming 準備狀態，尚未執行 Guest，工作是接收、載入與恢復。跨主機遷移傳遞的是可恢復的裝置狀態，不是把來源端的本地 DMA 映射原封不動套到另一台主機。</p>
<p>也因此，恢復資料面不應想成「把整條 SVQ 塞回晶片」。Ring 與 payload 通常在 RAM 中，硬體透過 DMA 存取；真正需要設定或恢復的，是裝置的佇列位址、進度、功能、映射與通知等狀態。目的端必須讓本地硬體設定與載入的 Guest 記憶體一致，才有條件接管。</p>
<table>
	<thead>
			<tr>
					<th>階段</th>
					<th>來源端</th>
					<th>目的端</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>背景複製</td>
					<td>VM 持續執行，回報髒頁</td>
					<td>建立 incoming VM，接收 RAM 與可提前準備的資源</td>
			</tr>
			<tr>
					<td>Stop-and-copy</td>
					<td>停止 vCPU 及相關資料面，收集最後的變更與裝置狀態</td>
					<td>保持 Guest 尚未執行，載入最終狀態</td>
			</tr>
			<tr>
					<td>準備接管</td>
					<td>保持停止，依協定等待或完成交接</td>
					<td>建立本地 DMA 映射、佇列與通知，確認可恢復</td>
			</tr>
			<tr>
					<td>接管後</td>
					<td>不再作為可直接恢復的最新執行個體</td>
					<td>開始執行 Guest，承擔後續狀態與外部副作用</td>
			</tr>
	</tbody>
</table>
<p>這個最後交接的狀態不只包含網卡，還有 CPU 暫存器、虛擬中斷控制器、其他裝置及 RAM。因此，在 QEMU 的生命週期中可能看到 <code>vm_stop()</code>、<code>vm_start()</code> 或 vhost 後端 stop / start 等操作，但它們是不同層次的協調，不代表一個函式呼叫就能原子地凍結整台主機。</p>
<h3 id="安全交接不等於零停頓或零丟包">安全交接，不等於零停頓或零丟包</h3>
<p>傳統 QEMU/KVM live migration 通常仍有最後的 stop-and-copy 與接管窗口；SVQ 模式切換也可能另外造成短暫 I/O pause。但以下幾種觀測值不是同一件事：</p>
<table>
	<thead>
			<tr>
					<th>指標</th>
					<th>觀察的是什麼</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>VM downtime</td>
					<td>Guest vCPU 暫停執行的時間</td>
			</tr>
			<tr>
					<td>I/O pause</td>
					<td>裝置暫停處理工作的時間</td>
			</tr>
			<tr>
					<td>Network jitter</td>
					<td>封包延遲的變動</td>
			</tr>
			<tr>
					<td>Packet loss</td>
					<td>因緩衝區不足、路徑收斂或故障等原因遺失的封包</td>
			</tr>
	</tbody>
</table>
<p>裝置切換、VM 交接與網路重導向可能出現在不同時間點，不能把一次遷移固定描述成「恰好兩次卡頓」。停頓長度也取決於 RAM 髒化速度、頻寬、佇列數、裝置狀態、映射與網路收斂，不能只憑 vDPA 或 SVQ 名稱給出通用的毫秒數。硬體能降低穩態 CPU 成本，也不代表遷移切換必然比軟體後端更快。</p>
<p>緩衝區可以吸收部分停頓，但空間有限，因此不能保證零丟包。TCP 遇到短暫停頓時，RTO 到期通常先觸發重傳與退避，不是立即斷線；持續沒有進展，才可能依重試策略或 <code>TCP_USER_TIMEOUT</code> 終止連線。TCP keepalive 通常也需要明確啟用，並非每條閒置連線都會自動探測。</p>
<p>更重要的是，應用程式的 RPC deadline、心跳或連線池 timeout，可能遠短於 TCP 自己的放棄時間。即使 TCP 連線仍存活，應用請求也可能早已失敗；RST、程序退出或 NAT / 防火牆狀態變化，也可能直接中斷連線。因此，評估遷移品質時，必須把「VM 沒重開機」、「TCP 還活著」與「服務沒有違反延遲目標」分開。</p>
<h3 id="tcp-的幾種-timeout分別在等什麼">TCP 的幾種 timeout，分別在等什麼?</h3>
<p>對一條已建立且仍有資料待確認的 TCP 連線，收不到 ACK 可能觸發 RTO。重傳間隔會依協定與實作進行退避，並受到上限等條件影響，不是每條連線都固定依 <code>1、2、4、8</code> 秒開始計算。初始 RTO 與已取得 RTT 樣本後的 RTO 也不同，不能拿某個最小值當成所有連線的安全停頓時間。</p>
<p>Linux 常見的控制項如下；實際值可能因核心版本、namespace、發行版或應用程式設定而改變：</p>
<table>
	<thead>
			<tr>
					<th>參數或選項</th>
					<th>控制內容</th>
					<th>常見預設與注意事項</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>net.ipv4.tcp_retries2</code></td>
					<td>已建立連線持續重傳時的放棄策略</td>
					<td>常見預設為 <code>15</code>，不是固定「15 秒」或所有情境下精確重傳 15 次就斷線</td>
			</tr>
			<tr>
					<td><code>TCP_USER_TIMEOUT</code></td>
					<td>對 socket 設定資料未獲確認或因零視窗而無法送出時可容忍的最長時間</td>
					<td>單位為毫秒，不是控制每次 RTO 的間隔</td>
			</tr>
			<tr>
					<td><code>SO_KEEPALIVE</code></td>
					<td>是否啟用該 socket 的 TCP keepalive</td>
					<td>只改系統 keepalive 參數，不會替所有 socket 自動啟用</td>
			</tr>
			<tr>
					<td><code>net.ipv4.tcp_keepalive_time</code></td>
					<td>閒置多久後開始探測</td>
					<td>常見預設為 7200 秒</td>
			</tr>
			<tr>
					<td><code>net.ipv4.tcp_keepalive_intvl</code></td>
					<td>keepalive 探測間隔</td>
					<td>常見預設為 75 秒</td>
			</tr>
			<tr>
					<td><code>net.ipv4.tcp_keepalive_probes</code></td>
					<td>放棄前的未回應探測次數</td>
					<td>常見預設為 9，socket 選項可另行調整</td>
			</tr>
	</tbody>
</table>
<p>可以用唯讀命令檢查目前 namespace 的系統設定：</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-9-1"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-9-1">1</a>
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>sysctl net.ipv4.tcp_retries2 net.ipv4.tcp_keepalive_time net.ipv4.tcp_keepalive_intvl net.ipv4.tcp_keepalive_probes
</span></span></code></pre></td></tr></table>
</div>
</div><p>這些系統值不會顯示所有 per-socket 覆寫或應用層 deadline。例如，一次 80 ms 的停頓就可能讓 50 ms deadline 的 RPC 失敗，即使 TCP 仍保留連線並稍後完成重傳。這只是說明兩種時間尺度的差異，不是某種遷移後端的實測 downtime。</p>
<h2 id="八遷移失敗的退路先確認狀態與執行權在哪一端">八、遷移失敗的退路：先確認狀態與執行權在哪一端</h2>
<p>能否回復，核心問題不是「用了哪種加速技術」，而是<strong>哪一端仍擁有可繼續執行的一致狀態，以及執行權是否已經交出去</strong>。</p>
<h3 id="pre-copy保留來源端不代表所有失敗都能自動回滾">Pre-copy：保留來源端，不代表所有失敗都能自動回滾</h3>
<p>在 pre-copy 中，如果目的端尚未接管、來源端保有一致狀態，而且來源端裝置仍可恢復，通常有機會取消遷移或恢復來源端。但實際結果取決於失敗階段、遷移協定、確認機制與管理平台，不是任意硬體錯誤都能觸發通用的 all-or-nothing 自動回滾。</p>
<p>來源端後端可能已在 stop-and-copy 階段停止，因此恢復時仍可能需要重新設定或啟動裝置，不能假設硬體從未停過。網路也可能需要透過交換機學習、RARP / GARP、IPv6 鄰居通告或 SDN 控制面恢復轉送，而不是 QEMU 必定發一個 GARP 就完成。</p>
<p>假設目的端在載入 queue state 或建立 DMA 映射時失敗，合理的錯誤處理目標是：</p>
<ol>
<li><strong>限制目的端的副作用。</strong> 讓尚未完成載入的 Guest 保持未執行，停止或清理已建立的後端資源，避免殘留 DMA。</li>
<li><strong>確認失敗與接管狀態。</strong> 由遷移通道、已配置的確認機制及管理平台判斷是否可放棄目的端；不能只看到一個 ioctl 錯誤就假設另一端已安全停止。</li>
<li><strong>恢復來源端可執行狀態。</strong> 重新建立必要的裝置設定、通知與映射，並按遷移狀態停用不再需要的 dirty logging 或調整 SVQ 模式。</li>
<li><strong>恢復執行與網路轉送。</strong> 確認只有來源端繼續對外服務，再由相應網路機制恢復封包路徑。</li>
</ol>
<p>這是需要滿足的安全條件，而不是宣稱所有 QEMU 部署都有同一套跨主機交易協定。若管理平台使用額外的交接確認機制，也要把它納入故障測試。</p>
<p>回復期間是否繼續使用 SVQ，或重新建立直接資料面，取決於當時配置。來源端 RAM 中的 ring 與 payload 可以仍然存在，但裝置可能已被 suspend 或 reset，所以仍需檢查硬體設定是否一致。<strong>不必搬移整條佇列進晶片，不等於完全不必恢復硬體狀態。</strong></p>
<p>如果目的端已經執行並產生外部副作用，就不能直接把舊來源端再啟動，否則會形成 split-brain。這個接管邊界，也解釋了 <strong>postcopy</strong> 為什麼沒有同樣簡單的退路。</p>
<h3 id="postcopy可以恢復傳輸不等於可以回到舊快照">Postcopy：可以恢復傳輸，不等於可以回到舊快照</h3>
<p>Postcopy 在 RAM 尚未全部傳完時，就先讓目的端執行；缺少的頁面再透過背景傳輸或缺頁處理取得，常涉及 <code>userfaultfd</code>。目的端開始修改 CPU、RAM 與裝置狀態後，來源端的舊狀態就不再是可直接恢復的最新快照。</p>
<p>在支援的 postcopy 路徑中，QEMU 處理目的端對缺頁的需求，向來源端取得頁面並補入記憶體，讓等待的存取繼續。這與 pre-copy 「先把可恢復的 RAM 狀態備齊，再讓目的端執行」的次序不同；postcopy 也可以先經過 pre-copy，並沒有固定「90% RAM 必然還在來源端」的比例。</p>
<p>若目的端已把某頁改成新內容，直接恢復來源端就會丟掉那次修改；若目的端還缺另一頁而來源端永久失效，缺頁又可能無法補齊。問題是最新執行狀態與尚未傳送的必要頁面分別依賴兩端，而不是 RAM 位於兩台主機本身就必然代表資料已損毀。</p>
<p>Virtqueue metadata 與 payload 也是 Guest RAM 的一部分，因此還要解決裝置能否存取未到達的頁面。CPU 的缺頁可以透過受支援的 postcopy 機制處理，但一般 DMA 不能被假設會自動觸發同樣的 <code>userfaultfd</code> 補頁流程；硬體能 DMA，也不代表支援可恢復的 DMA page fault。這正是必須檢查裝置與後端 postcopy 能力，而不能只看 VirtIO 介面的原因。</p>
<p>若兩端仍存活、只是遷移通道中斷，QEMU 的 postcopy recovery 可以在重新連線後繼續遷移，但這不是回滾。若任一端永久失去必要狀態，就可能無法恢復。裝置 DMA 存取尚未到達的頁面另有要求，使用 vDPA、SVQ 或 VFIO 並不代表配置支援 postcopy，必須先確認後端限制。</p>
<table>
	<thead>
			<tr>
					<th>比較項目</th>
					<th>Pre-copy 接管前</th>
					<th>已進入目的端執行的 postcopy</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>正在推進 VM 狀態的一端</td>
					<td>來源端，或已為交接暫停的來源端</td>
					<td>目的端</td>
			</tr>
			<tr>
					<td>來源端舊狀態可否直接繼續</td>
					<td>滿足一致性及裝置恢復條件時有機會</td>
					<td>通常不行，已不包含目的端的新修改</td>
			</tr>
			<tr>
					<td>通道中斷時的方向</td>
					<td>可能取消並恢復來源端，依階段與協定決定</td>
					<td>在支援條件下重新連線並繼續傳輸</td>
			</tr>
			<tr>
					<td>主要風險</td>
					<td>目的端啟動失敗、來源端裝置無法恢復、接管確認不明</td>
					<td>任一端遺失必要狀態、缺頁無法補齊、DMA 能力不匹配</td>
			</tr>
	</tbody>
</table>
<p>這裡真正不可忽略的，是「先停止舊執行個體，再確認誰能繼續」的所有權管理。單機上的 SVQ 模式切換，即使有嚴密的本地狀態機，也不能自動提供跨主機的 split-brain 防護。</p>
<h2 id="九回到選型先確認需要什麼再確認裝置真的支援什麼">九、回到選型：先確認需要什麼，再確認裝置真的支援什麼</h2>
<p>沿著資料路徑走到這裡，可以看到各種方案並非單純的快慢排名，而是在 CPU 成本、可程式性、硬體依賴與遷移能力之間取捨。</p>
<table>
	<thead>
			<tr>
					<th>主要需求</th>
					<th>可優先評估的方向</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>一般 VM，優先考慮部署與維護便利</td>
					<td>VirtIO + vhost-net</td>
			</tr>
			<tr>
					<td>可程式化的高效能軟體交換</td>
					<td>vhost-user + DPDK / OVS-DPDK</td>
			</tr>
			<tr>
					<td>降低 Host 資料面 CPU 成本，保留 VirtIO Guest 介面</td>
					<td>硬體 vDPA</td>
			</tr>
			<tr>
					<td>直接使用 VF 專屬能力</td>
					<td>SR-IOV + VFIO，另確認遷移支援</td>
			</tr>
			<tr>
					<td>驗證 vDPA 框架，不要求硬體效能</td>
					<td>軟體 vDPA 模擬裝置</td>
			</tr>
	</tbody>
</table>
<p>硬體 vDPA 的前提，是裝置與驅動真的能提供所需的 VirtIO 相容資料面及控制能力。<strong>支援 DPDK，不等於支援 vDPA</strong>：PMD 能操作廠商格式的 NIC queue，並不能證明裝置能處理 VirtIO queue。DPU / SmartNIC 也只是較廣的硬體類別，不等於 vDPA，更不保證交換、安全與控制面都已卸載。</p>
<p>例如，不能由 Intel E810 具有 <code>ice</code> 驅動、DPDK PMD、SR-IOV 或 DDP package，就推導它具備原生硬體 vDPA 能力；DPDK IFCVF 文件描述的是 Intel FPGA 100G VF，也不能把 <code>ifcvf</code> 的支援對象直接等同於 E810。相同原則適用於其他廠牌：應核對精確型號、PCI ID、韌體、驅動及功能矩陣，而不是只看產品家族名稱。</p>
<p>DDP（Dynamic Device Personalization）處理的可程式化封包解析等能力，與裝置是否能消費 VirtIO queue 是不同問題；同理，switchdev 模式、RDMA / RoCE / iWARP 與 vDPA 也各有自己的功能矩陣及資源限制。不能由其中一項能力，推論其他能力已具備，也不應把某一型號的互斥限制擴大成所有 vDPA 裝置的共同規則。</p>
<p>即使裝置已能建立 vDPA queue，遷移仍可能受 feature bits、queue 規模、控制佇列、ring 格式及 Guest vIOMMU 等條件限制。例如，本文參考的 QEMU <code>hw/virtio/vhost-vdpa.c</code> 版本包含對 SVQ 與 Guest vIOMMU 組合的檢查，不能因為 Host 本身有 IOMMU，就認定 Guest vIOMMU 與 SVQ 可任意搭配。實際部署應查對應版本的檢查條件與 migration blocker。</p>
<p>可以先用下列唯讀命令收集部署資訊；部分環境可能需要額外權限：</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-10-1"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-10-1">1</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-10-2"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-10-2">2</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-10-3"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-10-3">3</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-10-4"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-10-4">4</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-10-5"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-10-5">5</a>
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>lspci -nnk
</span></span><span style="display:flex;"><span>ethtool -i &lt;介面名稱&gt;
</span></span><span style="display:flex;"><span>vdpa mgmtdev show
</span></span><span style="display:flex;"><span>vdpa dev show
</span></span><span style="display:flex;"><span>devlink dev info
</span></span></code></pre></td></tr></table>
</div>
</div><p>沒有列出管理裝置，可能是驅動、韌體或配置問題；成功列出裝置，也只代表評估的起點，不代表遷移能力已經齊備。真正部署前，至少要回答以下問題：</p>
<ul>
<li><strong>介面相容嗎?</strong> 核心、QEMU、後端、驅動與韌體的版本是否匹配?Guest feature bits、Split / Packed Ring、佇列數與大小是否受支援?</li>
<li><strong>記憶體路徑完整嗎?</strong> DMA 映射與隔離、pinning 限制、Hugepages / NUMA 是否配置妥當?vCPU、QEMU 與 DMA 的寫入是否全被納入髒頁追蹤?</li>
<li><strong>狀態能接續嗎?</strong> 來源端能否安全停止與保存，目的端能否恢復?兩端硬體與功能是否相容?所選 pre-copy / postcopy、SVQ 模式是否存在 migration blocker?</li>
<li><strong>失敗與服務影響測過嗎?</strong> 除了吞吐量與 CPU，也應量測 downtime、延遲分位數、丟包、重傳與收斂情形，並測試高髒化率、滿載流量、取消遷移及後端 / 目的端失敗。</li>
</ul>
<p>最後再把這些結果放回總持有成本：回收多少 Host 核心，是否值得新增硬體、功耗、維運與相容性成本?硬體卸載的價值應由工作負載與量測證明，而不是用「CPU 歸零」或固定收益比例替代評估。</p>
<p>把架構、驅動與遷移放在同一張表裡，可以作為最後的交叉檢查：</p>
<table>
	<thead>
			<tr>
					<th>維度</th>
					<th>vhost-net</th>
					<th>vhost-user + DPDK</th>
					<th>硬體 vDPA</th>
					<th>傳統 VFIO VF 直通</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>主要 VirtIO 資料處理位置</td>
					<td>Host 核心</td>
					<td>外部使用者空間後端</td>
					<td>VirtIO 相容裝置，必要時由 SVQ 中介</td>
					<td>Guest 使用的通常是廠商特定硬體資料面</td>
			</tr>
			<tr>
					<td>Guest 驅動</td>
					<td>virtio-net</td>
					<td>virtio-net</td>
					<td>virtio-net</td>
					<td>對應 VF 驅動</td>
			</tr>
			<tr>
					<td>Host CPU 主要成本</td>
					<td>核心後端、網路與通知</td>
					<td>PMD、軟體交換與後端處理</td>
					<td>通知、管理、例外及可能的 SVQ 處理</td>
					<td>VFIO 管理、通知與平台路徑</td>
			</tr>
			<tr>
					<td>Payload 是否必然零複製</td>
					<td>否</td>
					<td>否</td>
					<td>可避免 QEMU 額外複製，不代表全鏈路皆無複製</td>
					<td>可直接 DMA，仍須看完整工作負載</td>
			</tr>
			<tr>
					<td>髒頁與狀態責任</td>
					<td>KVM、QEMU 與核心後端協作</td>
					<td>KVM、QEMU 與外部後端協作</td>
					<td>額外確認 DMA tracking、SVQ 與裝置可恢復性</td>
					<td>額外確認 VFIO migration、DMA tracking 與硬體能力</td>
			</tr>
			<tr>
					<td>可否只憑方案名稱保證熱遷移</td>
					<td>否</td>
					<td>否</td>
					<td>否</td>
					<td>否</td>
			</tr>
	</tbody>
</table>
<h2 id="結語卸載的是工作不能丟掉的是狀態">結語：卸載的是工作，不能丟掉的是狀態</h2>
<p>VirtIO 先統一 Guest 與裝置之間的語言，vhost 讓資料處理離開 QEMU，硬體 vDPA 再把相容資料面交給裝置。這條演進路徑的目的，是在不迫使 Guest 跟著底層實作改變的前提下，降低 Host 的 I/O 成本。</p>
<p>熱遷移則把另一面問題帶回來：當硬體可以自主工作，軟體仍必須知道哪些記憶體被修改、哪些描述符已完成，以及誰有權繼續執行。SVQ 的價值，正是在保留 payload 直接 DMA 的同時，讓 QEMU 重新掌握描述符與完成回報，把硬體工作接回遷移所需的狀態管理。</p>
<p>因此，評估這套架構最重要的，不只是問「封包有沒有繞過 QEMU」，而是接著問：<strong>繞過之後，誰還能完整說明系統現在的狀態?</strong>，能回答這個問題，才有機會同時做好效能、可攜性與可靠的熱遷移。</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
