VM間共有メモリivshmemを試してみる
はじめに
qemu(-kvm)にはVM間共有メモリ(Inter-VM shared memory: ivshmem)という機能があります。名前のとおりVM間の共有メモリを実現する機能です。
今回はこの機能を体験することが目的です。内部動作解析などは行なっていません。
How does ivshmem work?
以下のドキュメントを参考にしてください。
- qemu-kvm付属のドキュメント
- パッチに書かれた説明
- ivshmemを使うために必要な追加ツール(便宜上ivshmemツールと呼びます)
ざっくり説明すると、ホストのshmem(shm)をPCIデバイスを介してゲストに見せる、ということになると思います*1。ゲストでUIOドライバを使うと、ユーザレベルのプログラムから共有メモリに(比較的)簡単にアクセスできるようになります。
環境
- Rootfs over Virtfsで作ったもの
- ivshmemツール(master/HEAD)
ゲスト用のファイルは以下のように用意されている前提で以降の説明をしていきます。
- ゲスト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は役に立つと思います。
内部解析はまた別の機会にやりたいと思います。