外部記憶装置

外付け記憶装置

Mewz on libkrun - その8 Mewz 追加実装(virtio-vsock)

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

目次

Virtio Socket (virtio-vsock) のドライバ実装

virtio-vsock 向けのドライバの実装はいたってシンプルである。 Virtqueue としては rx, tx, event の3つを実装すればよい。 rx, tx キューについては、それぞれ後述の vsocket の処理層へ受け渡す、受け取る機構を用意すればよい。 event キューについては、VMマイグレーションによりゲスト側の CID が変わった場合など、トランスポート層の処理自体がリセットされた場合に VIRTIO_VSOCK_EVENT_TRANSPORT_RESET を受け取ることになる。 今回は VMマイグレーションについては考慮していないため、event キューの処理は行わない方針とした。

https://docs.oasis-open.org/virtio/virtio/v1.3/csd01/virtio-v1.3-csd01.html#x1-4800006

実装としてはこれまでの virtio デバイスと変わらないため、詳細は省略する。

github.com

なお、ホスト側では受け口として /dev/vhost-vsock を用いている。 これを用いることで、カーネル側で virtio-vsock について接続処理等を行い、ユーザープロセスから AF_VSOCK を指定することでソケットAPI経由の操作を行うことができる。

ソケットの vsock への対応

vsock ソケットの実装

前回説明したとおり、virtio-vsock においてはドライバそのものよりその上に構築されるソケットの実装が重要となる。 Mewz への実装にあたり、すでに用意されているソケットの仕組みを利用しつつ下図のような vsock 向けのソケットを実装した。

実装については、前回まとめた接続処理に対応する動作を実装すればよい。 vsock の自体は以下のファイルにまとめられている。

github.com

構造としては、VsockMuxer が受け取ったパケットを対応する VsockSocket に対して受け渡し、各 vsock ソケットの状態を含めて管理する形となっている。 この時、どのポートに bind されているか、listen, 接続状態にあるかなどを管理し、意図しない挙動であれば VIRTIO_VSOCK_OP_RST を返すといった基本的なエラー処理も実装している。

詳細については process_rx 関数に記述されたハンドラを理解しやすい。

github.com

既存ソケット・WASI API との統合

既存ソケットへ統合は stream.zig でストリーム系のAPIをまとめて管理している部分に vsock も追加する形で行った。

https://github.com/naoki9911/mewz/blob/6d6b946b67d9fe19353b784535f1b4f5561ebe13/src/stream.zig#L89-L94

pub const Stream = union(enum) {
    uart: void,
    socket: Socket,
    vsock: VSocket,
    opened_file: OpenedFile,
    dir: Directory,

Stream union が内部的にソケット API に相当する関数群を提供しているため、それぞれ対応する vsock.zig の実装を呼び出す形で実装した。

WASI API との統合については、AddressFamily に追加で AF_VSOCK に相当する VSOCK を追加し、各種 API においても vsock 用の実装を追加した。 なお、AF_VSOCK の値については Linux の実装と合わせている。

https://github.com/naoki9911/mewz/blob/6d6b946b67d9fe19353b784535f1b4f5561ebe13/src/wasi/types.zig#L339-L344

pub const AddressFamily = enum(i32) {
    Unspec = 0,
    INET4 = 1,
    INET6 = 2,
    VSOCK = 40,
};

WASI API に対応する実装については現状すべては網羅できていない。 例えば、connect(2) に相当する WASI API である sock_open では、宛先の CID は "2" つまりホストに限定され、ポートのみ指定する形となっている。

https://github.com/naoki9911/mewz/blob/6d6b946b67d9fe19353b784535f1b4f5561ebe13/src/wasi.zig#L536-L555

pub export fn sock_connect(fd: i32, buf_ioved_addr: i32, port: i32) WasiError {
    log.debug.printf("WASI sock_connect: {d} {d} {d}\n", .{ fd, buf_ioved_addr, port });

    @setRuntimeSafety(false);

    const s = stream.fd_table.get(fd) orelse return WasiError.BADF;
    switch (s.*) {
        Stream.socket => |*socket| {
            const buf_iovec = @as(*IoVec, @ptrFromInt(@as(usize, @intCast(buf_ioved_addr)) + linear_memory_offset));
            const ip_addr_ptr = @as(*anyopaque, @ptrFromInt(@as(usize, @intCast(buf_iovec.buf)) + linear_memory_offset));
            socket.connect(ip_addr_ptr, port) catch return WasiError.INVAL;
        },
        Stream.vsock => |*vss| {
            vss.connect(2, @intCast(port)) catch return WasiError.INVAL;
        },
        else => return WasiError.BADF,
    }

    return WasiError.SUCCESS;
}

また、getlocaladdr といったアドレス情報を取得するための API についても未対応である。 これらについては、後述するユーザープログラム向けのライブラリも含めて今後対応する必要がある。

https://github.com/naoki9911/mewz/blob/6d6b946b67d9fe19353b784535f1b4f5561ebe13/src/wasi.zig#L646-L675

pub export fn sock_getlocaladdr(fd: i32, ip_iovec_addr: i32, type_addr: i32, port_addr: i32) WasiError {
    log.debug.printf("WASI sock_getlocaladdr: {d} {d} {d} {d}\n", .{ fd, ip_iovec_addr, type_addr, port_addr });

    @setRuntimeSafety(false);

    var s = stream.fd_table.get(fd) orelse return WasiError.BADF;
    var socket = switch (s.*) {
        Stream.socket => &s.socket,
        Stream.vsock => @panic("unimplemented! sock_getlocaladdr for vsock"),
        else => return WasiError.BADF,
    };

ユーザープログラム向けのライブラリ対応

AF_VSOCK について、WASI 向けの対応はまだ一般的ではないようである。 そのため、ユーザー向けに専用のライブラリを通して AF_VSOCK に対応する機能を提供する必要がある。

今回は wasmedge_wasi_socket をフォークして対応した。 対応個所としては AddressFamilyVsock を追加しただけである。 ユーザープログラムはソケット作成時に Vsock を指定すれば vsock なソケットを利用できる。 アドレスについては std::net::Ipv4Addr で指定し、前述したようにポート番号のみ利用する。

github.com

#[derive(Copy, Clone, Debug)]
#[repr(u8, align(1))]
pub enum AddressFamily {
    Unspec,
    Inet4,
    Inet6,
    Vsock = 40,
}

ここまでの動作確認

WebAssembly アプリケーションとして、vsock ソケットを利用した簡易的な Echo サーバーを実装した。

https://github.com/naoki9911/mewz/blob/6278db7ee37527416eeee5a060c35ca076198711/examples/echo_server_vsock/src/main.rs

use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use wasmedge_wasi_socket::socket::{Socket, SocketType, AddressFamily};

fn main() {
    let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1234);
    let sock = Socket::new(AddressFamily::Vsock, SocketType::Stream).unwrap();
    sock.bind(&addr).unwrap();
    sock.listen(10).unwrap();
    let mut buf: [u8; 100] = [0; 100];
    loop {
        let new_sock = sock.accept(false).unwrap();
        let recv_cnt = new_sock.recv(&mut buf).unwrap();
        let recv_utf8 = std::string::String::from_utf8((&buf[0..recv_cnt]).to_vec()).unwrap();
        println!("recv_cnt={} content={:?}", recv_cnt, recv_utf8);
        let sent_cnt = new_sock.send(&buf[0..recv_cnt]).unwrap();
        println!("sent_cnt={}", sent_cnt);
    }
}

ホスト側のクライアントについては Python で記述した。

https://github.com/naoki9911/mewz/blob/7d8a686e9737d088d34ae7ac9cb1200119e4a978/examples/echo_server_vsock/connect_vsock.py

import socket

sock = socket.socket(socket.AF_VSOCK, socket.SOCK_STREAM)
sock.connect((3, 1234))

msg = "Hello from host"
sock.send(msg.encode())
res = sock.recv(1024).decode()
print("res = {}".format(res))

sock.close()

前回の環境を引き続き利用し、動作確認を行った。なお、 GitHub - naoki9911/mewz-on-libkrun の submodule は更新済みであり、必要に応じて pull すること。

$ cd mewz/examples/echo_server_vsock/
$ ./build.sh
$ cd ../../
$ zig build -Dapp-obj=examples/echo_server_vsock/wasm.o run

別ターミナルでクライアントを実行するとエコーバックされていることが分かる。

$ python3 examples/echo_server_vsock/connect_vsock.py
res = Hello from host

このとき Mewz 側では

Booting from ROM..
UART: recv_cnt=15 content="Hello from host"
UART: sent_cnt=15

となり、クライアント側のメッセージを受信し正しく送信できていることが分かる。