仮想マシンの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
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}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} 10page_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
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
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
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
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
vcpu_counts = [2, 2, 2]; host_cpus = 1; print(sum(vcpu_counts), host_cpus, sum(vcpu_counts) / host_cpus)6 1 6.0
演習完了