KVM のメモリとデバイス仮想化

このページの目次

ゲストのアドレスのページフォールト、第2段階変換の失敗、デバイスのDMA境界違反は、それぞれ異なる制御パスで発生する。まずCPU のアドレス変換とデバイスアクセスの認可を明確にし、次に共有キューと移行について考察する。

参照目標読書位置
アドレス、memslot およびフォールト最初の3節
キューの所有権とバックエンドvirtio、vhost
デバイスの認可と移行条件VFIO、パsthrough移行

2段階アドレス変換

ゲストアドレスとホストアドレス:2段階変換とユーザー空間マッピング GVA ゲスト仮想アドレス GPA ゲスト物理アドレス HPA ホスト物理アドレス ゲストページテーブル EPT / NPT HVA VMM のユーザー仮想アドレス memslot 対応関係 HVA はホストソフトウェアがバックアップを見つけるために使用されます。ハードウェアがゲストのメモリアクセスを実行する際、QEMU のページテーブルを先に照会することはありません。

ゲストOSはGVAからGPAへのページテーブルを管理し、x86のEPT/NPTはGPAをHPAにマッピングし、ホスト制御の権限を適用します。2段階の権限が、アクセスが許可されるかどうかを共同で決定します。VMMのHVAは、ホストソフトウェアがバックアップを管理する視点であり、ゲストが直接使用できるアドレスではありません。

古典的なユーザーメモリインターフェースは、memslot を使用して、GPAとVMMユーザー空間メモリの対応関係を登録します。登録は、すべての範囲の物理ページを一度にロックするわけではありません。ホストのページフォールト、回収、およびマッピングの無効化には、KVMによる調整が必要です。プライベートゲストメモリなどの新しいメカニズムは異なるインターフェースを持っており、すべてのVMメモリを通常の読み書き可能なHVAと仮定することはできません。メモリインターフェース

memslot のオフセット計算は物理ページ番号を返さない

通常のユーザーメモリスロットがGPA [0x10000000, 0x10003000) をカバーし、対応するHVAの起点が 0x70000000 であるとします。GPA 0x10001234 はスロット内にあり、オフセットは 0x1234 であるため、対応するHVAは 0x70001234 となります。この加算はVMMアドレス空間内の位置のみを返し、HPAを導き出すことはできません。ホストのページテーブルおよびバックアップの状態が、現在どの物理ページで保持されているかを決定します。

アドレスがスロット内にあるからといって、すべてのアクセスがその範囲内にあるとは限りません。例えば、0x10002ffc から8バイト読み込む場合、後4バイトはスロットの排他的上限を超えます。以下のモデルでは、空でないアクセスの全体が単一のスロット内に存在することを要求し、スロットをまたぐリクエストの分割は行いません。

GPA_BASE, HVA_BASE, SIZE = 0x10000000, 0x70000000, 0x3000

def slot_hva(gpa, length):
    if not 0 <= gpa < 2**64 or not 0 < length <= 2**64:
        raise ValueError("不正なアドレスまたは長さ")
    offset = gpa - GPA_BASE
    if not 0 <= offset < SIZE or length > SIZE - offset:
        raise ValueError("全体アクセスは本スロット内に存在する必要があります")
    return HVA_BASE + offset

assert slot_hva(0x10001234, 8) == 0x70001234
assert slot_hva(0x10002ffc, 4) == 0x70002ffc
for gpa, length in [(0x10002ffc, 8), (0x10003000, 1),
                    (GPA_BASE - 1, 1), (GPA_BASE, 0), (2**64 - 1, 8)]:
    try:
        slot_hva(gpa, length)
    except ValueError:
        pass
    else:
        raise AssertionError("境界外または不正なアクセスは拒否されるべきです")

固定幅言語では、「長さが残りの空間を超えているかどうか」をチェックすることで、gpa + length を計算する際の整数オーバーフローを回避できます。実際のデバイスバックエンドでは、RAMスロットが見つからない場合、マシンアドレスマップに従ってMMIOデバイスまたは無効なアクセスに属するかどうかを判断する必要があり、空洞をすべて読み書き可能なデバイスと見なしてはいけません。

EPT violation はゲストがページテーブルを変更したことを意味しない

ゲストが自らのGVAページテーブルを更新しても、すべてのGPAからHPAへのマッピングを再構築する必要はありません。EPT violation は、第2段階変換が存在しないか、権限が満たされていないことを示しており、オンデマンドでのマッピング構築、書き込み保護の追跡、またはMMIOとして扱われる領域へのアクセスなどが考えられます。

ゲスト自身のページテーブルに起因するページフォールトと第2段階の問題も異なります。前者はゲストカーネルに処理を委ねる必要がある場合があります。診断時には、フォールトがどのレイヤーで発生したかを区別する必要があり、すべてのフォールトをゲストによるページテーブルの変更と見なしてはいけません。

ハードウェアによるネストされたページテーブルは、ソフトウェアシャドウページテーブルの保守負担を軽減しますが、KVM MMU には依然として複雑なマッピングおよび無効化の管理があり、ネスト仮想化も追加の組み合わせをもたらします。シャドウMMUを単に完全に存在しなくなった古いCPUの遺産と理解してはいけません。KVM MMU

ゲストPTEとEPTの権限をそれぞれ変更し、1回のメモリアクセスが第1段階または第2段階で停止するようにします。図では同じページ内オフセットを保持していますが、失敗後のアドレスは完了した変換の結果として表示されません。

図を準備しています
ゲストは書き込みを許可されているのに、なぜ第2段階で拒否されるのか?

GVA→GPA と GPA→HPA は異なるマッピングであり、権限は共同で有効になります。EPT violation はゲストのページテーブルが直前に変更されたことを証明するものではなく、同じ種類のページフォールトをゲストに注入する必要があることを意味するものでもありません。

ページテーブルのトラバーサルコストをなぜ24ステップに固定できないのか

4段階のゲストページテーブルに4段階のネストページテーブルを重ねると、理想的なキャッシュなし、大ページなしの条件下で、複数のアクセスが発生することが導き出せます。各ゲストページテーブルエントリ自体を取得するためにも、第2段階変換が必要です。実際のハードウェアにはTLB、ページテーブルトラバーサルキャッシュ、大ページ、および異なるレイヤー数があり、すべてのロードで完全なパスを再走行するわけではありません。

大ページは変換の負荷を軽減する可能性がありますが、ゲストとホストの両方の大ページ、アラインメント、および連続性を考慮する必要があります。ダーティページ追跡、共有、または無効化によりマッピングが分割される可能性もあり、ゲスト内でHugePagesを使用しているからといって、第2段階でも大ページが使われていると断言することはできません。

完全ミスした4段階トラバーサルの展開

ゲストページテーブルが g レベルを歩き、各ゲストページテーブルエントリが存在するGPAに対して完全な h レベルの第2段階ページテーブルを走行すると仮定します。関連するすべてのキャッシュがミスしており、大ページによる早期終了はなく、アクセスビット/ダーティビットの更新などの追加トランザクションは無視します。

各ゲストページテーブルエントリを取得するには、h 回の第2段階ページテーブルエントリの読み取りが必要で、さらにゲストページテーブルエントリ自体の読み取りが必要であり、合計 h+1 回となります。そのようなエントリが g 個あります。最終的なGPAを取得した後、さらに h 回の第2段階ページテーブルエントリの読み取りが必要で、最後に1回だけターゲットデータの読み取りを行います:

  • 変換関連の読み取り:g × (h+1) + h。
  • 最終的なデータの読み取りを加えると:(g+1) × (h+1)。
gh変換関連の読み取り最終的なデータの読み取りを含む
442425
431920
341920
331516
def uncached_walk(g, h):
    assert g >= 1 and h >= 1
    translation = sum(h + 1 for _ in range(g)) + h
    return translation, translation + 1

assert uncached_walk(4, 4) == (24, 25)
assert uncached_walk(4, 3) == uncached_walk(3, 4) == (19, 20)
assert uncached_walk(3, 3) == (15, 16)

これにより、「24」が通常何を数えているのか、そしてなぜそれがすべてのアクセスの固定コストとして記述できないのかが説明されます。TLB ヒット時には上記の完全なトラバーサルは発生しません。ページテーブルエントリが存在するページと最終的なデータページが、同じ第2段階のリーフレベルを持つ必要はありません。表ではそれらをすべて h に統一していますが、これは仮定の変化を比較しているに過ぎず、ゲストに大ページ1つを割り当てれば、すべての h が必ず1減ることを意味するものではありません。

virtio はデバイスプロトコルを定義するが、バックエンドの場所を限定しない

virtio ドライバとデバイスは、ネゴシエートされた機能と virtqueue を介してバッファ記述子を交換します。PCI、MMIO などは転送方式であり、virtio-net、virtio-blk などはデバイスタイプです。バックエンドは QEMU、カーネル vhost、ユーザー空間サービス、または互換性のあるハードウェアのいずれか也可以是です。Virtio

split virtqueue は、記述子テーブル、available および used 領域を分離します。packed virtqueue は、別の公開および回収形式を使用します。split のフィールドフローを使用して、すべての packed 実装を直接記述することはできません。

split virtqueue の段階所有権および可視性の要件
ドライバの準備まず記述子とデータを埋める
available の公開要件に従ってソートし、デバイスに渡す
デバイスの処理および used の公開完了状態がプロトコルに従って可視になるようにする
ドライバの回収完了を確認してからバッファを再利用する

バリアと公開順序は、単なるパフォーマンス最適化ではなく、このプロトコルを維持します。

kick および割り込みは通知であり、1つのリクエストが厳密に1回の通知に対応するわけではありません。抑制やマージが可能であり、ドライバはポーリングも可能です。バッファをデバイスに渡した後、デバイスがまだ使用している間に随意に修正または解放することはできません。共有メモリは所有権プロトコルがないことを意味するものではなく、ゼロコピーを意味するものでもありません。Virtio 仕様

2つのリクエストが連続して公開されるが、回収順序が逆になる場合

VIRTIO_F_IN_ORDER がネゴシエートされていない split queue を例に挙げます。ドライバが記述子チェーンのヘッド 0 を最初に公開し、次にチェーンのヘッド 1 を公開した場合、デバイスは 1 を先に完了し、0 を後に完了させることができます。ドライバは、used 内のチェーンヘッドIDに基づいてリクエストを検索する必要があり、「最初に完了した項目」を「最初に送信されたリクエスト」に強制的に割り当てることはできません。IN_ORDER がネゴシエートされた場合は、その規則に従う必要があり、この逆順の例を適用してはいけません。

完了順序は変更可能であり、チェーンヘッドIDは依然として自らのリクエストを指す 公開順序 0:A → 1:B デバイス完了順序 1:B → 0:A ドライバはIDに基づいて回収 先に B を回収し、次に A を回収 0 は A が完了し、回収される前に、新しいリクエスト C を受け入れるために使用することはできません split queue、IN_ORDER はネゴシエートされていない;図中の 0、1 は記述子チェーンのヘッドIDであり、used ring の位置ではありません。

以下では、デバイスの読み取り専用リクエストバッファ2つとそのライフサイクルのみをモデル化します。device 状態には、すでに公開され、デバイスがまだ開始していない、または処理中の期間が含まれます。used は、完了したがドライバがまだ回収していない公開済みを示します。キューインデックスのラップアラウンド、記述子チェーン、バリア、および通知メカニズムは、完全な実装によって別途処理されます。

from collections import deque

class Buffers:
    def __init__(self):
        self.state = {0: "driver", 1: "driver"}
        self.data = {}
        self.used = deque()

    def write(self, ident, payload):
        if self.state[ident] != "driver":
            raise RuntimeError("バッファはドライバに戻っていないため、書き換えることはできません")
        self.data[ident] = bytes(payload)

    def publish(self, ident):
        assert self.state[ident] == "driver" and ident in self.data
        self.state[ident] = "device"

    def complete(self, ident):
        assert self.state[ident] == "device"
        self.state[ident] = "used"
        self.used.append(ident)

    def reclaim(self):
        ident = self.used.popleft()
        assert self.state[ident] == "used"
        self.state[ident] = "driver"
        return ident, self.data[ident]

queue = Buffers()
queue.write(0, b"request A"); queue.publish(0)
queue.write(1, b"request B"); queue.publish(1)
try:
    queue.write(0, b"overwritten")
except RuntimeError:
    pass
else:
    raise AssertionError("早期に再利用することはできません")
queue.complete(1)
assert queue.reclaim() == (1, b"request B")
assert queue.state[0] == "device"
queue.complete(0)
assert queue.reclaim() == (0, b"request A")
queue.write(0, b"request C")
queue.publish(0)
assert queue.state == {0: "device", 1: "driver"}

「割り込みを受信していない」からといって、まだ完了していないと断言することはできません。「割り込みを1つ受信した」からといって、1つのリクエストのみが完了したと断言することもできません。回収は、プロトコルに従って可視な used 情報に基づいて行われます。割り込みと kick は、他側がキューをチェックするよう促すだけです。メモリ順序の要件は、受信側がインデックスの進捗を見る際に、記述子、データ、または完了情報がすでに可視であることを保証しますが、上記の順序のPython呼び出しは、実際の弱いメモリ順序を検証していません。Virtio split queue と IN_ORDER

vhost と vhost-user の境界

カーネル vhost-net などのバックエンドは、キューデータの処理の一部をカーネルに配置し、QEMU デバイスモデルへの対応するリクエストの往復を削減します。QEMU は設定と制御を担当し、プロセスが完全に仮想マシン実行から退出するわけではありません。

vhost-user はバックエンドを別のユーザー空間プロセスに配置し、プロトコルを介してキュー、メモリ、および通知情報を伝達します。名前に vhost が含まれていても、すべてのバックエンドがカーネル内にあるわけではありません。vhost-vsock は AF_VSOC シマンティクスを処理し、「AF_UNIX を VM 境界間で超える」と記述してはいけません。

バックエンドの選択は、コピー、スケジューリング、メモリ共有、およびフォールト分離パスを変更します。パフォーマンスについては、実際のキュー数、パケットまたはブロックサイズ、CPU 配置、および負荷測定に基づいて評価する必要があり、遅延が一律に50%削減されると約束することはできません。

VFIO:デバイスアクセス権の割り当てとDMA分離

VFIO は制御されたデバイスアクセス能力を公開し、パsthroughやユーザー空間ドライバに使用できます。CPU の EPT/NPT は、デバイスによるDMAを自動的に制限しません。IOMMU マッピング、デバイス分離トポロジ、および割り込みリマッピングなどは、それぞれ個別に構成する必要があります。VFIO

従来のインターフェースにおける IOMMU グループは、分離可能なデバイスの集合を表しており、必ずしも1つのPCI functionに等しいわけではありません。デバイスをアンバインドする前に、同じグループ内のデバイス、リセット範囲、およびホストの依存関係を理解する必要があります。理論的なチュートリアルでは、1行の unbind でライフサイクル全体の設定を完了したかのように偽装してはいけません。

新しいインターフェースでは、IOMMUFD などの方法を使用してデバイスアドレス空間を管理する場合がありますが、これにより基盤となるハードウェアの分離制限を無視してはいけません。DMA が認可された範囲のみアクセス可能であることは、正しい構成後の目標であり、「vfio-pci のバインド」という1つの操作で自動的にすべての保証が完了するものではありません。

パsthrough移行は絶対不可能ではない

移行前にデバイスが提供する必要がある回復材料

移行には、デバイス状態の保存と復元、進行中のDMAの処理、およびメモリに対するデバイスの変更の正確な記録が必要です。これらの機能を持たないデバイスは、透過的に移行できません。VFIO migration のサポートを備え、QEMU およびターゲットの互換性条件を満たすデバイスは、対応する移行プロセスをサポートする可能性があります。VFIO 移行

したがって、「すべてのパsthroughデバイスがオンライン移行できない」と記述するのではなく、機能と互換性の問題として記述する必要があります。VM全体のメモリ、CPU、およびデバイスの一貫性の境界については、QEMU と KVM の協力 を参照してください。