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-server
  • uio_ivshmem.ko(ゲスト用)
  • テストプログラム

ちなみに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を作るところですが。