一覧OS-14-1 · 仮想化と隔離の原理

仮想マシンのCPU・メモリ・入出力

ゲストから見える資源がホスト上のどこへ対応するかを追います。

説明

仮想マシンはゲストOSへCPU、メモリ、ディスク、ネットワーク装置を提示します。ゲストが扱う番号やアドレスは、ホストの番号やアドレスと同じとは限りません。

一台のVMの対応表を使い、vCPUの実行、二段階のメモリ変換、仮想ディスク内の位置、NICの送信経路を調べます。最後に入出力要求と完了通知の記録を読み、ゲストが待つ場所とホスト側の処理を結び付けます。

1VMに提示した資源

lab-vmにはvCPUを2個、メモリを256MiB提示しています。KVMを使うVMMを想定し、vCPUを実行するホストスレッドとの対応を確認します。

vm = {"name": "lab-vm", "memory_bytes": 256 * 1024**2, "vcpus": {0: 5101, 1: 5102}}VMの構成表; print(vm["name"], vm["memory_bytes"] // 1024**2, vm["vcpus"])メモリ量とスレッド対応

vCPU0と1をそれぞれTID5101と5102のスレッドが担当する構成です。ゲストでCPUが2個見えても、ホストの物理コアが2個専有されているとは限りません。ここではホストがスレッドをスケジュールする方式を扱います。

2同じ物理CPUを時間で共有

二つのvCPUスレッドが、ホストの論理CPU3を交互に使った記録を集計します。単位はmsです。

runs = [(5101, 3, 0, 4), (5102, 3, 4, 8), (5101, 3, 8, 10)]ホストCPUの実行区間; totals = {tid: sum(end-start for t,cpu,start,end in runs if t == tid) for tid in vm["vcpus"].values()}スレッドごとの時間; print(totals, sum(totals.values()))合計を照合

10msの区間でTID5101が6ms、5102が4msです。vCPU数だけでは同時実行量は分かりません。ホスト上の競合や割当てによって、ゲストから見た実行待ちが変わります。

3ゲスト仮想アドレスからゲスト物理アドレスへ

ページサイズを4KiBとし、ゲストのページ表で仮想ページ4をゲスト物理フレーム2へ対応させます。

page_size = 4096; guest_pt = {4: 2}; gva = 0x4123ゲストのページ表と入力; gpa = guest_pt[gva // page_size] * page_size + gva % page_sizeゲスト物理位置へ変換; print(hex(gva), hex(gpa))変換前後

ゲスト仮想アドレス0x4123は、ゲスト物理アドレス0x2123になります。オフセット0x123は変わりません。この物理アドレスはゲストが認識するもので、ホストの物理位置への変換はさらに必要です。

4ゲスト物理からホスト物理へ

二段目の対応でゲスト物理フレーム2をホスト物理フレーム0x91へ割り当てます。

stage2 = {2: 0x91}二段目の対応; hpa = stage2[gpa // page_size] * page_size + gpa % page_sizeホスト物理位置へ変換; print(hex(hpa))このVMの位置; print(hex(0xB1 * page_size + gpa % page_size))別VMの対応例

このVMでは0x91123です。別VMが同じゲスト物理0x2123をホストフレーム0xB1へ対応させていれば0xB1123になります。同じゲスト物理値でも別のホスト位置に置けるため、VM間のメモリを分離できます。実際の変換方式はCPUアーキテクチャ等によります。

5仮想ディスクのLBAとイメージ内位置

ヘッダーのないrawイメージをディスクとして提示し、論理セクターを512バイトとします。LBA8から1セクターを読む位置を求めます。

sector_size = 512; lba = 8; offset = lba * sector_size仮想LBAからバイト位置; disk = bytearray(16 * sector_size); disk[offset:offset+4] = b"DATA"rawイメージの試料; print(offset, bytes(disk[offset:offset+4]), len(disk[offset:offset+sector_size]))位置と内容と長さ

イメージ内オフセット4096から512バイトを読み、先頭はDATAです。これはraw形式内の位置であり、ホストの物理ディスク上のセクター番号ではありません。ホストのファイルシステムや別形式のイメージでは、その下にさらに対応付けがあります。

6仮想NICからホストの接続先へ

このVMは仮想NIC→TAP→Linux bridgeの経路で接続しています。設定表から接続先をたどります。

links = {"guest:eth0": "host:tap7", "host:tap7": "host:br-lab", "host:br-lab": "host:eth2"}接続表; path = ["guest:eth0"]; path.append(links[path[-1]]); path.append(links[path[-1]]); path.append(links[path[-1]])経路をたどる; print(" -> ".join(path))送信経路

ゲストのeth0とホストのeth2は別のインターフェースです。この構成にはNATを入れておらず、bridgeがフレームを転送します。VMの接続はNAT、独立した仮想スイッチ、装置の直接割当て等にもでき、方式により観測位置や制御箇所が変わります。

7入出力要求と完了通知

時刻をマイクロ秒で表した一件のディスク読取り記録です。要求ID7の流れを表示します。

events = [(0,"guest submit",7),(4,"backend receive",7),(24,"host read done",7),(27,"used ring update",7),(29,"guest completion",7)]同じ要求の時系列; print(*events, sep="\n")処理の順序; print("completion_us=", events[-1][0] - events[0][0])要求から完了まで

要求から完了まで29µsです。ゲストが要求を置き、バックエンドが処理し、完了情報を返しています。virtioのような準仮想化では共有キューを使いますが、通知の処理先や割込みの抑制などは構成によって変わります。

8VMを増やしたときに競合する資源

同じホストに3台のVMを置き、各2vCPUを1個の論理CPUで動かす構成を計算します。

vcpu_counts = [2, 2, 2]; host_cpus = 1提示数とホストの実行資源; print(sum(vcpu_counts), host_cpus, sum(vcpu_counts) / host_cpus)割当ての比率

提示したvCPUは6個でも、同時に命令を実行できるホストCPUは1個です。メモリやI/Oにも共有する資源があります。VMごとの見え方が分かれていることと、性能資源が専有されていることは別に確認します。

まとめ

ゲストのCPU番号、物理アドレス、LBAは、ホスト側の番号や位置と同じではありません。

メモリ変換と仮想入出力の仲介を通して、VMに資源を提示します。

VMが独立して見えても、ホストのCPU・メモリ・I/Oを共有する構成があります。

操作履歴と解説を振り返れます。

Python 3

1
vm = {"name": "lab-vm", "memory_bytes": 256 * 1024**2, "vcpus": {0: 5101, 1: 5102}}; print(vm["name"], vm["memory_bytes"] // 1024**2, vm["vcpus"])
lab-vm 256 {0: 5101, 1: 5102}
2
runs = [(5101, 3, 0, 4), (5102, 3, 4, 8), (5101, 3, 8, 10)]; totals = {tid: sum(end-start for t,cpu,start,end in runs if t == tid) for tid in vm["vcpus"].values()}; print(totals, sum(totals.values()))
{5101: 6, 5102: 4} 10
3
page_size = 4096; guest_pt = {4: 2}; gva = 0x4123; gpa = guest_pt[gva // page_size] * page_size + gva % page_size; print(hex(gva), hex(gpa))
0x4123 0x2123
4
stage2 = {2: 0x91}; hpa = stage2[gpa // page_size] * page_size + gpa % page_size; print(hex(hpa)); print(hex(0xB1 * page_size + gpa % page_size))
0x91123
0xb1123
5
sector_size = 512; lba = 8; offset = lba * sector_size; disk = bytearray(16 * sector_size); disk[offset:offset+4] = b"DATA"; print(offset, bytes(disk[offset:offset+4]), len(disk[offset:offset+sector_size]))
4096 b'DATA' 512
6
links = {"guest:eth0": "host:tap7", "host:tap7": "host:br-lab", "host:br-lab": "host:eth2"}; path = ["guest:eth0"]; path.append(links[path[-1]]); path.append(links[path[-1]]); path.append(links[path[-1]]); print(" -> ".join(path))
guest:eth0 -> host:tap7 -> host:br-lab -> host:eth2
7
events = [(0,"guest submit",7),(4,"backend receive",7),(24,"host read done",7),(27,"used ring update",7),(29,"guest completion",7)]; print(*events, sep="\n"); print("completion_us=", events[-1][0] - events[0][0])
(0, 'guest submit', 7)
(4, 'backend receive', 7)
(24, 'host read done', 7)
(27, 'used ring update', 7)
(29, 'guest completion', 7)
completion_us= 29
8
vcpu_counts = [2, 2, 2]; host_cpus = 1; print(sum(vcpu_counts), host_cpus, sum(vcpu_counts) / host_cpus)
6 1 6.0

演習完了