はじめに
gokvm開発 1 2 の近況報告。
これまでは1つの仮想CPUにしか対応していなかった。
マルチCPUのためSMP(Symmetric Multiprocessing)対応させたいと思い立ってから2~3週間くらい試行錯誤し、無事実装することができた。
自分の知る限り KVM でVMMを作ってみたという取り組みを探す中で、
具体的にSMP対応とはどのような実装なのか解説されている資料がなかなか見つからなかった。
稚拙な記事ではあるけれど、今後自作VMMに挑戦する方にこの記事が役に立てば嬉しい。
例によってコミット単位に開発の経過を紹介していく。もちろん実際にはもっともっと泥臭い実装から始めていて、
何度もgit rebase
を繰り返しながら、最終的に解説できるよう粒度を調整したので、コミットのタイムスタンプはあてに出来ない。
#34 vCPUスレッドを複数生成
プルリクエストをいただいた。まずはioctl(fd,KVM_CREATE_VCPU,...)
でvCPUを複数生成できるよう変更する。
その後vCPUごとに個別のスレッドを生成して各vCPUごとに独立してioctl(fd,KVM_RUN,...)
を発行する。
vCPUはライフタイム全体を通して、同一のスレッドからioctl
を発行する必要がある。
Go言語の場合にはスレッドの代わりにgoroutineを使うことが多いので、
runtime.LockOSThread()
を呼び出してgoroutineとスレッドを静的に関連づけた。
ce22a91 struct mpf_intel の実装
vCPUがカーネルに認識されるためにはIntel MultiProcessor Specification 3 に従ったデータ構造を認識させる必要がある。
このデータ構造はLinuxカーネルの中で struct mpf_intel
のPhysPtr
が指す先 struct mpc_table
に対応する。
コード 4 を読むと、チェックサム・バーション・マジックナンバーを読み取ることができたので、
仕様書は斜め読みしかしていない。
このデータ構造はどこに配置すれば良いのか。仕様書を読むとExtended BIOS Data Area (EBDA)の最初の1KB以内とあるのでそこに配置することにした。 EBDAは典型的に 0x0009FC00 に置かれる 5 ようなので、それに倣った。
- a. In the first kilobyte of Extended BIOS Data Area (EBDA), or
- b. Within the last kilobyte of system base memory (e.g., 639K-640K for systems with 640 KB of base memory or 511K-512K for systems with 512 KB of base memory) if the EBDA segment is undefined, or
- c. In the BIOS ROM address space between 0F0000h and 0FFFFFh.
この仕様書の中でブートを担当するCPUをBoot Strap Processor(BSP)、その他のCPUをApplication Processor(AP)
と呼ぶことを知った。
このコミットでは struct_mpf_intel
の準備まで。
いくつかデータ構造が登場するので、下図にメモリマップとしてまとめておく。
0x0009fc00 (EBDA) ---> +--------------------------------------------------------+
| 16 Bytes Alignment |
| |
| |
| |
0x0009fc00 + 0x30 ---> +--------------------------------------------------------+
| struct mpf_intel (16 Bytes) |
| |
| // physical address of struct mpc_table |
| .phys_ptr = EBDA start + 0x40 |
| .signature = "_MP_" |
| .length = 1 |
| .Specification = 4 |
| |
0x0009fc00 + 0x40 ---> +--------------------------------------------------------+
| struct mpc_table |
| |
| .phys_ptr = EBDA start + 0x40 |
| .signature = "PCMP" |
| .length = 1 |
| .specification = 4 |
| // number of entries (mpc_cpu, mpc_bus, etc.) |
| .oem_count = 2 |
| |
| +-------------------------------------------------+ |
| | struct mpc_cpu (for processor #0) | |
| | .type = 0 | |
| | .apic_id = 0 | |
| | .cpu_flag = ENABLE_PROECSSOR | BOOT_PROCESSOR| |
| +-------------------------------------------------+ |
| |
| +-------------------------------------------------+ |
| | struct mpc_cpu (for processor #1) | |
| | .type = 0 | |
| | .apic_id = 1 | |
| | .cpu_flag = ENABLE_PROECSSOR | |
| +-------------------------------------------------+ |
| |
+--------------------------------------------------------+
3478e15 struct mpc_table の実装
struct mpc_table
はstruct mpf_intel
の直後に配置した。この場所が正しいのかは分からない。
ここで、Local APIC(LAPIC)のアドレスを設定する。LAPICは典型的に 0xFEE00000 に置かれるので、それに倣った。
vCPUがこのメモリにアクセスすると、KVMを通してホストカーネルがエミュレートしてくれるLAPICへのアクセスに置き換えられるようだ。
メモリアクセスをトラップしてユーザランドでエミュレーションしたり… といったことはやらなくて大丈夫だった。
無事ブート時に認識されるようになった。
本来は、struct mpc_cpu
は struct mpc_table
の後にメモリアドレスが連続するように配置されるが、
今回は簡単のため struct mpc_table
のメンバにstruct mpc_cpu
を埋め込むように実装した。
$ dmesg
[ 0.031995][ T0] Intel MultiProcessor Specification v1.4
[ 0.033955][ T0] mpc: 9fc40-9fc6a
4ac446f struct mpc_cpu の実装
struct mpc_table
はテーブル構造のヘッダであり、テーブルエントリとしてstruct mpc_cpu
や struct mpc_bus
などが任意の個数連続する。
ここではとりあえずvCPU数を2とした。
エントリはTypeによって区別される。
出典:Intel MultiProcessor Specification 3
Local APICはvCPUごとに存在しているので、vCPUの通し番号を流用した。 vCPUごとに有効・無効、BSP・APの制御フラグがあるので、注意する。 1つ目のvCPUのみをBSPとして、残りをAPとした。 Local APICとCPUの関連、BSP・APの区別については下図が分かりやすい。
出典:Intel MultiProcessor Specification 3
ここまで適切に設定するとブート時にvCPUが検出され、メッセージが流れるようになる。
$ dmesg
[ 0.038781][ T0] MPTABLE: processor found.
[ 0.039497][ T0] Processor #0 (Bootup-CPU)
[ 0.040918][ T0] MPTABLE: processor found.
[ 0.041626][ T0] Processor #1
e02bd77 ゲストカーネルで CONFIG_SMP を有効化
なぜかこのタイミングでCONFIG_SMP
フラグの存在に気づく。
もちろんこのフラグの有効化は大前提なので対応した。
9a53099 ioctl KVM_RUN のEAGAIN ハンドリング
この段階でCPU数を2個に増やすとなぜかgokvm
がパニックするようになった。
調べてみると、APに対応するioctl KVM_RUN
からEAGAIN
が返ってきていた。
なぜEAGAIN
なのか。KVMが返しているはずなので辿っていく。
KVMはvCPUごとに内部状態を持っていることがわかった6。
その解説を読むと、APはINITシグナルによって初期化され、それまでは
KVM_MP_STATE_UNINITIALIZED
状態のようだ。
KVM_MP_STATE_UNINITIALIZED
状態の時にioctl KVM_RUN
を読み出すと、
EAGAIN
が返ってくることがKVMのコードから確認できた 7。
KVM_MP_STATE_UNINITIALIZED:
the vcpu is an application processor (AP) which has not yet received an INIT signal [x86]
ゲストカーネルが動き出した時点ではBSPのみが動いていて、
LAPICを使ったINIT-SIPI-SIPI
シーケンスに従ってAPが動き出すということもここで知った(要調査)。
[ 0.713385][ T1] do_boot_cpu:1057: smpboot: Setting warm reset code and vector.
[ 0.715064][ T1] wakeup_secondary_cpu_via_init:805: smpboot: Asserting INIT
[ 0.716266][ T1] wakeup_secondary_cpu_via_init:816: smpboot: Waiting for send to finish...
[ 0.717332][ T1] wakeup_secondary_cpu_via_init:821: smpboot: Deasserting INIT
[ 0.718299][ T1] wakeup_secondary_cpu_via_init:827: smpboot: Waiting for send to finish...
[ 0.719375][ T1] wakeup_secondary_cpu_via_init:846: smpboot: #startup loops: 2
[ 0.720569][ T1] wakeup_secondary_cpu_via_init:849: smpboot: Sending STARTUP #1
[ 0.721341][ T1] wakeup_secondary_cpu_via_init:853: smpboot: After apic_write
[ 0.722505][ T1] wakeup_secondary_cpu_via_init:873: smpboot: Startup point 1
[ 0.723653][ T1] wakeup_secondary_cpu_via_init:875: smpboot: Waiting for send to finish...
[ 0.725354][ T1] wakeup_secondary_cpu_via_init:849: smpboot: Sending STARTUP #2
[ 0.726552][ T1] wakeup_secondary_cpu_via_init:853: smpboot: After apic_write
[ 0.727728][ T1] wakeup_secondary_cpu_via_init:873: smpboot: Startup point 1
[ 0.729328][ T1] wakeup_secondary_cpu_via_init:875: smpboot: Waiting for send to finish...
[ 0.730929][ T1] wakeup_secondary_cpu_via_init:892: smpboot: After Startup
参考としてkvmtoolの実装を見ると、やはりEAGAIN
を無視するような実装になっていた 8
c8d5459 任意のvCPU数対応
これまでは2vCPUに限定していたが、このコミットで任意のvCPU数に対応した。 特筆すべきことはなさそうに思う。
おわりに
なんとか動くところまで実装できてよかった。後から振り返るとほとんどKVMがよしなにやってくれていることが分かる。 SMP環境でのCPU初期化に関するデータ構造や処理の流れについて勉強になった。 まだまだやりたいことはいっぱいあるけど少しずつ粛々と。