仮想マシンとBIOSと準仮想化
はじめに
PCエミュレータや完全仮想化の仮想マシンの場合は、当然ながらBIOS(もしくはEFI)をエミュレーションする必要があります。BIOSが用意しなければならない情報には、例えばNUMAにおけるCPUやメモリの構成情報があります*1。物理マシンの場合は、基本的な構成は固定のため(BIOSでon/offやパラメタ変更はできますが)、それらの情報はBIOS ROMに固定値を書きこんでおくことができます。一方、仮想マシンの場合は、柔軟にマシン構成を変更するために、BIOSに固定値を持たせるのは好ましくありません。
高機能なPCエミュレータや仮想マシンでは、起動時にマシン構成をBIOSソフトウェアに渡す機能が備わっています。KVM(qemu)では、コマンドライン引数で渡された構成情報(例えば-smpなど)をBIOS(現在はSeaBIOS)ソフトウェアに渡すための機能を持っています。もちろんBIOSソフトウェア側も情報を受け取る仕組みを持っています。つまり、BIOSソフトウェアも準仮想化対応していると言えます。
概要
ポート番号、識別番号(とデータサイズ/形式)、動作手順がプロトコルになっている。
BIOS関係の基礎知識
注意
実はこの辺りのことはあまり詳しくないので、間違っているかもしれません。ご注意ください。(ツッコミ歓迎です。)
SeaBIOSのファイルリスト
まずはSeaBIOSのファイルリストを見てみましょう。
$ ls -1 src/ # 省略 paravirt.c paravirt.h # 省略 virtio-blk.c virtio-blk.h virtio-pci.c virtio-pci.h virtio-ring.c virtio-ring.h
paravirtといういかにも準仮想化ぽい名前のファイルがあります。またvirtio向けのファイルもあります。どうやらSeaBIOSはvirtio-blkを読むことができるようです。確かによく考えるとvirtio-blkデバイスからブートするとき(MBRからブートローダを読み出すとき)には必要ですよね。
virtioの方は置いておいて、今回はparavirt方を見ていくことにします。
SeaBIOSの準仮想化コード
paravirt.cやparavirt.hを見てみるとわかりますが、マクロや関数名のプリフィックスがqemuになっています。完全にqemu向けに作られていますね。
このqemu_*関数を呼び出しているファイルは以下の通りです。
$ grep -r qemu_ src |cut -f 1 -d ' ' |cut -f 1 -d ':' |sort -u src/acpi.c src/boot.c src/mptable.c src/optionroms.c src/paravirt.c src/paravirt.h src/post.c src/shadow.c src/smbios.c src/smp.c src/util.h
以下では、わかり易そうなsmp.c(CPU数関連)のコードを見ていきます。当該コードはこんな感じです。
MaxCountCPUs = qemu_cfg_get_max_cpus(); if (!MaxCountCPUs || MaxCountCPUs < CountCPUs) MaxCountCPUs = CountCPUs; dprintf(1, "Found %d cpu(s) max supported %d cpu(s)\n", readl(&CountCPUs), MaxCountCPUs);
どうやら最大CPU数を受け取っているようです。コードを見てみると、稼働中のCPUは別の場所でprobeしているみたいなので、空きソケットを含めたCPUソケット(もしくはコアの数)のことのようです。
今度はMaxCountCPUsでgrepしてみます。
$ grep -r MaxCountCPUs src src/smp.c:u32 MaxCountCPUs VAR16VISIBLE; src/smp.c: MaxCountCPUs = 1; src/smp.c: MaxCountCPUs = qemu_cfg_get_max_cpus(); src/smp.c: if (!MaxCountCPUs || MaxCountCPUs < CountCPUs) src/smp.c: MaxCountCPUs = CountCPUs; src/smp.c: MaxCountCPUs); src/mptable.c: for (i = 0; i < MaxCountCPUs; i+=pkgcpus) { src/acpi.c: + sizeof(struct madt_processor_apic) * MaxCountCPUs src/acpi.c: for (i=0; i<MaxCountCPUs; i++) { src/acpi.c: int acpi_cpus = MaxCountCPUs > 0xff ? 0xff : MaxCountCPUs; src/acpi.c: u64 *numadata = malloc_tmphigh(sizeof(u64) * (MaxCountCPUs + nb_numa_nodes)); src/acpi.c: qemu_cfg_get_numa_data(numadata, MaxCountCPUs + nb_numa_nodes); src/acpi.c: sizeof(struct srat_processor_affinity) * MaxCountCPUs + src/acpi.c: for (i = 0; i < MaxCountCPUs; ++i) {src/smbios.c: for (cpu_num = 1; cpu_num <= MaxCountCPUs; cpu_num++) src/util.h:extern u32 MaxCountCPUs;
MP-TableやACPIのデーブルを生成するときに使っているようです。
どう使われてるかは置いておいて、最大CPU数をどうやってqemuから受け取っているのか調べるため、qemu_cfg_get_max_cpus関数を見てみます。
u16 qemu_cfg_get_max_cpus(void) { u16 cnt; if (!qemu_cfg_present) return 0; qemu_cfg_read_entry(&cnt, QEMU_CFG_MAX_CPUS, sizeof(cnt)); return cnt; }
static void qemu_cfg_read_entry(void *buf, int e, int len) { qemu_cfg_select(e); qemu_cfg_read(buf, len); }
static void qemu_cfg_select(u16 f) { outw(f, PORT_QEMU_CFG_CTL); } static void qemu_cfg_read(u8 *buf, int len) { insb(PORT_QEMU_CFG_DATA, buf, len); }
PORT_QEMU_CFG_CTL番号のI/OポートにQEMU_CFG_MAX_CPUSを書きこんだ後、PORT_QEMU_CFG_DATA番号のI/Oポートを読みだしています。どうやら、コントロールポートに欲しいデータの識別番号を書きこみ、その後データポートから当該データを(決められたサイズ分)読み出す、といったプロトコルでqemuからデータを受け取っているようです*2。
I/Oポート番号はioport.hに定義されています。
#define PORT_QEMU_CFG_CTL 0x0510 #define PORT_QEMU_CFG_DATA 0x0511
このポート番号をヒントにqemu側のコードを見つけられそうです。
qemu側のコード
qemu-kvmのコードをgrepしてみます。(0x0510では見つかりませんでした。。。)
$ grep -r 0x510 hw/ hw/fw_cfg.h: uint16_t select; /* write this to 0x510 to read it */ hw/musicpal.c:#define MP_GPIO_IN_HI 0x510 hw/pc.c:#define BIOS_CFG_IOPORT 0x510 hw/omap2.c: [ 32] = { 0x51000, 0x1000, 32 | 16 | 8 }, /* L4TA10 */ hw/sun4u.c:#define BIOS_CFG_IOPORT 0x510 hw/pl061.c: case 0x510: /* Pull-up */ hw/pl061.c: case 0x510: /* Pull-up */
hw/pc.cのマクロ定義が当たりのようです。
このポート番号はbochs_bios_init関数内*3で、fw_cfg_init関数に渡されています。
fw_cfg = fw_cfg_init(BIOS_CFG_IOPORT, BIOS_CFG_IOPORT + 1, 0, 0);
BIOS_CFG_IOPORT + 1でコントロールポート番号も渡していますね。
fw_cfg_init()@hw/fw_cfg.cの中身を見てみましょう。
dev = qdev_create(NULL, "fw_cfg"); qdev_prop_set_uint32(dev, "ctl_iobase", ctl_port); qdev_prop_set_uint32(dev, "data_iobase", data_port); qdev_init_nofail(dev); d = sysbus_from_qdev(dev); s = DO_UPCAST(FWCfgState, busdev.qdev, dev); // 省略 fw_cfg_add_bytes(s, FW_CFG_SIGNATURE, (uint8_t *)"QEMU", 4); fw_cfg_add_bytes(s, FW_CFG_UUID, qemu_uuid, 16); fw_cfg_add_i16(s, FW_CFG_NOGRAPHIC, (uint16_t)(display_type == DT_NOGRAPHIC)); fw_cfg_add_i16(s, FW_CFG_NB_CPUS, (uint16_t)smp_cpus); fw_cfg_add_i16(s, FW_CFG_MAX_CPUS, (uint16_t)max_cpus); fw_cfg_add_i16(s, FW_CFG_BOOT_MENU, (uint16_t)boot_menu);
FW_CFG_MAX_CPUSをキーにmax_cpusで最大CPU数が設定されているのがわかります。
fw_cfg_add_i16関数を追ってみます。
int fw_cfg_add_i16(FWCfgState *s, uint16_t key, uint16_t value) { uint16_t *copy; copy = qemu_malloc(sizeof(value)); *copy = cpu_to_le16(value); return fw_cfg_add_bytes(s, key, (uint8_t *)copy, sizeof(value)); } int fw_cfg_add_bytes(FWCfgState *s, uint16_t key, uint8_t *data, uint32_t len) { int arch = !!(key & FW_CFG_ARCH_LOCAL); key &= FW_CFG_ENTRY_MASK; if (key >= FW_CFG_MAX_ENTRY) return 0; s->entries[arch][key].data = data; s->entries[arch][key].len = len; return 1; }
FWCfgState#entriesにI/Oポート番号とデータ、データサイズが設定されています。
このentriesにアクセスしているのは、fw_cfg_read関数です。(他にもありますが、関係ないので省略。)
static uint8_t fw_cfg_read(FWCfgState *s) { int arch = !!(s->cur_entry & FW_CFG_ARCH_LOCAL); FWCfgEntry *e = &s->entries[arch][s->cur_entry & FW_CFG_ENTRY_MASK]; uint8_t ret; if (s->cur_entry == FW_CFG_INVALID || !e->data || s->cur_offset >= e->len) ret = 0; else ret = e->data[s->cur_offset++]; FW_CFG_DPRINTF("read %d\n", ret); return ret; }
s->cur_entryが指すエントリのデータを取っているようです*4。s->cur_entryはfw_cfg_select関数で設定されています。
static int fw_cfg_select(FWCfgState *s, uint16_t key) { int ret; s->cur_offset = 0; if ((key & FW_CFG_ENTRY_MASK) >= FW_CFG_MAX_ENTRY) { s->cur_entry = FW_CFG_INVALID; ret = 0; } else { s->cur_entry = key; ret = 1; } FW_CFG_DPRINTF("select key %d (%sfound)\n", key, ret ? "" : "not "); return ret; }
fw_cfg_select関数はfw_cfg_io_writew関数から呼ばれています。SeaBIOSがoutwでコントロールポートにアクセスしていたので、どうやら合ってそうです。
ちなみにfw_cfg_read関数の方はfw_cfg_io_readbから呼ばれています。
この2つの関数はfw_cfg_init1関数でコールバックとして登録されています。
if (s->ctl_iobase) { register_ioport_write(s->ctl_iobase, 2, 2, fw_cfg_io_writew, s); } if (s->data_iobase) { register_ioport_read(s->data_iobase, 1, 1, fw_cfg_io_readb, s); register_ioport_write(s->data_iobase, 1, 1, fw_cfg_io_writeb, s); }
register_*がSeaBIOS(やゲストOS)でin/out命令が発行されたときに呼び出されるコールバックを登録する関数なのでしょう。
これでSeaBIOSとqemuの動作が繋がりました。
最後に、max_cpusについて調べてみます。qemu-options.hxの定義がわかり易そうです。
DEF("smp", HAS_ARG, QEMU_OPTION_smp,
"-smp n[,maxcpus=cpus][,cores=cores][,threads=threads][,sockets=sockets]\n"
" set the number of CPUs to 'n' [default=1]\n"
" maxcpus= maximum number of total cpus, including\n"
" offline CPUs for hotplug, etc\n"
" cores= number of CPU cores on one socket\n"
" threads= number of threads on one CPU core\n"
" sockets= number of discrete sockets in the system\n",
QEMU_ARCH_ALL)
STEXI
@item -smp @var{n}[,cores=@var{cores}][,threads=@var{threads}][,sockets=@var{sockets}][,maxcpus=@var{maxcpus}]
@findex -smp
Simulate an SMP system with @var{n} CPUs. On the PC target, up to 255
CPUs are supported. On Sparc32 target, Linux limits the number of usable CPUs
to 4.
For the PC target, the number of @var{cores} per socket, the number
of @var{threads} per cores and the total number of @var{sockets} can be
specified. Missing values will be computed. If any on the three values is
given, the total number of CPUs @var{n} can be omitted. @var{maxcpus}
specifies the maximum number of hotpluggable CPUs.
ETEXI
qemu -smp 2,maxcpus=4などとコマンドライン引数に指定すると、最大CPU数を設定できるようです。その数値が今まで追ってきた仕組みでSeaBIOSに渡されているのでしょう。
おわりに
というわけで、SeaBIOSがqemuからデータを受け取るコードを見てきました。分かってしまえば、やっていることは難しく無かったと思います。(むしろ受け取ったデータからACPIテーブルなどを生成する方が面倒そうです。)
ちなみに、このようなデータ受け渡しは、昔qemuが採用していたBochsOSやXenでもやられているようです。調べていないですが、VirtualBoxやVMwareも同じようなことをやっているのではないかと推測します(違ってたら教えてください)。