2011-05-29
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://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は役に立つと思います。
内部解析はまた別の機会にやりたいと思います。
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から情報を受け取っているのか、当該ソースコードを追ってみたいと思います。
概要
ポート番号、識別番号(とデータサイズ/形式)、動作手順がプロトコルになっている。
BIOS関係の基礎知識
- x86 BIOS Boot Loader
- Understanding the ACPI BIOS for an x86-based computer
- BIOS - Wikipedia, the free encyclopedia
- ACPI - OSDev Wiki
注意
実はこの辺りのことはあまり詳しくないので、間違っているかもしれません。ご注意ください。(ツッコミ歓迎です。)
調査対象
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。
#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 */
このポート番号は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も同じようなことをやっているのではないかと推測します(違ってたら教えてください)。
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の詳しい説明は、開発者が公開している資料を参照ください。
環境
- OS
- ゲスト、ホスト共にUbuntu 2011.04
- ゲストrootfsはdebootstrapを使って構築
- 試してないですがOpenVZ precreate rootfsでも良いはずです
- カーネルもUbuntu配布のもの(linux-image-2.6.38-8-generic)
- qemu-kvm
- Ubuntu配布のもの(0.14.0+noroms4)
- ただし再ビルドが必要(後述)
注意
今回はとりあえずブートさせることが目的なので、セキュリティに関しては何も考えていません。(qemuをrootで動かし、virtfsのsecurity_modelをnoneにしています。)
手順
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
やらなければならないことは、
- qemu引数に指定するmount_tagと9pファイルシステムのmount時に指定する識別名*2を一致させるため、カーネル引数経由で渡すmount_tagをinitrdのinitで解釈できるようにする
- 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のスレッドでも似たような話をしてますね。ここで紹介したやり方とは違うみたいですが。。。
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
- ほとんどの処理はDom0 & QEMUが行なう
- Dom0にLinuxとWindowsをサポートしている
- (明記されてなかったけどおそらく)完全仮想化
- virtioを使ったパラもできそう
既知の制限とバグ
疑問
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ドライバの使い方
(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
