virtioについて調査した事を以下に記述する。
virtioはLinux 2.6.24-rc1で導入された準仮想ドライバです。現在のところディスクおよびNICの入出力を処理できます。KVMlguestなどのVMMなどで利用されています。

virtioの利点

一般的に準仮想ドライバを実装するのはかなり難しく、Linuxカーネルについて詳しく知らなければなりません。virtioを利用することによって、Linuxの準仮想ドライバの実装コストが0になります。

virtioによる入出力処理の構造

virtioの入出力処理の構造は入出力要求側(ゲスト側)と入出力処理側(VMM側)に分かれています。Xenと同様にゲスト側のフロントエンドドライバとホスト側のバックエンドドライバと同じようになっています。

  • ゲスト側: Linuxカーネルをvirtioを有効にしてコンパイルすることにより、仮想的なvirtioデバイスが存在するカーネルになります。ゲスト側の入出力要求は実機のデバイスの様にvirtioデバイスに対して入出力の要求を行います(入出力命令を発行)。
  • VMM側: 入出力要求を受け取り、Type2VMMならばホストOSに対してread, writeなどのシステムコールを発行を行い入出力を処理します。

qemu + virtio

qemuとvirtioを組み合わせた実装 http://www.carfax.org.uk/docs/qemu-virtio 作者はvirtioを実装した人です。実装はわかりやすいので、処理の流れがすぐにわかると思います。

処理の流れ

ゲスト側

  • virtioデバイスに対して入出力命令を発効(入出力要求を発行)

qemu

  • 初期化時に入出力要求のキューを作成
/* qemu-0.9.1/hw/virtio-blk.c */
void *virtio_blk_init(PCIBus *bus, uint16_t vendor, uint16_t device,
                     BlockDriverState *bs)
{
    VirtIOBlock *s;

    s = (VirtIOBlock *)virtio_init_pci(bus, "virtio-blk", 6900, 0x1001,
                                      0, VIRTIO_ID_BLOCK,
                                      0x01, 0x80, 0x00,
                                      16, sizeof(VirtIOBlock));

    s->vdev.update_config = virtio_blk_update_config;
    s->vdev.get_features = virtio_blk_get_features;
    s->bs = bs;

    virtio_add_queue(&s->vdev, 128, virtio_blk_handle_output);

    return &s->vdev;
}
  • 入出力要求を受け取り、入出要求があるなら取り出しホストOSに対してシステムコールを発行し入出力を処理する。以下のコードは、実際のホストOSに対して入出力実行しているコードです。
/* qemu-0.9.1/hw/virtio-blk.c */
static void virtio_blk_handle_output(VirtIODevice *vdev, VirtQueue *vq)
{
    VirtIOBlock *s = to_virtio_blk(vdev);
    VirtQueueElement elem;
    unsigned int count;

    while ((count = virtqueue_pop(vq, &elem)) != 0) {
       struct virtio_blk_inhdr *in;
       struct virtio_blk_outhdr *out;
       unsigned int wlen;
       off_t off;
       int i;

       out = (void *)elem.out_sg[0].iov_base;
       in = (void *)elem.in_sg[elem.in_num - 1].iov_base;
       off = out->sector;

       if (out->type & VIRTIO_BLK_T_SCSI_CMD) {
           wlen = sizeof(*in);
           in->status = VIRTIO_BLK_S_UNSUPP;
       } else if (out->type & VIRTIO_BLK_T_OUT) {
           wlen = sizeof(*in);

           for (i = 1; i < elem.out_num; i++) { 
               /* システムコール pwriteと同様な処理 */
               bdrv_write(s->bs, off,
                          elem.out_sg[i].iov_base,
                          elem.out_sg[i].iov_len / 512);   
               off += elem.out_sg[i].iov_len / 512;
           }

           in->status = VIRTIO_BLK_S_OK;
       } else {
           wlen = sizeof(*in);

           for (i = 0; i < elem.in_num - 1; i++) {
               /* システムコール preadと同様な処理 */
               bdrv_read(s->bs, off,
                         elem.in_sg[i].iov_base,
                         elem.in_sg[i].iov_len / 512);
               off += elem.in_sg[i].iov_len / 512;
               wlen += elem.in_sg[i].iov_len;
           }

           in->status = VIRTIO_BLK_S_OK;
       }

       virtqueue_push(vq, &elem, wlen);
       virtio_notify(vdev, vq);
    }
}
  • 入出力要求は入出力を行うゲスト空間の先頭のアドレスとサイズのペアのベクトルで発行されているので、VMM側の処理でホスト側の入出力を行うアドレスとサイズのペアに変更しなければなりません。virtqueue_popの関数内でゲストからホストのアドレスに変換しているのが以下のコードです。
int virtqueue_pop(VirtQueue *vq, VirtQueueElement *elem)
{
       /* 省略 */
       sg->iov_len = vq->vring.desc[i].len;
       /* ここでホストのアドレス空間のアドレスに変換 */
       sg->iov_base = phys_ram_base + vq->vring.desc[i].addr; 
       /* 省略 */
}

VMIについて調査した事を以下に記述する。

VMIとは透過的な準仮想化インターフェイスと公式サイトで説明されています。http://www.vmware.com/jp/interfaces/paravirtualization.html VMIはparavirt-opsの一つの実装なので、VMIを説明する前にparavirt-opsについて説明します。

paravirt-ops

paravirt-opsLinux 2.6.20(x86)で導入された準仮想化を行うための関数の表です。この関数の表にはタイマ、割り込み、特権命令(センシティブな命令を含む)、MMU(ページテーブル)の設定、スピンロックなどが記述されいます。paravirt-opsは準仮想化を行う上でのスタブと考えてもいいかもしれません。このスタブの受け側はVMMで処理され現在のところXenおよびVMwareに実装されています。

具体的な関数の表は以下のようになっています。以下の例ではコントロールレジスタに読み書きする表が記述されています。

struct pv_mmu_ops pv_mmu_ops = {
	/* 省略 */
	.read_cr2 = native_read_cr2,
	.write_cr2 = native_write_cr2,
	.read_cr3 = native_read_cr3,
	.write_cr3 = native_write_cr3,
	/* 省略 */
}

paravirt-opsの欠点として入出力を行う関数の表が抜けています。

VMI(Virtual Machine Interface)

VMIはVMware用のparavirt-opsの一つ実装です。http://www.vmware.com/jp/interfaces/techpreview.html VMIの特徴してparavirt-opsに含まれていない入出力命令を行う関数の表が含まれています。
VMIのコードはLinuxカーネル(x86)にマージされているので、VMIを有効にしてコンパイルするだけで準仮想化を行う事ができます。

VMIのコードは現在のところ4つのファイルから構成されております。

  • linux/arch/x86/kernel/vmi_32.c
    • 割り込み、特権命令(センシティブな命令を含む)、MMU(ページテーブル)の設定のスタブ
  • linux/arch/x86/kernel/vmiclock_32.c
    • タイマのスタブ
  • linux/include/asm-x86/vmi.h
  • linux/include/asm-x86/vmi_time.h

タイマの準仮想ドライバが実装されているのはかなりすごいですね、またVMIの仕様書を見る限りロックの準仮想の実装はないようです。

具体的なコードはparavirt-opsの関数の表をVMware用に関数のポインタを上書きしています。para_fillはマクロであり第二引数のハイパーバイザコールを第一引数のparavirt-opsの関数の表のエントリに上書きしています。

/* linux-2.6.27-rc2/arch/x86/kernel/vmi_32.c */
static inline int __init activate_vmi(void)
{
	/* 省略 */
	para_fill(pv_cpu_ops.read_cr0, GetCR0);
	para_fill(pv_mmu_ops.read_cr2, GetCR2);
	para_fill(pv_mmu_ops.read_cr3, GetCR3);
	para_fill(pv_cpu_ops.read_cr4, GetCR4);
	para_fill(pv_cpu_ops.write_cr0, SetCR0);
	para_fill(pv_mmu_ops.write_cr2, SetCR2);
	para_fill(pv_mmu_ops.write_cr3, SetCR3);
	para_fill(pv_cpu_ops.write_cr4, SetCR4);
	/* 省略 */
}

VMIROM

VMIROMはハイパーバイザコールの呼び出しを行う上での処理および、VMIのAPIを提供します。VMware社からVMIROMをダウンロードして展開すると以下のようなディレクトリ構成になっています。

vmirom
|-- COPYING
|-- Makefile
|-- fixRom
|   |-- fixRom
|   `-- fixRom.c
|-- geninfo
|   |-- Subdir.mk
|   |-- genasminfo.pl
|   |-- vmiGenInfo.c
|   `-- vmiGenInfo.h
|-- make
|   `-- vmm-genrom-ld.pl
|-- rom
|   |-- Subdir.mk
|   |-- apic.c
|   |-- cpu.c
|   |-- cpuasm.S
|   |-- hypercallInterface.h
|   |-- hypervisorCalls.h
|   |-- mmu.c
|   |-- oprom_defs.h
|   |-- paravirtualInterface.h
|   |-- pci_defs.h
|   |-- relocated.c
|   |-- romHeader.S
|   |-- stubs.S
|   |-- support.c
|   |-- vm_basic_defs.h
|   |-- vm_basic_types.h
|   |-- vmi.h
|   |-- vmiCalls.h
|   |-- vmiatomic.h
|   |-- x86_defs.h
|   `-- x86types.h
`-- vmi_spec.txt

VMIROMをコンパイルすると最終的にはvmi.elfが生成されます。vmi.elfは静的リンクライブラリであり、ハイパーバイザコールなど以外の関数を呼べないように公開するシンボルを制限しています。

VMIROMは以下のようにビルドされます。

  1. gcc -fPICオプションで全てのvmirom/rom以下のCファイルをコンパイル
  2. gcc -o fixRom fixRom.c
  3. ar rcs vmirom/rom/*.o vmi.a
    • vmi.aの公開しているシンボルは615行 (nm vmi.a | wc)
  4. vmm-genrom-ld.pl でvmi.ldのリンカスクリプトを作成
  5. ld vmi.a vmi.elf -T vmi.ld
    • vmi.aの公開しているシンボルは220行 (nm vmi.elf | wc)
  6. fixRom -o vmi.elf

このようにしてVMIROMはゲストカーネルの関数名が重ならないよう公開するシンボルを制限して実装されています。

VMIROMの初期化およびVMIとVMIROMの関係

ゲストカーネルのVMIROMのロードは以下のコードのようにして行います。まず、VMMがゲストのメモリ空間の0xc0000から0xe0000の間(128KB)にVMIROMを保存します。ゲストカーネルが起動時にVMIROMを見つけます。

/* linux-2.6.27-rc2/arch/x86/kernel/vmi_32.c */
static inline int __init probe_vmi_rom(void)
{
        unsigned long base;

        /* VMI ROM is in option ROM area, check signature */
        for (base = 0xC0000; base < 0xE0000; base += 2048) {
                struct vrom_header *romstart;
                romstart = (struct vrom_header *)isa_bus_to_virt(base);
                if (check_vmi_rom(romstart)) {
                        vmi_rom = romstart; /* オフセットの先頭 */
                        return 1;
                }
        }
        return 0;
}

VMIの関数の呼び出しは、VMIROMのオフセットから関数呼び出しをしています。

/* linux-2.6.27-rc2/arch/x86/kernel/vmi_32.c */
static void *vmi_get_function(int vmicall)
{
        u64 reloc;
        const struct vmi_relocation_info *rel = (struct vmi_relocation_info *)&reloc;
        reloc = call_vrom_long_func(vmi_rom, get_reloc, vmicall); /* オフセットを元に関数呼び出し */
        BUG_ON(rel->type == VMI_RELOCATION_JUMP_REL);
        if (rel->type == VMI_RELOCATION_CALL_REL)
                return (void *)rel->eip;
        else
                return NULL;
}

VMIROMとゲストカーネルを静的リンクするのではなく、起動時にリンクする考えはすばらしい考えだと思います。
自分の研究で使用している仮想計算機でも同様な技術があるのですが静的リンクで行っているので見習いたいと思います。

gccのビルトイン関数には関数のリターンアドレスやフレームポインタなどを取得することができる関数がある。この関数を使用しsetjmp/longjmpの実装は以下の様になる。たぶんバグはないはず?

jmp.c

#include <stdio.h>
#include <stdlib.h>

#define JMP_BUFFSIZE 6

typedef struct {
	unsigned long __jmp_buf[JMP_BUFFSIZE];
} jmp_buf[1];

jmp_buf jmp_buffer;

int setjmp(jmp_buf env)
{
	void *return_addr = __builtin_return_address(0);

	asm volatile("movl %%edx, 0(%%ecx)\n\t"	/* return address */
		     "movl %%ebx, 4(%%ecx)\n\t"
		     "movl %%esp, 8(%%ecx)\n\t"
		     "movl %%ebp, 12(%%ecx)\n\t"
		     "movl %%esi, 16(%%ecx)\n\t"
 		     "movl %%edi, 20(%%ecx)"
		     :: "d" (return_addr), "c" (env));
	return 0;
}

int longjmp(jmp_buf env, int val)
{
	asm volatile("movl 0(%%edx), %%ecx\n\t"	/* return address */
		     "movl 4(%%edx), %%ebx\n\t"
		     "movl 8(%%edx), %%esp\n\t"
		     "movl 12(%%edx), %%ebp\n\t"
		     "movl 16(%%edx), %%esi\n\t"
		     "movl 20(%%edx), %%edi\n\t"
		     "cmpl $0, %%eax\n\t" /* if val = 0 */
		     "jne  1f\n\t"
		     "movl $1, %%eax\n\t" /* then val = 1 */
		     "1: jmp *%%ecx"
		     :: "a" (val), "d" (env));
	/* NOTREACHED */

        return 0;
}

void func2(void)
{
	longjmp(jmp_buffer, 100);
}

void func1(void)
{
	func2();
}

int main(void)
{
	int ret = 0;

	ret = setjmp(jmp_buffer);
	if (ret != 0) {
		printf("longjmp: %d\n", ret);
		exit(0);
	}

	func1();

	printf("Error: cannot reach this code\n");
	
	return ret;
}

コンパイルと実行

$ gcc jmp.c
$ ./a.out
longjmp: 100

カーネルやVMMを実装するときは標準ライブラリを使用せずにコンパイルを行う。32ビットカーネルやVMMを実装中に、long longなどの変数の除算やかけ算を行うとコンパイルが通らない。それはlong longなどの64ビット変数の乗算や除算などの命令を実行しているからである。32ビットカーネルでは64ビットレジスタの除算や演算が存在しないので、ライブラリが64ビット変数の演算のエミュレーションを行っている。

エミュレーションを行っているライブラリはlibgcc.aであり、プログラムにこのライブラリをリンクすればこの問題を解決できる。

例えば、32ビットカーネルで以下のようなコードの場合

/* test.c */
int main(void)
{
	long long x = 100;
	long long y = 10;
	long long z = x / y;

	while (1);

	return 0;
}

うまくいかない場合

$ gcc -nostdinc -e main test.c
/tmp/cc6vAL0z.o: In function `main':
div.c:(.text+0x49): undefined reference to `__divdi3'
collect2: ld はステータス 1 で終了しました

うまくいく場合

libgcc.aは使用しているgccディレクトリを合わせる必要がある。

$ gcc -nostdlib -e main test.c -L/usr/lib/gcc/i486-linux-gnu/4.1.2/ -lgcc
$ ./a.out

エミュレーションのコード

gcc4.2で、libgcc.Sは以下のディレクトリにある。

$ cd gcc4.2
$ less ./gcc/config/libgcc2.c

FreeBSDソースコードのダウンロードの仕方は2種類ある。

FTPFreeBSDのダウンロードの仕方

i386 ftp://ftp.freebsd.org/pub/FreeBSD/releases/i386/
amd64 ftp://ftp.freebsd.org/pub/FreeBSD/releases/amd64/

上のftpサイトから自分の欲しいバージョンを指定する。かなり古いバージョンから7.0-BETA1.5まである。例えば6.2の場合では

$ wget -r ftp://ftp.freebsd.org/pub/FreeBSD/releases/i386/6.2-RELEASE/src

カーネルソースコードが欲しい場合はssys.a[a-p]をcatで連結し、gunzipで展開するとコードが取得できる。カーネル以外も同様にできる。

$ cat ssys.a* > ssys.gz
$ gunzip ssys.gz

CVSソースコードのダウンロードの仕方

$ export CVSROOT=:pserver:anoncvs@anoncvs.tw.FreeBSD.org:/home/ncvs
$ cvs login
パスワードが聞かれるので"password"と入力する。
$ cvs co src/sys # カーネルのコードが欲しい場合

詳しくは以下のサイトに書いてある。
http://www.jp.freebsd.org/www.FreeBSD.org/doc/en_US.ISO8859-1/books/handbook/anoncvs.html

最近、ディスクがクラッシュしたのでpxegrubの設定をやり直した。手順は以下のようにやる。

pxegrubはgrubを使用しネットワーク越しにカーネルを立ち上げる事ができる。カーネル設計者にとってメリットはGRUBのマルチブートを利用できる事や、カーネルの再起動がすぐにできることである。pxegrubを利用するときネットワークカードの制限があるのでGRUBに対応しているネットワークカードを買うか、仮想マシンを利用するかどちらかにしなければならない。

pxegrub概要

pxegrubを使用しカーネルを立ち上げるにはいくつかの手順が必要になる。

  1. tftpserverを立ち上げる(開発マシンで立ち上げるとよい)。
  2. tftpserverにカーネルをアップロード
  3. 実機または仮想マシンPXE用にコンパイルしたGRUBをインストール
  4. 実機または仮想マシンの/boot/grub/menu.lstを編集
具体的には次のように行う。

tftpserverをインストール

$ sudo aptitude install tftpd
$ sudo mkdir /tftpboot # ここにカーネルをアップロードする。
$ vi /etc/xinetd.d/tftp
$ sudo /etc/init.d/xinetd restart

tftpserverの設定ファイルのサンプルは以下の用になる。

service tftp
{
        socket_type     = dgram
        protocol        = udp
        wait            = yes
        user            = your_user_name
        server          = /usr/sbin/in.tftpd
        server_args     = -s /tftpboot
        disable         = no
        per_source      = 11
        cps             = 100 2
}

実機または仮想マシンで以下のように行う。以下はVMwareで行ったので./configureのオプションでlanceのネットワークカードを有効にしている。

$ cvs -z3 -d:pserver:anonymous@cvs.savannah.gnu.org:/sources/grub co grub
$ sudo aptitude install cvs gcc autconf automake
$ cd grub
$ ./configure --prefix=/usr/local/ --enable-diskless --enable-lance
$ make
$ make check
$ make install
$ sudo grub-install /dev/hda
$ sudo update-grub
$ sudo vi /boot/grub/menu.lst

/boot/grub/menu.lstのサンプルは以下のようなコードを追加すればよい。

title pxegrub + kernel
dhcp
tfptserver	server_address
root		(nd)
kernel		/kernel
#module		/init
#module		/fs
#module		/pm

実機または仮想マシンを立ち上げ"pxegrub + kernel"を選べばカーネルを立ち上げる事ができる。