外部記憶装置

外付け記憶装置

Mewz on libkrun - その4 Mewz 追加実装(Virtio MMIO)

Mewz (WebAssembly x Unikernel) を libkrun で動かしてみた - 外部記憶装置 の詳細を記すシリーズ

目次

Virtio MMIO について

正しくは Virtio Over MMIO である。 PCI 経由で Virtio デバイスを提供するのではなく、Memory Mapped IO として提供する。 PCIバイスのスキャン等の必要がなく、デバイスレジスタが存在するアドレスを指定すればよいため Virtio Over PCI よりもシンプルである。

docs.oasis-open.org

Mewz への追加実装

Mewz は既に Virtio Over PCI な virtio-net ドライバを持つ。 そのため、Virtqueue などのプリミティブなバッファ系は既に実装されているため、それらを流用し Virtio MMIO な virtio-net デバイスへ対応する。

MMIO Device Register Layout

Virtio MMIO はデバイスごとに指定されたデバイスについて Device Register を持つ。この Device Register から DeviceID 等を読み取り、必要に応じてデバイスを初期化する。

Virtual I/O Device (VIRTIO) Version 1.3

Zig においては packed struct を用いることでメモリマップドなレジスタ群についても容易に扱うことができる。実際には以下のような実装となっている。

https://github.com/naoki9911/mewz/blob/12b61046f478f174de5af634aa67eb0f13839e23/src/drivers/virtio/mmio.zig#L148-L190

pub const DeviceRegister = packed struct {
    magic: u32, // 0x000
    version: u32, // 0x004
    device_id: u32, // 0x008
    vendor_id: u32, // 0x00C
    device_features: u32, // 0x010
    device_features_sel: u32, // 0x014
    _pad1: u64, // 0x018
    driver_features: u32, // 0x020
    driver_features_sel: u32, // 0x024
    _pad2: u64, // 0x28
    queue_sel: u32, // 0x30
    queue_size_max: u32, // 0x34
    queue_size: u32, // 0x38
    _pad3: u64, // 0x3C
...

Vritio MMIOバイスの初期化

バイスの初期化についても、Virtio PCI なデバイスとは異なる手順となっている。Virtqueue の用意を行う部分が異なり、残りの部分については "Further initialization MUST follow the procedure described in 3.1 Device Initialization." とあるように Virtio PCI と変わらないため、これについても既存のドライバを流用しつつ実装した。

Virtual I/O Device (VIRTIO) Version 1.3

なお、そのまま実装すると正しく動作しなかったため、mfence 命令をはさみ記述どおりの順番でメモリへの読み書きが行われるようにした。

mewz/src/drivers/virtio/mmio.zig at 12b61046f478f174de5af634aa67eb0f13839e23 · naoki9911/mewz · GitHub

            const virtqueues = try allocator.alloc(common.Virtqueue, queue_num);
            for (0..queue_num) |i| {
                const queue_index = @as(u16, @intCast(i));

                transport.common_config.queue_sel = queue_index;
                x64.mfence();

                if (transport.common_config.queue_ready != 0) {
                    log.fatal.printf("virtio.mmio: virtqueue[{}] is not ready", .{i});
                    @panic("virio.mmio is not available");
                }

                const queue_size: u16 = @intCast(transport.common_config.queue_size_max & 0xFFFF);
                log.debug.printf("virtio.mmio: virtqueue[{}] queue size is {}\n", .{ i, queue_size });
                if (queue_size == 0) {
                    log.fatal.printf("virtio.mmio: virtqueue[{}] is not avaiable", .{i});
                    @panic("virio.mmio is not available");
                }

                const virtqueue = try common.Virtqueue.new(queue_index, queue_size, allocator);
                transport.common_config.queue_size = queue_size;
                const desc = @as(u64, @intFromPtr(virtqueue.desc));
                const driver = @as(u64, @intCast(virtqueue.avail.addr()));
                const device = @as(u64, @intCast(virtqueue.used.addr()));
                transport.common_config.queue_desc_low = @intCast(desc & 0xFFFFFFFF);
                transport.common_config.queue_desc_high = @intCast(desc >> 32);
                transport.common_config.queue_driver_low = @intCast(driver & 0xFFFFFFFF);
                transport.common_config.queue_driver_high = @intCast(driver >> 32);
                transport.common_config.queue_device_low = @intCast(device & 0xFFFFFFFF);
                transport.common_config.queue_device_high = @intCast(device >> 32);
                x64.mfence();
                transport.common_config.queue_ready = 1;
                virtqueues[i] = virtqueue;
                x64.mfence();
            }

割り込み周りの修正

Virtio MMIO では、割り込み終了後明示的に Device Register の InterruptACK にフラグを立てる必要がある。 Virtio MMIO の場合についてのみ該当処理を行う形で対応した。

mewz/src/drivers/virtio/net.zig at 12b61046f478f174de5af634aa67eb0f13839e23 · naoki9911/mewz · GitHub

fn handleIrq(frame: *interrupt.InterruptFrame) void {
    _ = frame;
    log.debug.print("interrupt\n");
    if (virtio_net) |vn| {
        vn.receive();
        // acknowledge irq
        switch (vn.virtio) {
            .mmio => |*m| m.transport.common_config.interuupt_ack = 1,
            else => {},
        }

Virtio PCI との共存

Mewz は QEMU、つまり Virtio PCI を利用する環境がデフォルトとして想定されている。 QEMU は Virtio MMIO についてもサポートしているため、Virtio PCI 自体をサポートしないという選択肢も考えられるが、今回は Virtio MMIO と Virtio PCI の両者に対してドライバを対応させた。

Virtio PCI と Virtio MMIO の違いとしてはそれぞれのデバイス初期化等の設定周りがほとんどであり、比較的容易に共存させることができる。 Zig では struct の union が提供されているため、今回はそれを利用した。

mewz/src/drivers/virtio/common.zig at 12b61046f478f174de5af634aa67eb0f13839e23 · naoki9911/mewz · GitHub

pub fn Virtio(comptime DeviceConfigType: type) type {
    return union(enum) {
        pci: VirtioPCI(DeviceConfigType),
        mmio: VirtioMMIO(DeviceConfigType),
    };
}

呼び出し時は関数名が同じならば Zig 0.10.0 からサポートされた inline else を使うことで以下のように簡潔に記述できる。

mewz/src/drivers/virtio/net.zig at 12b61046f478f174de5af634aa67eb0f13839e23 · naoki9911/mewz · GitHub

    fn receiveq(self: *Self) *common.Virtqueue {
        switch (self.virtio) {
            inline else => |v| return &v.virtqueues[0],
        }
    }

その他の修正

Mewz では、IP アドレスとデフォルトゲートウェイ(DGW)のアドレスは kernel cmd params 経由で受け取る。 libkrun では環境変数を自由に設定することができるため、環境変数としてアドレス類を受け渡すように対応した。

github.com

libkrun chroot_vm.c の修正

これまで libkrun の実行に利用している chroot_vm.c では、標準では virtio-net ではなく TSI と呼ばれる virtio-vsock 経由の通信機能を提供する。 virtio-net を利用する場合は --net=passt を指定することで virtio-net を利用できる。 --net=passtQEMU における -netdev user と類似した機能であり、ゲスト側は指定された IP アドレスと DGW を使う必要がある。 本来は DHCP Client を実装すればよいが、面倒なためとりあえず環境変数経由で指定することにした。 chroot_vm.c 側においても明示的に利用するアドレス類を指定するようにした。

また、ポートを公開する設定として、-t 1234 を指定している。 これは QEMUhostfwd に相当するオプションであり、ホスト側の1234/tcpポートに対する接続をゲスト側の1234/tcpへと中継する設定である。

github.com

ここまでの動作確認

通信機能を持つアプリケーションとして、Mewz が example として提供している hello_server を利用した。 環境は前回の動作確認した時の環境を使いまわしている。

$ sudo apt install passt
$ cd mewz/examples/hello_server
$ cargo build --target wasm32-wasi
$ wasker target/wasm32-wasi/debug/hello_server.wasm
$ cd ../../
$ zig build -Dapp-obj=examples/hello_server/wasm.o -Dlog-level=info -Denable-pci=false libkrunfw
$ cd ../libkrun/examples
$ make
$ sudo LD_LIBRARY_PATH=../lib ./chroot_vm --net=passt dummy dummy
Don't run as root. Changing to nobody...
No routable interface for IPv6: IPv6 is disabled
Template interface: eth0 (IPv4)
MAC:
    host: 00:15:5d:f5:94:7a
DHCP:
    assign: 192.168.10.2
    mask: 255.255.255.0
    router: 192.168.10.1
DNS:
    10.255.255.254
DNS search list:
    tailbffcc.ts.net
    flets-west.jp
    iptvf.jp
[LOG INFO]: booted with linux zero page
[LOG INFO]: zeropage: e820_entries=2
[LOG INFO]: E820 Entry [1] addr=0x0 size=0x9fc00 type=1
[LOG INFO]: E820 Entry [2] addr=0x100000 size=0x20310001 type=1
[LOG INFO]: available memory: 14747000 - 20410001
[LOG INFO]: virtio_mmio device detected: addr=0xd0000000 size=0x1000 IRQ=5
[LOG INFO]: virtio_mmio device detected: addr=0xd0001000 size=0x1000 IRQ=6
[LOG INFO]: virtio_mmio device detected: addr=0xd0002000 size=0x1000 IRQ=7
[LOG INFO]: virtio_mmio device detected: addr=0xd0003000 size=0x1000 IRQ=8
[LOG INFO]: virtio_mmio device detected: addr=0xd0004000 size=0x1000 IRQ=9
[LOG INFO]: virtio.net: found mmio device
[LOG INFO]: mac: 5a:94:ef:e4:c:ee
[LOG INFO]: virtio.net: initialized mmio device

Listening on http://0.0.0.0:1234

別ターミナルで curl を実行する。

$ curl localhost:1234
Hello World!

となり、無事 Mewz で動作しているサーバーからレスポンスが得られた。