bobuhiro11/gokvm - GitHub

はじめに

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_intelPhysPtrが指す先 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_tablestruct mpf_intelの直後に配置した。この場所が正しいのかは分からない。 ここで、Local APIC(LAPIC)のアドレスを設定する。LAPICは典型的に 0xFEE00000 に置かれるので、それに倣った。 vCPUがこのメモリにアクセスすると、KVMを通してホストカーネルがエミュレートしてくれるLAPICへのアクセスに置き換えられるようだ。 メモリアクセスをトラップしてユーザランドでエミュレーションしたり… といったことはやらなくて大丈夫だった。 無事ブート時に認識されるようになった。 本来は、struct mpc_cpustruct 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_cpustruct mpc_bus などが任意の個数連続する。 ここではとりあえずvCPU数を2とした。 エントリはTypeによって区別される。

entry_types 出典:Intel MultiProcessor Specification 3

Local APICはvCPUごとに存在しているので、vCPUの通し番号を流用した。 vCPUごとに有効・無効、BSP・APの制御フラグがあるので、注意する。 1つ目のvCPUのみをBSPとして、残りをAPとした。 Local APICとCPUの関連、BSP・APの区別については下図が分かりやすい。

apic 出典: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初期化に関するデータ構造や処理の流れについて勉強になった。 まだまだやりたいことはいっぱいあるけど少しずつ粛々と。