Hatena::ブログ(Diary)

KVM日記 RSSフィード

Linux KVM(Kernel-based Virtual Machine)好きの独り言

2011-05-29

VM間共有メモリivshmemを試してみる

はじめに

qemu(-kvm)にはVM間共有メモリ(Inter-VM shared memory: ivshmem)という機能があります。名前のとおりVM間の共有メモリを実現する機能です。

今回はこの機能を体験することが目的です。内部動作解析などは行なっていません。

How does ivshmem work?

以下のドキュメントを参考にしてください。

ざっくり説明すると、ホストのshmem(shm)をPCIデバイスを介してゲストに見せる、ということになると思います*1。ゲストでUIOドライバを使うと、ユーザレベルのプログラムから共有メモリに(比較的)簡単にアクセスできるようになります。

環境

ゲスト用のファイルは以下のように用意されている前提で以降の説明をしていきます。

  • ゲストrootfs: /path/to/natty_root
  • ゲストカーネル: /boot/vmlinuz-2.6.38-8-generic
  • ゲストinitrd: /path/to/initrd.img-2.6.38-8-virtfs

qemu(-kvm)がivshmem機能をもっているかどうかは以下のように確かめます。(Ubuntu 2011.04ならばあるはずです。)

$ kvm -device '?' 2>&1|grep ivshmem
name "ivshmem", bus PCI

環境構築

単純にshmemをゲストに見せるだけという構成も可能ですが、それだけではVM間で同期が取れない(更新通知ができない)ので、今回はivshmem-serverという中継サーバを使う構成を試します。ivshmem-serverがイベント通知をサポートしてくれます*2

通常のVM環境に加えて必要なものは以下の通り。すべてivshmemツールに含まれています。


ちなみにivshmemツールのディレクトリ構成は以下の通りです。

$ tree -d ivshmem
ivshmem
├── ivshmem-server
├── kernel_module
│   └── uio
├── scripts
├── startup_files
├── tests
│   ├── DumpSum
│   │   ├── Host
│   │   └── VM
│   ├── FTP
│   │   ├── Java
│   │   │   ├── old
│   │   │   └── org
│   │   │       └── ualberta
│   │   │           └── shm
│   │   └── VM
│   ├── Interrupts
│   │   └── VM
│   ├── Java
│   │   └── JNI
│   ├── Semaphores
│   │   ├── Host
│   │   └── VM
│   └── Spinlocks
│       ├── Host
│       └── VM
└── uio
    ├── benchmarks
    │   └── VM
    │       └── coyote
    └── tests
        ├── DumpSum
        └── Interrupts
            └── VM

34 directories

今回使うのは、ivshmem-server/, kernel_module/uio, tests/, uio/tests/Interrupts/VMです。

git cloneして必要なものをコンパイルします。

git clone git://gitorious.org/nahanni/guest-code.git ivshmem
cd ivshmem/
cd ivshmem-server/
make
cd -
cd kernel_module/uio
make
cd -
cd uio/tests/Interrupts/VM
cmake CMakeLists.txt
make

簡単に準備完了、と思ったのですが、ivshmem_server.cのコンパイルでエラーが出たので以下のように修正しました。

diff --git a/ivshmem-server/ivshmem_server.c b/ivshmem-server/ivshmem_server.c
index ae7a113..d187fa8 100644
--- a/ivshmem-server/ivshmem_server.c
+++ b/ivshmem-server/ivshmem_server.c
@@ -62,7 +62,10 @@ int main(int argc, char ** argv)
         exit(-1);
     }
 
-    ftruncate(s->shm_fd, s->shm_size);
+    if (ftruncate(s->shm_fd, s->shm_size) == -1) {
+        perror("ftruncate");
+        exit(1);
+    }
 
     s->conn_socket = create_listening_socket(s->path);
 

次は必要なファイルをゲストrootfsにインストールするのですが、せっかくなのでvirtfsを活用して以下のようにしました。

sudo cp ivshmem/kernel_module/uio/uio_ivshmem.ko /lib/modules/2.6.38-8-generic/kernel/drivers/uio/
sudo depmod -a
sudo mount --bind /lib/modules /path/to/natty_root/lib/modules
sudo mount --bind ivshmem/uio/tests/Interrupts/VM /path/to/natty_root/root/tests

カーネルモジュール(uio_ivshmem.ko)はホストへインストールして、モジュールディレクトリをゲストrootfsへbindマウントします(ゲストもホストと(ほぼ)同じ環境なのでこれで問題ありません)。同じくテストプログラムがあるディレクトリもbindマウントします。非常に簡単で良いですね*3

あとゲストでlspciを使いたかったのでpciutilsもインストールしました。

sudo chroot /path/to/natty_root/ apt-get install pciutils # エラーがでますが気にしない

実際に動かしてみる

まずはivshmem_serverを起動させます。

$ ./ivshmem_server
listening socket: /tmp/ivshmem_socket
shared object: ivshmem
shared object size: 1048576 (bytes)
vm_sockets (0) =

Waiting (maxfd = 4)

ivshmemという名前のshmemファイル(/dev/shm/ivshmem)を作って、/tmp/ivshmem_socketでqemuからの接続を待っているのがわかります。共有メモリのサイズは1 MB(デフォルト値)です。

次にVMを起動します。

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 -device ivshmem,size=1,chardev=ivshmem -chardev socket,path=/tmp/ivshmem_socket,id=ivshmem

-device ivshmem,size=1,chardev=ivshmem -chardev socket,path=/tmp/ivshmem_socket,id=ivshmemがivshmemを使うのに必要な引数です。

VMを起動すると、ivshmem_serverが接続を受け入れた旨を出力しているのがわかります。

#(cont.)

[NC] new connection
increasing vm slots
[NC] Live_vms[0]
        efd[0] = 6
[NC] trying to send fds to new connection
[NC] Connected (count = 0).
Live_count is 1
vm_sockets (1) = [5|6]

Waiting (maxfd = 5)

続いてもう一つVMを起動します。(上記と同じ引数で起動しました。つまり同じrootfsを参照しています。試しにやってみたところうまくいったのでこのまま作業を続けましたが、別々のrootfsを用意した方が良いと思います*4。)

#(cont.)

[NC] new connection
[NC] Live_vms[1]
        efd[0] = 8
[NC] trying to send fds to new connection
[NC] Connected (count = 1).
[UD] sending fd[1] to 0
        efd[0] = [8]
Live_count is 2
vm_sockets (2) = [5|6] [7|8]

Waiting (maxfd = 7)

ivshmem_serverがもう一つ接続を受け入れたことがわかります。

今度はゲストの中の作業です(2つのVMをkvm0, kvm1と呼ぶことにします)。lspciとdmesgの出力を見てみます。

root@kvm0:~# lspci -v -s 00:05.0
00:05.0 RAM memory: Red Hat, Inc Device 1110
        Subsystem: Red Hat, Inc Device 1100
        Physical Slot: 5
        Flags: fast devsel, IRQ 10
        Memory at f2022000 (32-bit, non-prefetchable) [size=256]
        Memory at f2023000 (32-bit, non-prefetchable) [size=4K]
        Memory at f2100000 (32-bit, non-prefetchable) [size=1M]
        Capabilities: [40] MSI-X: Enable- Count=1 Masked-
root@kvm0:~# dmesg
# 省略
uio_ivshmem 0000:00:05.0: PCI INT A -> Link[LNKA] -> GSI 10 (level, high) -> IRQ 10
uio_ivshmem 0000:00:05.0: irq 42 for MSI/MSI-X
MSI-X enabled

ivshmem PCIデバイスがRAM memoryという名前で認識されていることがわかります。またuio_ivshmemカーネルモジュールがロードされ初期化されています。さらにUIOデバイス用に/dev/uio0というキャラクタデバイスファイルが生成されています。当該デバイスファイルにivshmemツールに含まれるgetidentというプログラムを使うとIDを調べることができます。

root@kvm0:~/tests# ./getident /dev/uio0
ID is 0
exiting
root@kvm1:~/tests# ./getident /dev/uio0
ID is 1
exiting

それぞれ別のIDが振られていることが確認できます。

いよいよ本番です。uio_sendとuio_readというプログラムを動かしてみます。この2つのプログラムはivshmemを介してデータを送信/受信するだけです。

まずkvm0でuio_readを起動します。

root@kvm0:~/tests# ./uio_read
USAGE: uio_read <filename> <count>
root@kvm0:~/tests# ./uio_read /dev/uio0 10
[UIO] opening file /dev/uio0
[UIO] reading

すると、readシステムコールでブロックします。uio_sendからのデータを待っているようです。

次にkvm1でuio_sendを動かします。

root@kvm1:~/tests# ./uio_send
USAGE: uio_ioctl <filename> <count> <cmd> <dest>
root@kvm1:~/tests# ./uio_send /dev/uio0 10 zzz 0
[UIO] opening file /dev/uio0
[UIO] count is 10
[UIO] writing 0
[UIO] ping #0
[UIO] ping #1
[UIO] ping #2
[UIO] ping #3
[UIO] ping #4
[UIO] ping #5
[UIO] ping #6
[UIO] ping #7
[UIO] ping #8
[UIO] ping #9
[UIO] Exiting...

1秒おきにデータを書き込んでいるようです。そうするとuio_readの方も反応します。

#(cont.)
[UIO] buf is 1
[UIO] buf is 2
[UIO] buf is 3
[UIO] buf is 4
[UIO] buf is 5
[UIO] buf is 6
[UIO] buf is 7
[UIO] buf is 8
[UIO] buf is 9
[UIO] buf is 10
[UIO] Exiting...

おそらく、uio_sendの書き込みをkvm0(qemu)が受け取ってivshmem_serverへ通知し、その後ivshmem_serverがそのイベントをkvm1(qemu)へ通知し、kvm1のPCIデバイス割り込みがかかるという動作なのだと思います(注意: コードをほとんど読んでないので違うかもしれません)。


おわりに

なにはともあれ、ivshmemがちゃんと機能していることが確認できました。

単純なデータの受け渡しやイベント通知ならばネットワークを介しても可能ですが、大きなデータの共有や低オーバヘッドを実現したい場合にはivshmemは役に立つと思います。

内部解析はまた別の機会にやりたいと思います。

*1:shmemを使っているのでゲスト・ホスト間でもデータ共有が可能です。

*2:らしいです。詳しい動作はまだ調べていません。

*3:実際にはVMをブートさせた後にbindマウントしましたが、それでも問題なく動きました。

*4:btrfsであればsnapshotを作るところですが。

2011-05-22

仮想マシンと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: 用意したデータを返す

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

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

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

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

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

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

2011-05-18

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の詳しい説明は、開発者が公開している資料を参照ください。

環境

注意

今回はとりあえずブートさせることが目的なので、セキュリティに関しては何も考えていません。(qemurootで動かし、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配布のカーネルをそのまま使いたかったのでこうしました。

2009-03-26

Luvalley project

Luvalley project: running KVM without LinuxというSubjectのメールがKVM MLに流れてました。

どうもKVMのXenの間の子のようなアーキテクチャのVMMのようです。

追記:

アナウンスメールとREADMEから推察するに、Luvalleyとは以下のようなVMMみたいです。

  • コードベースはKVM
  • LuvalleyのコアはKVMドライバをベースに作ったsuper thin hypervisor
    • XenのようにOS(XenでいうところのDom0)の下で動く
    • CPU(vmxとか)とMMU周りの処理だけやる
  • ほとんどの処理はDom0 & QEMUが行なう
  • Dom0にLinuxとWindowsをサポートしている
    • QEMUさえ動けば良い他のOSでも良い(はず)
  • (明記されてなかったけどおそらく)完全仮想化
    • virtioを使ったパラもできそう

既知の制限とバグ

  • kvm-65ベースなのでそのkvmに残っていたバグがそのまま残っている ;)
  • PAEと64bitにはまだ対応していない
  • AMD-Vへの対応もまだ

疑問

  • Dom0って(vmxでいうところの)root op.とnon-root op.のどちらで動くのだろう? non-root op.だそうです。kazuさん情報ありがとうございます!
    • "CPU's virtualization mode"とあったけどこれはどちらを指しているのだろう?
  • LuvalleyとQEMUのやりとりはどうやって行なわれるのだろう?
  • Dom0カーネルとLuvalley間で明示的なやり取りは行なわれるのだろうか?
  • 性能はどのぐらいでるのだろう?
    • KVMより上か下か

2008-10-16

virtio balloon driver解説

最新のLinuxカーネルQEMU/KVMを使えば、特にトラブルもなくゲストメモリを動的に増減(memory ballooning)できるようになりました。

QEMU/KVMにおけるmemory ballooningのメカニズムをちょっとだけご紹介します。

概要

  • ゲストで使用されていないメモリを、ゲストカーネルのballoonドライバがホストへ返却,もしくはホストへ返却されたメモリをゲストへ返すことができる
  • ホスト主導で行なわれ,(いまのところ)ゲスト主導でメモリを返却することはできない
  • qemu-kvm引数で指定したメモリ(もしくはデフォルトのメモリ)量よりゲストメモリを大きくすることはできない
  • virtioで実装されている
  • ゲストは(いまのところ)Linuxのみで,ゲストカーネルにvirtio_balloon.koドライバがロードされていなければならない
  • KVMは必須ではない

virtio balloonドライバの使い方

  • linux-2.6.27 guest/host & kvm-77を用意する
  • qemuを起動しqemuコンソールでballoonコマンドを発行する
(qemu) info balloon
balloon: actual=384
(qemu) balloon 256
(qemu) info balloon
balloon: actual=256
(qemu) balloon 384
(qemu) info balloon
balloon: actual=384
(qemu)
[root@localhost ~]# free
            total       used       free     shared    buffers     cached
Mem:        384768      48796     335972          0      10684      25128
-/+ buffers/cache:      12984     371784
Swap:      1015800          0    1015800
[root@localhost ~]# free
            total       used       free     shared    buffers     cached
Mem:        253696      48548     205148          0      10684      25128
-/+ buffers/cache:      12736     240960
Swap:      1015800          0    1015800
[root@localhost ~]# free
            total       used       free     shared    buffers     cached
Mem:        384768      48768     336000          0      10684      25128
-/+ buffers/cache:      12956     371812
Swap:      1015800          0    1015800

balloonデバイス&ドライバ初期化

ballooning処理の流れ

  • [host] qemu commandでballoonサイズを指定する
    • 値の受け渡しはvirtio pci config (PCI config領域)で行なわれる
  • [guest] vballoonが起床する
    • wake_up()で起こされる
    • ドライバがその要求に応えballooningを実行する
    • その結果をvirtio pci configでhostに返す
    • vballoonはhostからのackを待つ(wait_for_completion())
  • [host] guestの結果をみてホストカーネルに反映させる
    • madvise(MADV_WILLNEED or MADV_DONTNEED)を使う
    • 完了をguestに知らせる
  • [guest] その通知を受け取ったらvballoonを起床させる(complete())
    • vballoonは再びvirtio pciのconfigの変化を待ち眠る(wait_event_interruptible())