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; 
       /* 省略 */
}