仮想マシンとBIOSと準仮想化

はじめに

PCエミュレータや完全仮想化の仮想マシンの場合は、当然ながらBIOS(もしくはEFI)をエミュレーションする必要があります。BIOSが用意しなければならない情報には、例えばNUMAにおけるCPUやメモリの構成情報があります*1。物理マシンの場合は、基本的な構成は固定のため(BIOSでon/offやパラメタ変更はできますが)、それらの情報はBIOS ROMに固定値を書きこんでおくことができます。一方、仮想マシンの場合は、柔軟にマシン構成を変更するために、BIOSに固定値を持たせるのは好ましくありません。

高機能なPCエミュレータ仮想マシンでは、起動時にマシン構成をBIOSソフトウェアに渡す機能が備わっています。KVM(qemu)では、コマンドライン引数で渡された構成情報(例えば-smpなど)をBIOS(現在はSeaBIOS)ソフトウェアに渡すための機能を持っています。もちろんBIOSソフトウェア側も情報を受け取る仕組みを持っています。つまり、BIOSソフトウェアも準仮想化対応していると言えます。

今回はSeaBIOSがどうやってqemuから情報を受け取っているのか、当該ソースコードを追ってみたいと思います。

概要

  • SeaBIOSはI/Oポート経由でqemuからデータを受け取っている
  • コントロール用とデータ用のポートが用意されている
  • 扱うデータ毎に識別番号(とデータサイズ/形式)が決められている
  • 動作手順
    • qemu: 識別番号とそのデータのペアを登録
    • SeaBIOS: コントロールポートにデータ識別番号を書き込む
    • qemu: 書きこまれた識別番号を元にデータを用意する
    • SeaBIOS: データポートを読み込む
    • qemu: 用意したデータを返す

ポート番号、識別番号(とデータサイズ/形式)、動作手順がプロトコルになっている。

注意

実はこの辺りのことはあまり詳しくないので、間違っているかもしれません。ご注意ください。(ツッコミ歓迎です。)

調査対象

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.cparavirt.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でもやられているようです。調べていないですが、VirtualBoxVMwareも同じようなことをやっているのではないかと推測します(違ってたら教えてください)。

*1:OSにはACPIテーブル経由で情報を渡します。

*2:ちなみにinsb関数のlen引数はinsb命令につけるrep接頭辞に渡すもので、指定回数命令を繰り返して指定サイズのデータを取り出しています。

*3:BochsBIOSを使っていた名残りでしょうか。

*4:cur_offset++しているので、(I/Oポートのデータバスサイズより)大きなデータを複数回のin命令で取り出せることがわかります。

Rootfs over Virtfsでゲストを起動する

はじめに

Virtfsを使うと、ホストのディレクトリをゲストにmountさせることができます。ここではdebootstrap等でホストに用意したrootfsを使ってゲストをブートさせる方法について説明します。

What's Virtfs?

Virtfsは、"File system pass-through/Paravirtual file system"を実現する機能です。パススルーといえば、ゲスト(カーネル)がホストマシンの物理デバイスに直接アクセスする機能が一般的ですが、virtfsはファイルシステムで似たようなことを実現しています。Virtfsを使うと、ホストのディレクトリをゲストにmountさせることができ、その結果、ゲストのアプリケーションがホストのファイルに直接アクセスできるようになります。もし複数のゲストが同じホストのディレクトリをmountするならば、同じディレクトリが見えることになります。

これだけ聞くとNFSと同じじゃないかと思われるかもしれませんが、virtfsは(仮想)ネットワークデバイスを介した通信を必要としません。代わりに9P over VirtIO*1でゲスト・qemu間のAPI/データ転送を実現しています。(qemuが9Pサーバ、ゲストカーネルが9Pクライアントになります。)

Virtfsの詳しい説明は、開発者が公開している資料を参照ください。

環境

注意

今回はとりあえずブートさせることが目的なので、セキュリティに関しては何も考えていません。(qemuをrootで動かし、virtfsのsecurity_modelをnoneにしています。)

手順

  1. qemu-kvm再ビルド
  2. ゲストrootfs準備
  3. initrdカスタマイズ
  4. ブート!
  5. 動作確認

qemu-kvm再ビルド

残念ながらUbuntu添付のqemu-kvm (0.14.0+noroms4)はvirtfsが有効になっていません。まずは、再ビルドしてvirtfsが使えるようにします。

といっても、ソースコードを修正する必要はなく、以下のようにlibattr1-devパッケージをインストールした状態でビルドすればOKです。

sudo apt-get build-dep --no-install-recommends qemu-kvm
sudo apt-get install libattr1 libattr1-dev
apt-get source qemu-kvm
cd qemu-kvm-0.14.0+noroms/
dpkg-buildpackage

ゲストrootfs準備

debootstrapを使えば簡単に用意することができます。

mkdir natty_root
sudo debootstrap natty natty_root

initrdカスタマイズ

ブート時に9pファイルシステムをrootにmountできるようにinitrdを修正します。

mkdir initrd
cd initrd
zcat /boot/initrd.img-2.6.38-8-generic|cpio -i
# いろいろ修正(後述)
find . | cpio --quiet --dereference -o -H newc|gzip -c > ../initrd.img-2.6.38-8-virtfs

やらなければならないことは、

  1. qemu引数に指定するmount_tagと9pファイルシステムのmount時に指定する識別名*2を一致させるため、カーネル引数経由で渡すmount_tagをinitrdのinitで解釈できるようにする
  2. 9pファイルシステムのmount前に9p/virtio関連のカーネルモジュールをロードするため、当該モジュールをinitrdに入れておく*3

修正箇所/修正作業は以下のとおりです。

  • init修正、scripts/9pを用意
  • 9p関連のカーネルモジュールをコピー、depmod -a

init(シェルスクリプト)は以下のdiffのように修正しました。

diff -u initrd.orig/init initrd/init
--- initrd.orig/init	2011-05-16 10:37:02.796295655 +0900
+++ initrd/init	2011-05-16 10:28:30.903695192 +0900
@@ -52,6 +52,7 @@
 export blacklist=
 export resume=
 export resume_offset=
+export mount_tag
 
 # mdadm needs hostname to be set. This has to be done before the udev rules are called!
 if [ -f "/etc/hostname" ]; then
@@ -207,6 +208,9 @@
 	hwaddr=*)
 		BOOTIF=${x#BOOTIF=}
 		;;
+	mount_tag=*)
+		mount_tag=${x#mount_tag=}
+		;;
 	esac
 done
 
@@ -326,6 +330,7 @@
 unset readonly
 unset resume
 unset resume_offset
+unset mount_tag
 
 # Chain to real filesystem
 exec run-init ${rootmnt} ${init} "$@" <${rootmnt}/dev/console >${rootmnt}/dev/console 2>&1

scripts/9pは新規に用意しました。(scripts/nfsを参考にしました。)

# 9p filesystem mounting                        -*- shell-script -*-

mountroot()
{
        modprobe virtio_pci
        modprobe 9p
        modprobe 9pnet_virtio
        mount -t 9p -o trans=virtio ${mount_tag} ${rootmnt}
}

initrdに入れなければならなかったカーネルモジュールは以下の3つでした。virtio関連のモジュールは最初から入っていました。

/lib/modules/2.6.38-8-generic/kernel/fs/9p/9p.ko
/lib/modules/2.6.38-8-generic/kernel/net/9p/9pnet.ko
/lib/modules/2.6.38-8-generic/kernel/net/9p/9pnet_virtio.ko

cd initrd/
cp -a /sbin/depmod sbin/
sudo chroot . /sbin/depmod -a 2.6.38-8-generic

以上でinitrdの準備が終わりました。

ブート!

というわけでゲストのブートです。カーネルはホストにあるものを使います。識別名は-append 'mount_tag=natty'と指定、同じ識別名を-virtfsの引数の一部に指定します。

sudo kvm -enable-kvm -kernel /boot/vmlinuz-2.6.38-8-generic -initrd /path/to/initrd.img-2.6.38-8-virtfs -append 'mount_tag=natty single' -virtfs local,path=/path/to/natty_root,mount_tag=natty,security_model=none -curses

上手くいくとブートするはずです。

訂正:試したのはシングルユーザモードだけです。

動作確認

簡単な動作確認です。確かにrootfsが9pファイルシステムになっています。

guest# mount
natty on / type 9p (rw)
none on /proc type proc (rw,noexec,nosuid,nodev)
none on /sys type sysfs (rw,noexec,nosuid,nodev)
none on /dev type devtmpfs (rw,mode=0755)
none on /dev/pts type devpts (rw,noexec,nosuid,gid=5,mode=0620)
none on /sys/fs/fuse/connections type fusectl (rw)
none on /sys/kernel/debug type debugfs (rw)
none on /sys/kernel/security type securityfs (rw)
none on /dev/shm type tmpfs (rw,nosuid,nodev)
none on /var/run type tmpfs (rw,nosuid,mode=0755)
none on /var/lock type tmpfs (rw,noexec,nosuid,nodev)
guest# touch /tmp/hellovirtfs

↑ゲストで作った(ようにみえる)ファイルが、↓ホストにもちゃんと生成されていることが確認できました。

$ ls /path/to/natty_root/tmp
hellovirtfs

おわりに

なにはともあれ、ホストのファイルシステム上にあるrootfsでゲストがブートできました。

ゲスト環境を構築するときは、virt-installなどを使ってディスクイメージを用意するのが一般的ですが、virtfsの場合、代わりにdebootstrapやOpenVZのprecreated rootfsなどを使えるので多少は楽になるのではないでしょうか。

btrfsのsubvolumeを使うと、qcow2のように一回用意したrootfsで複数の(使い捨て)環境が用意できてさらに便利だと思います。

追記:kvm tools: Add virtio-9pのスレッドでも似たような話をしてますね。ここで紹介したやり方とは違うみたいですが。。。

*1:ゲストカーネル側の機能は実は2.6.24、つまりvirtioが最初にマージされたときにマージされています。

*2:仮想ファイルシステムの場合にnoneと指定することが多いアレ。

*3:もちろんカーネルに組み込んでも良いです。今回はUbuntu配布のカーネルをそのまま使いたかったのでこうしました。

mmapとページフォールト

はじめに

メモリ管理周りで勘違いしていた部分をコードを追いながら説明してみます。

概要

Q1: qemuはゲストメモリ領域をmmap(2)してるんだよね?

A1: してますがコードには呼び出し箇所はありません*1mmapposix_memalign(3)内で呼び出されます。

Q2: ゲストより先に(qemuメモリ空間内の)ゲストメモリ領域にqemuがアクセスした場合どうなる?

A2: qemuページフォールトが起きますが、SPTEは割当てられません。そのため、ゲストが当該ページにアクセスすると再度ページフォールトが起きます。SPTEが設定されるのはこのときです。

調査対象バージョン

  • x86_64 (Intel EPTあり)
    • シャドウページテーブルはさすがに今さらなので(^^;
  • linux-2.6.37.6
    • 2.6.38はasynchronous page faultを実装していて読みにくかったので2.6.37にしました
  • qemu-kvm-0.14.0

mmap(2)はどこで呼ばれている?

まずは、KVMqemuとゲストのメモリの関係を説明する際によく登場するmmapが、実際にどこで呼ばれているか調べてみました。とりあえずqemuをstraceしてみると、

mmap(NULL, 268443648, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa9532fb000
madvise(0x7fa9532fc000, 268435456, 0xc /* MADV_??? */) = 0
ioctl(3, KVM_CHECK_EXTENSION, 0x10) = 1
ioctl(4, KVM_SET_USER_MEMORY_REGION, 0x7fff2ab282e0) = 0
ioctl(4, KVM_SET_USER_MEMORY_REGION, 0x7fff2ab282e0) = 0

確かに呼ばれていることがわかります。mmapに続いて、madvise(2)とioctl(2)で、当該メモリ領域がKSMでマージ可能であることとゲストのメモリ領域であることをカーネルに伝えます。

qemu-kvmのコードでは、qemu_ram_alloc_from_ptr@exec.c辺りが該当するようです。この少し上にmmapが呼ばれているのが見えるのですが、それはS390向けのもので、x86の場合はqemu_vmalloc@oslib-posix.cが呼ばれるみたいです。この関数を追っていくと、最後にposix_memalign(3)を呼んでいます。posix_memalignは指定したアラインメントでメモリを割り当てる、どちらかというとmallocに近いライブラリ関数です(freeできますし)。

あれ?mmapは?と訝しんで他のコードも見てみたのですが、どうもこのコードで正しいようです。もしやと思い、straceではなくltrace -S(ライブラリコールとシステムコールを同時にトレースするコマンド)を使ってみると、

posix_memalign(0x7fff03c4ba60, 4096, 0x10000000, 0x2cc2127, 56
SYS_mmap(0, 0x10002000, 3, 34, 0xffffffff) = 0x7f6db7c05000<... posix_memalign resumed> ) = 0
madvise(0x7f6db7c06000, 0x10000000, 12, 0x7f6db7c05ff0, 0xffffffff
SYS_madvise(0x7f6db7c06000, 268435456, 12) = 0<... madvise resumed> ) = 0

ありました。posix_memalignがmmapを呼んでます。そういえばmallocmmapすることがあるので、この動作は最初に気づくべきでした。。。

ちなみにioctlはkvm_vm_ioctl@kvm-all.cで呼ばれます。この関数は、hw/pc.cで、qemu_ram_alloc(_from_ptr)の後に呼び出されているcpu_register_physical_memoryを辿っていくと見つかります。(コールバックでKVM向けのコードが呼ばれているみたいです。)なおメモリ領域はメモリスロット(qemu-kvmではKVMSlot、カーネル内ではkvm_memory_slot構造体)で管理されています。

qemuが先にページフォールトを起こしたらどうなる?

SPTEがまだ存在しないページにゲストがアクセスすると、ページフォールトが起きてKVMに処理が移り、SPTEを設定したあと処理がゲストに戻される。ゲストでページフォールトが起きるときはこんな感じで動作するはず。では、仮想DMAなどで先にqemuプロセスの方でページフォールトが起きたときはどうなるんだろう?という疑問がふと湧いてきました。

前述のゲストメモリ領域に対して実行されるioctlの行く末を、カーネルの中まで追ってみてもメモリ領域に対して特に特別な処理を行なっていませんでした。つまり、qemu(のゲストメモリ領域)でページフォールトが起きたときにもLinuxの通常のメモリ管理のコード(ページフォールトハンドラ)が呼ばれるようです。SPTEはこの際に設定されるものだと思っていたので、またしても、あれ?と思いました。

実はLinuxの通常のメモリ管理コードに(mmu_notifierのような)フックがあるのかと思い少しコードを追ってみました。

Linuxページフォールトが起きると、IDTに設定されたdo_page_faultからhandle_mm_faultが呼ばれます。handle_mm_faultでは当該メモリ領域の種類によって処理を振り分けます。qemuのゲストメモリ領域はanonymousページなのでdo_anonymous_pageが呼ばれ、フォールを起こしたアドレスのページ割当て、PTEの設定などが行われます。

ひと通りコードを見てみたのですが、残念ながらKVMのコードを呼び出している場所はありませんでした。どうやら、SPTEは設定されず、ゲストが当該ページにアクセスされたときに再度ページフォールトが起きる仕様のようです*2

というわけで、もう知りたいことはわかったのですが、せっかくなので今度はKVM(EPT)のページフォールト処理を見ていきます。

EPTの場合、VM exitでKVMに処理が戻ったあとexit reasonを調べてページフォールトが起きたとわかったら、tdp_page_faultへ飛んできます*3。ゲスト(ページ)フレーム番号(GFN)が渡されるので、tdp_page_faultはこれを元にPFNを割り出し、GFNとPFNをマッピングするSPTE*4を設定します。

PFNの割り出しはgfn_to_pfnという関数で行われます。この関数はまずKVMが管理するメモリスロットを使ってGFNからホスト仮想アドレス(HVA)、つまりqemuの仮想アドレスを算出し、仮想アドレスに対応するページを取得、そしてそのページからPFNを手にいれます。名前からは想像できないくらい色々な処理をやっています。

HVAから対応するページを取得するために、gfn_to_pfnはget_user_pages_fastというLinuxメモリ管理ではお馴染みのget_pageの派生関数を呼びます。get_user_pages_fastはfollow_pageというページテーブルを辿る関数を使ってページを取得しようとします。もしページが存在しない場合(またCoWページに書き込みアクセスした場合など)はhandle_mm_faultを呼び出してページを割り当て、そのページのPTEを設定します。この処理は、通常のページフォールトハンドラと同じです。つまり、この時点でqemu仮想アドレス空間の当該ページは既にPTEが設定され、次にqemuがアクセスしてもページフォールトは起きない、ということです。

いずれにせよページを割り当てたのでそこからPFNを取得することができるので、次は、GFNとPFNをマッピングするSPTEを設定する処理です。tdp_page_faultはgfn_to_pfnで得たPFNとGFNを__direct_mapに渡します。この関数からmmu_set_spteset_spteupdate_spteが呼ばれSPTEが設定されます。

かなり大雑把です。詳細はコードを参照ください。kvm.txtでコードで使われる用語を把握してから読むと理解しやすくなると思います。

おわりに

実際にコードを追ってみたことで、勘違いしていたり理解が浅かった部分が少し解消されました。やっぱり正しく理解するにはコードを読まないといけませんね。

ところで、コードを読むまでは、ゲストのメモリ領域はqemuの仮想アドレス空間mmapマッピングされている、という感じで理解していたのですが、実際は、qemuのメモリ領域の一部をゲストのメモリ領域とみなしているだけ、の方が実状に近いみたいです。

*1:x86の場合です。昔はあったのかも?

*2:pr_debugを使って実際にそうなってることは確認しましたが、煩雑なので省略。もっと賢い方法ないですかね。

*3:TDP=Two-Dimensional Paging。Intel EPTやAMD VRI(NPT)のアーキテクチャ中立な名称です。

*4:EPTでもシャドウという名前を使っているようです。

qemu-0.12.1

http://download.savannah.gnu.org/releases/qemu/qemu-0.12.1.tar.gz

0.11からの主要な変更点は以下の通り。

 - target-arm: FP16 and Cortex-A9 cpu support
 - target-i386: change bios from PC-BIOS to SeaBIOS
 - target-i386: change netboot roms from etherboot to gPXE
 - target-i386: remove kqemu support
 - target-s390: introduce new s390 target (KVM-only)
 - live migration: support migration with non shared storage
 - live migration: make save/restore data driven (VMState)
 - monitor: introduce machine-protocol (QMP)
 - qdev: convert many devices, support reading config from file
 - pci: infrastructure to support 64-bit busses
 - net: introduce -netdev to support point-to-point networking
 - net: add support for GSO and checksum offload
 - tons of bug fixes throughout the code base

SeaBIOSへの移行、kqemuの廃止、非共有ストレージでのライブマイグレーションサポート、QMP、qdev、GSO & チェックサムオフロードなど見どころがたくさんありますね。

追記:qemuに合わせてqemu-kvm-0.12.1もリリースされたみたいです。すぐにコンパイルエラーが報告されてますが(^^;

http://sourceforge.net/projects/kvm/files/

ioeventfd & irqfd

2.6.32でマージされた*1KVMの新機能ioeventfdとirqfdについて調べてみました。

eventfd

どちらもeventfd(2)を利用しているので、まずはその概略から。

eventfdは2.6.22から利用可能なユーザプロセス間もしくはユーザ・カーネル間のイベント通知専用のシステムコールです。eventfdはeventfdオブジェクトと呼ばれるディスクリプタを返します。利用者はこのディスクリプタに対してread/write/select/pollなどをすることでイベントの通知や受け取りを行ないます。この種のディスクリプタを介したイベント通知はpipeやsocketでも可能ですが、特化している分eventfdの方が簡潔に書けて軽量です。

同種のシステムコールにsignalfdやtimerfdなどがあり、各種ディスクリプタとまとめてselect/pollすることができて便利です。

共通

  • 主な機能は、ユーザモード(qemu)へ遷移する事なしに、ゲストへ割り込みを挿入したり、PIO/MMIOをハンドリングすることです*2
  • どちらの機能もシステムコールではありません
    • KVMではお馴染みのioctl(2)を使います
  • どちらも利用者はまだいません
    • qemu-kvmにも実際に利用しているコードはありません*3
    • おそらくvhost-net(virtio in host kernel)が最初の利用者になるはずです

ioeventfd

ioeventfdゲストのPIO/MMIOによる書き込みをeventfdの通知に変換してくれる機能です。

qemuはPIO/MMIO領域(アドレスとサイズ)とeventfdオブジェクトを引数にioeventfd(ioctl)を呼び出します。KVMは指定領域と同オブジェクトを自身のI/Oバスに登録(__kvm_io_bus_register_dev)し、そのコールバックにioeventfd独自のものを登録します。そのコールバックはeventfdオブジェクトにイベントを通知する(eventfd_signalを呼ぶ)だけです。これでPIO/MMIO書き込みがあればeventfdオブジェクトで待っているプロセスやカーネルの他のモジュールを起こすことができます。

vhost-netを例に挙げるならば、ゲストのvirtio_netのMMIO書き込みでホストカーネル側のvhost-netモジュールにパケット送信命令を発行することができるようになるでしょう。

irqfd

irqfdはゲストへの割り込み挿入を非同期に、またカーネルからも行なえるようにする機能です。

qemuはGSI*4とeventfdオブジェクトを引数にirqfd(ioctl)を呼び出します。KVMでは同オブジェクトをカーネル内pollに登録して、イベントがあれば当該IRQに割り込みを発行します。こうすることで、qemuからでもカーネルからでも割り込みを発行することができますし、間にeventfdを挟むことで、KVMの仕組みを知らない他のプロセス/カーネルモジュールも、内部構造を知る必要なく割り込みを発行することができます。

こちらもvhost-netのパケット受信時の割り込みに利用できそうですね。

雑感

どちらの機能もqemuをショートカットしてホスト・ゲスト間の軽量なイベント通知を行なうための基盤となります。やはり、ユーザ・カーネルモード遷移は処理によっては無視できないオーバヘッドになっていたということでしょうか。

あと、どう考えてもvhostのための機能とみて間違いないでしょうね (^^; virtio in host kernelは構想だけならKVM Forum 2007のKVM Para-Virtualized Guest Drivers [pdf]に既にあった話なので、ようやくここまで来たかーといったところでしょうか。

リンク

*1:irqfdはkvm-87、ioeventfdはkvm-88でkvm.gitに入っていたので知っている人にとっては今更かもしれませんが。。。

*2:現在もPIC仮想化などはqemuへの遷移を必要としませんが、そちらはゲスト←→KVMで完結する処理。ioeventfd/irqfdはゲスト←→KVM←→他のコンポーネントを結びつけるという点が違います

*3:irqfd(ioctl)を呼び出している関数はあるのですが、その関数を使っている関数がないみたいです

*4:Global System Interrupt: ACPIが提供するシステム全体の統一的な割り込み番号

Rawhide virt preview repo for Fedora 12

Fedora 12向けのvirt previewレポジトリが公開されました。

Fedora 11時代からありましたが、このレポジトリを使うと、仮想化関連パッケージの開発版(つまりFedora 13)を先取りして試すことができます。

yumの設定は以下のとおり。

# cat > /etc/yum.repos.d/fedora-virt-preview.repo << EOF
[rawvirt]
name=Virtualization Rawhide for Fedora 12
baseurl=http://jforbes.fedorapeople.org/virt-preview/f12/$basearch/
enabled=1
gpgcheck=0
EOF
# yum update

現時点ではFedora 12がリリースされたばかりなのでパッケージにあまり大きな違いはないと思いますが、いずれバージョンが一つ二つ違うような新しいパッケージに変わっていくと思います。

net-bridge

[Qemu-devel] [PATCH 0/4] net-bridge: rootless bridge support for qemu

ここ最近で一番盛り上がっているスレッド。

qemu(-kvm)を使ったことがある人ならば一度は不満に思ったことがある、tapを使うためにはroot権限が必要、という制限をなくそうという話題。