外部記憶装置

外付け記憶装置

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

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

Mewz on libkrun - その7 virtio-vsock について

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

目次

Virtio Socket (virtio-vsock) について

docs.oasis-open.org

Virtio Socket (virtio-vsock) はゲストとホスト間をいわゆるソケットを用いて通信を行うためのデバイスである。 virtio-vsock のデバイス自体は Virtuqueue を3つ(rx, tx, event) 持つだけのシンプルな構造である。 ただし、ソケット相当の機能を提供するためにそれらの Virtqueue 上でソケットの multiplexing やフロー制御を行う必要がある。

virtio-vsock の概要

virtio-vsock では、ホスト・ゲストの識別子として 64bit 長の Context ID (CID) を持つ。また、ポートの概念も持ち、32bit 長の識別子で扱う。

これらの識別子については TCP, UDP のようにヘッダに格納されペイロードと合わせてパケットとして Virtqueue を流れる。

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

virtio-vsock ではソケットの管理やAPIについては言及されていないため、ゲスト側でソケットと送受信するパケットを適切に振り分ける仕組みが別途必要となる。 そのため、これまでの virtio-net や virtio-console と異なり、ドライバ単体だけでなく OS 側のソケットのサブシステム自体にも手をいれることになる。

通信の種類

virtio-vsock では、VIRTIO_VSOCK_TYPE_STREAMVIRTIO_VSOCK_TYPE_SEQPACKET の2種類の通信方式が定義されている。

VIRTIO_VSOCK_TYPE_STREAM はいわゆる SOCK_STREAM に相当するものであり、順序保証付きコネクション指向型のメッセージ境界がない(=ストリーム形式) 通信を提供する。プロトコルとしては TCP 相当の機能を提供する。

VIRTIO_VSOCK_TYPE_SEQPACKET はいわゆる SOCK_SEQPACKET に相当するものであり、順序保証付きコネクション指向型でメッセージ境界が存在する通信を提供する。IP で利用されるプロトコルとしては SCTP が近い。

virtio-vsock ではこれらの通信を用途に応じて使い分けることができる。

プロトコル

virtio-vsock はコネクション指向型の通信を提供するため、セッションの確立および切断について上図に示すプロトコルが存在する。

接続

接続の起点は VIRTIO_VSOCK_OP_REQUESTを送信するところからである。 dst_cid,dst_portに対応するソケットが存在し接続を受け入れる場合は VIRTIO_VSOCK_OP_RESPONSE を返す。接続を拒否する場合は VIRTIO_VSOCK_OP_RST を返す。接続については以上で完了する。

データの送受信

その後、VIRTIO_VSOCK_OP_RW でデータの送受信を行う。virtio-vsock はロスレスな完全性が保証された通信路を前提とするため、Ack を行う必要はない。 データの送受信中、ピアのソケットバッファの状況を把握するためのリクエストとして VIRTIO_VSOCK_OP_CREDIT_REQUEST を送信すると、ピアは VIRTIO_VSOCK_OP_CREDIT_UPDATE としてバッファの状況をヘッダに記して返信する。VIRTIO_VSOCK_OP_CREDIT_UPDATE についてはリクエストがない場合一方的に送っても構わない。

切断

切断については VIRTIO_VSOCK_OP_SHUTDOWN により行う。フラグとして、ピアは以降いかなるデータも受信しないことを示す VIRTIO_VSOCK_SHUTDOWN_F_RECEIVE と、いかなるデータも送信しないことを示す VIRTIO_VSOCK_SHUTDOWN_F_SEND がある。これらのフラグを用いて、ピアに対してデータのこぼれが生じないように終了処理を行う。 いかなるデータの送受信が行われることが無いことが確認された時点で、切断処理は完了する。つまり、必要に応じて複数回のVIRTIO_VSOCK_OP_SHUTDOWN がやり取りされる場合がある。例えば、VSockA→VSockBでVIRTIO_VSOCK_SHTDOWN_F_RECEIVEとし、VSockB→VSockA で VIRTIO_VSOCK_SHTUDOWN_F_RECEIVEとした場合、両者ともにデータの受信を行わないことを通知しているため切断とみなすことができ、VSockA→VSockB でVIRTIO_VSOCK_OP_RSTを送信して切断処理を完了する。なお、一定時間VIRIO_VSOCK_OP_RSTが届かなかった場合、VIRTIO_VSOCK_OP_RSTを送信して終了とする。

SEQPACKET について

VIRTIO_VSOCK_TYPE_SEQPACKET では、2種類のメッセージ境界(VIRTIO_VSOCK_SEQ_EOM, VIRTIO_VSOCK_SEQ_EOR)を扱うことができる。最小単位は "Message" であり、メッセージを複数の RW パケットに分割後、最後のパケットに VIRTIO_VSOCK_SEQ_EOM フラグを立てることで境界となる。

もう一つのメッセージ境界としては、複数のメッセージをまとめたものである "Record" がある。Record については 一連の連続した Message をまとめ、Record 間の区切りは VIRTIO_VSOCK_SEQ_EOR フラグを RW パケットに立てることで行う。

フロー制御

virtio-vsock はパケットロスが一切許容されないロスレスな通信路を前提としているため再送機構を持たない。 そのため、相手のバッファ溢れによるパケットロスが生じないようにフロー制御を行っている。

フロー制御としてはシンプルなものであり、ピアから通知されるバッファサイズや転送カウントを元に、バッファの空き状況に応じてデータの送信可否を決定する。

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

実際の計算では、(ピアのバッファサイズ) - (送信済みバイト数 - ピアが処理したバイト数) となる。 つまり、RW パケット送信時点で相手が少なくとも持ちうる空き容量を計算している。 ピアのバッファサイズや処理したバイト数については、ピアが送信する RW パケットや VIRTIO_VSOCK_OP_CREDIT_UPDATE パケットヘッダから得ることができる。

まとめ

virtio-vsock のデバイス自体はシンプルであるが、その上にソケットの仕組みを実装することになる。 そのため、カーネルが持つソケット相当のサブシステムに対して上手く組み込む必要がある。 次回は実際に Mewz に対してソケットとの統合も含めて virtio-vsock の実装を行う。

Mewz on libkrun - その6 Mewz 追加実装(virtio-console)

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

目次

Virtio Console (virtio-console) について

docs.oasis-open.org

virtio-console はコンソール入出力向けの virtio デバイスである。 一般的に、シリアル経由で行われるコンソール入出力を置き換えるために用いられる。

libkrun においても、コンソールデバイスとして標準で用いられる。 シリアルの場合 x86_64 や aarch64 でそれぞれ対応するデバイスが異なり、ドライバを複数用意する必要がある。 virtio-console を用いることで、そういったデバイスドライバを用意する必要が無くなる。

Mewz においては今のところ x86_64 を対象としているため、IO Port 経由でシリアルデバイスを操作することでコンソールの入出力を行っている。 libkrun は virtio-console を用いることが想定されているため、これについても追加実装を行い対応する。

基本機能の実装

virtio-console は Virtqueue を送受信用で計2つ用意し、他の virtio デバイスと同じ手順で初期化すれば基本的な機能は利用できる。 コンソール出力については transmitq(port0)、入力については receiveq(port0) で扱い、Mewz 側でこれに対応するだけで良い。

QEMU においては、-device virtio-serial -device virtconsole,chardev=my_console -chardev socket,id=my_console,host=127.0.0.1,port=3334,server,nowait をオプションとして与え、別ターミナルで telnet localhost 3334 を実行すれば telnet 経由で virtio-console の入出力を扱うことができる。

VIRTIO_CONSOLE_F_MULTIPORT

libkrun では、VIRTIO_CONSOLE_F_MULTIPORT と呼ばれる拡張機能を前提としているため、上述の単純な virtio-console ドライバでは動作しない。 この拡張はその名の通り、複数 virtio-console ポートを扱うためのものであり、Virtqueue 初期化後に各ポートの初期化処理を行う必要がある。 これらの制御は専用の Virtqueue である control receiveqcontrol transmitqを介して行われる。

VIRTIO_CONSOLE_F_MULTIPORT feature を利用する場合、control キューでは以下のフォーマットに従い制御通信を行う。

struct virtio_console_control { 
        le32 id;    /* Port number */ 
        le16 event; /* The kind of control event */ 
        le16 value; /* Extra information for the event */ 
};

id はポート番号、event は制御用コード、valueステータスコード等を含める。 ポートの初期化は以下の流れに従う。

Virtqueue の初期化後、VIRTIO_CONSOLE_DEVICE_READY を libkrun(Device) に対して通知することでデバイスの初期化が完了する。

その後、Device からポートの追加要求(VIRTIO_CONSOLE_DEVICE_ADD) を受け取る。id で指定されたポートの準備が完了した後、成功を示す value=1 を持つVIRTIO_CONSOLE_PORT_READYメッセージで返答する。

ポートをコンソールの入出力として利用する場合、Device は VIRTIO_CONSOLE_CONSOLE_PORT により Driver に通知を行う。Driver はこれを受けて VIRTIO_CONSOLE_PORT_OPENvalue=1 を入れて返答する。※なお、QEMU においては VIRTIO_CONSOLE_PORT_OPEN を返答しなくとも動くようではあるが、Virtio の仕様上は "MUST" となっているため必ず返答する必要がある。

その後、Device 側から VIRTIO_CONSOLE_PORT_OPEN を受け取ると、該当ポートは既に利用可能な状態であるため、Driver はコンソールの入出力を対応する Virtqueue に流せば良い。

Mewz への実装

drivers/virtio/console.zig に Multiport 対応のドライバを実装した。

github.com

libkrun については標準で port0 だけでコンソールの入出力を扱えるため、port0 のみ受け付け、それ以外のポートは拒否するように実装した。

    fn handleCtrl(self: *Self, ctrl: *VirtioConsoleControl) void {
        switch (ctrl.event) {
            .VIRTIO_CONSOLE_DEVICE_ADD => {
                // only accept port 0
                if (ctrl.id == 0) {
                    log.info.printf("virtio.console: ctrl port added (port={})\n", .{ctrl.id});
                    self.port0_added = true;
                    self.ctrlTransmit(ctrl.id, .VIRTIO_CONSOLE_PORT_READY, 1);
                } else {
                    log.warn.printf("virtio.console: ctrl port is not ready (port={})\n", .{ctrl.id});
                    self.ctrlTransmit(ctrl.id, .VIRTIO_CONSOLE_PORT_READY, 0);
                }
            },
            .VIRTIO_CONSOLE_CONSOLE_PORT => {
                if (ctrl.id != 0 or !self.port0_added) {
                    log.warn.printf("virtio.console: cannot use port{} as a console: not added\n", .{ctrl.id});
                } else {
                    self.port0_console = true;
                    log.info.print("virtio.console: port0 is specified as a console\n");
                    self.ctrlTransmit(0, .VIRTIO_CONSOLE_PORT_OPEN, 1);
                }
            },
            .VIRTIO_CONSOLE_PORT_OPEN => {
                if (ctrl.id != 0 or !self.port0_added) {
                    log.warn.printf("virtio.console: cannot open port{}: not added\n", .{ctrl.id});
                } else {
                    if (ctrl.value == 1) {
                        self.port0_opened = true;
                        log.info.print("virtio.console: port0 is opened\n");
                    } else if (ctrl.value == 0) {
                        self.port0_opened = false;
                        log.info.print("virtio.console: port0 is closed\n");
                    } else {
                        log.warn.printf("virtio.console: invalid value: {}\n", .{ctrl});
                    }
                }
            },
            .VIRTIO_CONSOLE_RESIZE => {
                log.warn.print("virtio.console: VIRTIO_CONSOLE_RESIZE is ignored\n");
            },

UART(シリアル)との共存については、virtio-console の port0 がコンソールとして利用かつ、既に OPEN な状態にある場合は port0 を利用し、それ以外はシリアルにコンソール出力を流すようにした。

github.com

他にも、virtio-console の PCI BAR が IO space mapped な場合への対応等を行い、QEMU と libkrun のどちらでも動作するようにした。

pci: Handle IO space mapped BAR · naoki9911/mewz@8495787 · GitHub

ここまでの動作確認

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

$ cd mewz
$ zig build -Dapp-obj=wasm.o -Dlog-level=info -Denable-pci=false libkrunfw
$ cd ../libkrun/examples
$ sudo LD_LIBRARY_PATH=../lib ./chroot_vm --net=passt dummy dummy
[sudo] password for naoki:
UART: [LOG INFO]: booted with linux zero page
UART: [LOG INFO]: zeropage: e820_entries=2
UART: [LOG INFO]: E820 Entry [1] addr=0x0 size=0x9fc00 type=1
UART: [LOG INFO]: E820 Entry [2] addr=0x100000 size=0x200b0001 type=1
UART: [LOG INFO]: available memory: 144eb000 - 201b0001
UART: [LOG INFO]: virtio_mmio device detected: addr=0xd0000000 size=0x1000 IRQ=5
UART: [LOG INFO]: virtio_mmio device detected: addr=0xd0001000 size=0x1000 IRQ=6
UART: [LOG INFO]: virtio_mmio device detected: addr=0xd0002000 size=0x1000 IRQ=7
UART: [LOG INFO]: virtio_mmio device detected: addr=0xd0003000 size=0x1000 IRQ=8
UART: [LOG INFO]: virtio_mmio device detected: addr=0xd0004000 size=0x1000 IRQ=9
UART: [LOG INFO]: virtio.console: found mmio device
UART: [LOG INFO]: virtio.console: initialized mmio device
UART: [LOG INFO]: virtio.net: found mmio device
UART: [LOG INFO]: mac: 5a:94:ef:e4:c:ee
UART: [LOG INFO]: virtio.net: initialized mmio device

UART: [LOG INFO]: virtio.console: ctrl port added (port=0)
UART: [LOG INFO]: virtio.console: port0 is specified as a console
VC: [LOG INFO]: virtio.console: port0 is opened
VC: [LOG WARN]: virtio.console: VIRTIO_CONSOLE_RESIZE is ignored
VC: Hello, wasker

UART 経由の出力については UART:、virtio-console 経由の出力については VC:を先頭に付与する実装としている。 virtio-console ドライバで port0 が open となった時点で出力が virtio-console となっており、意図した通り動作していることがわかる。

2024年振り返り

時間がないため簡単に 2024 年を振り返る。

主な開発したもの

public にできるもの、かつ、わりと時間をかけて開発したものをまとめる。

quic-zig

「そういや QUIC の詳細な仕様は知らないなぁ」で勉強がてらハンドシェイクのみ Zig で実装した。 暗号化部には tls13-zig を使っているため、TLS 1.3 も含めて全部スクラッチしたことになる。 tls13-zig も quic-zig も実験的な実装であるため、もっと実用的な実装(テストも含む)にしたいと思っている。

github.com

研究室内でその知見を共有する機会があったため、その時のスライドの一部を公開している。

speakerdeck.com

bcachefs_exporter

github.com

bcachefs の各種メトリクスを扱う prometheus expoter。

自宅サーバーを更新し、ストレージ用のファイルシステムZFS から bcachefs に乗り換えたため、その監視に用いている。

そういえば自鯖の再構築に関する知見をまとめようとしていたが忘れていたことを思い出した。

rv32-zig

「そういやプロセッサの割り込みや権限管理の詳細な仕組みは知らないなぁ」で勉強がてら RISC-V(rv32imasu)のエミュレータを Zig で実装した。 先人たちの実装は多く存在するが、あえてそれらを読まず仕様書のみで実装し、riscv-tests の該当テスト(rv32ui, rv32um, rv32ua, rv32mi, rv32si)をパスする状態になっている。 ファームウェアとしては OpenSBI が動作する。 Linux も buildroot で作った rootfs が途中まで動いたが、シェルまでは到達できていない。

あるアイデアを試すためにも実装しているが、年内には実装が終わらなかったため来年頑張る。

github.com

出版した論文

bypass4netns: Accelerating TCP/IP Communications in Rootless Containers

bypass4netns は須田瑛大さんの PoC を元に僕が 2022 年の NTT 研究所インターン時から開発している、Rootless Containers の通信を高速化するモジュールである。 昨年 reject となった論文を書き直し、8月の AINTEC 2024 (Asian Internet Engineering Conference) で発表した。

光栄なことに Best Paper Award をもらい、自分としては研究的にも一定の評価が得られたと思っている。

12 月の Container Runtime Meetup #6 でも発表させていただいた。

speakerdeck.com

今年の振り返り・来年の抱負

今年は博士課程2年目にあたり、色々取り組んでいたものをまとめ論文にする作業をしていた。 bypass4netns 以外にも、論文誌に投稿したものが採択され来年出版される予定であり、研究活動はぼちぼち(?)であった。 本来はもっと「研究」をすべきなのだが、なかなか難しく、興味の赴くままに色々調べて実装をするという一般的な博士課程学生とは少し異なる1年であったように思う。

1年を通して、自分の向かう先や存在意義などを問う時間が多かった。これについては年末に書くべきでない事柄であり、機会があればポエムとしてまとめるかもしれない。

来年は博士論文・審査があり、己を試される1年となる。ここまで来たからには折れることなく乗り越えたい。

Mewz on libkrun - その5 Mewz on libkrun してみた

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

目次

ビルド

Mewz on libkrun 用のイメージを用いてビルドする。

$ git clone --recursive http://github.com/naoki9911/mewz-on-libkrun
$ cd mewz-on-libkrun
$ docker run --rm -v $(pwd):/work ghcr.io/naoki9911/mewz-on-libkrun:main /work/build.sh

Mewz on libkrun してみた

ホスト側で chroot_vm を実行する。

$ cd likbrun/examples
$ 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
[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: 14748000 - 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 を libkrun で動かし、Mewz 上で稼働する Web サーバーと通信することができた。 libkrun 自体は crun 等との連携も可能であり、libkrun を用いることで Mewz と、各種コンテナエンジンや Kubernetes との連携がさらに容易になると考えられる。

libkrun は virtio-fs を用いたホスト上のディレクトリのマウントや TSI (Transparent Socket Impersonation) というゲスト側で TCP/IP スタックを必要としない仕組みを持つ。

rheb.hatenablog.com

TSI については既に Mewz に組み込み動作させることには成功しているため、次回以降その詳細についてまとめる予定である。

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 で動作しているサーバーからレスポンスが得られた。

Mewz on libkrun - その3 Mewz 追加実装(Linux zeropage, kernel cmd params)

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

目次

ビルドスクリプトの整備

Mewz kernel の libkrunfw 互換な共有ライブラリへの変換

その2 で、libkrun は libkrunfw を利用することを確認した。 libkrun で Mewz を動かすためには、Mewz kernel のビルド後、適切に libkrunfw 互換な共有ライブラリへ変換する必要がある。

公式の手順に従いビルドされた kernel (mewz.qemu.elf) を libkrunfw が提供する bin2cbundle.py で変換する形で対応した。

実装としては、以下のコミットに対応する。

github.com

zig のビルドスクリプト(build.zig) では、zig build libkrunfw と実行すると WebAssembly と kernel のリンク後、libkrunfw 互換の共有ライブラリへ変換するように処理を追加した。 実行すると、libkrunfw-mewz.so という名前で共有ライブラリが生成される。

const rewrite_kernel_cmd = b.addSystemCommand(&[_][]const u8{"./scripts/rewrite-kernel.sh"});
rewrite_kernel_cmd.step.dependOn(b.getInstallStep());
...    
const libkrunfw_cmd = b.addSystemCommand(&[_][]const u8{"./scripts/gen-libkrunfw.sh"});
libkrunfw_cmd.step.dependOn(&rewrite_kernel_cmd.step);
const libkrunfw_step = b.step("libkrunfw", "build libkrunfw compatible shared library");
libkrunfw_step.dependOn(&libkrunfw_cmd.step);

libkrun への対応

libkrun 側では、起動用の kernel の取得に libkrunfw-mewz.so を利用するようにする必要がある。 また、標準では Port IO 経由で利用するシリアルデバイスは接続されないため、Mewz を利用するときにはシリアルデバイスを接続する必要がある。

これらについては、libkrun の feature として mewz を追加し、mewz feature が有効になっているときには libkrunfw-mewz と Port IO 経由のシリアルデバイスを利用するようにした。 また、examples の chroot_vm についても同様に libkrunfw-mewz を利用するようにした。 これらの改変は以下の mewz-on-libkrun ブランチにある。

github.com

Mewz への追加実装

Linux zeropage への対応

zeropage への対応としては、主に利用可能なメモリ領域の取得と cmd params のポインタの取得を行う。 実装としては、0x7000 に存在する zeropage を読み取れば良い。 対応するコミットは以下である。

github.com

メモリの初期化において、zeropage が持つ E820 を順番に読み取り、利用可能な領域(E820 RAM) を multiboot プロトコルで起動したときと同様の手順で登録する。

pub fn initWithZeroPage(info: zeropage.ZeroPageInfo) void {
    const image_end_addr = @intFromPtr(&image_end);
    {
        // disable alignment checks for mmaps
        @setRuntimeSafety(false);
        var entry_idx: u8 = 0;
        while (entry_idx < info.e820_entry_num) {
            entry_idx += 1;
            const entry = info.e820_entries[entry_idx - 1];
            log.info.printf("E820 Entry [{}] addr=0x{x} size=0x{x} type={}\n", .{ entry_idx, entry.addr, entry.size, entry.type_ });
            if (entry.type_ == 1) { // E820 RAM
                // exclude the kernel image from available memory, because it's already used
                const base = @max(image_end_addr, entry.addr);
                const end = entry.addr + entry.size;
                if (end <= base) {
                    continue;
                }
                log.info.printf("available memory: {x} - {x}\n", .{ base, end });
                // align the range to BLOCK_SIZE
                const aligned_base = util.roundUp(usize, base, BLOCK_SIZE);
                const aligned_end = util.roundDown(usize, end, BLOCK_SIZE);
                const length = aligned_end - aligned_base;
                if (length >= MIN_BOOTTIME_ALLOCATOR_SIZE) {
                    // create a new allocator for the boot time
                    const buf = @as([*]u8, @ptrFromInt(aligned_base))[0..length];
                    boottime_fba = FixedBufferAllocator.init(buf);
                    boottime_allocator = boottime_fba.?.allocator();
                } else {
                    // add the range to the free list
                    initRange(aligned_base, length);
                }
            }
        }
    }
}

環境によっては multiboot プロトコルを使う可能性もあるため、Magic value を読み取り共存する形とした。

    if (boot_magic == 0x2badb002) {
        log.info.print("booted with multiboot1\n");
        const bootinfo = @as(*multiboot.BootInfo, @ptrFromInt(boot_params));
        printBootinfo(boot_magic, bootinfo);
        mem.init(bootinfo);
        param.parseFromArgs(util.getString(bootinfo.cmdline));
    } else {
        log.info.print("booted with linux zero page\n");
        const info = zeropage.parseZeroPageInfo(0x7000);
        mem.initWithZeroPage(info);
    }

kernel command-line parameters への対応

Virtio MMIO への対応をするために、cmd params の読み取り部分を拡張した。 cmd params のアドレスや長さは zeropage の setup_header に含まれるため、それを読み取りパースすればよい。 対応するコミットは以下である。

github.com

以下のように、zeropage の setup_header から文字列を切り出し、param.parseFromArgs 関数で処理する。

const cmd_params = @as([*]u8, @ptrFromInt(info.setup_header.cmd_line_ptr))[0..info.setup_header.cmdline_size];
param.parseFromArgs(cmd_params);

param.parseFromArgs 関数内では、virtio_mmio.device へ対応した。

        } else if (std.mem.eql(u8, k, "virtio_mmio.device")) {
            if (params.mmio_device_num >= params.mmio_devices.len) {
                log.warn.printf("too many virtio_mmio devices. '{s}' is ignored\n", .{v});
                continue;
            }
            // TODO: error handle
            // 4K@0xd0004000:9 SIZE@ADDR:IRQ
            var iter = std.mem.splitScalar(u8, v, '@');
            const size_str = iter.next() orelse continue;
            // TODO: support non 4KiB device
            if (!std.mem.eql(u8, size_str, "4K")) {
                log.warn.printf("vrtio_mmio: size {s} is not supported\n", .{size_str});
                continue;
            }
            // addr_with_irq = 0xd0004000:9
            const addr_with_irq = iter.next() orelse continue;
            // addr_str = 0xd0004000
            iter = std.mem.splitScalar(u8, addr_with_irq, ':');
            const addr_str = iter.next() orelse continue;
            const irq_str = iter.next() orelse continue;
            // addr_str contains prefix '0x', so base=0 is specified.
            const addr = std.fmt.parseInt(usize, addr_str, 0) catch |err| {
                log.warn.printf("failed to parse {s}: {}\n", .{ addr_str, err });
                continue;
            };
            const irq_line = std.fmt.parseInt(usize, irq_str, 10) catch |err| {
                log.warn.printf("failed to parse {s}: {}\n", .{ irq_str, err });
                continue;
            };
            params.mmio_devices[params.mmio_device_num] = virtio_mmio.MMIODeviceParam{
                .addr = addr,
                .size = 4096,
                .irq = irq_line,
            };
            params.mmio_device_num += 1;
        }

文字列としては virtio_mmio.device=4K@0xd0004000:9 のようなサイズ@アドレス:IRQ line 番号 という形で受け渡される。 これをパースし、Virtio MMIOバイスとして登録する。 実際のデバイスの認識処理については次回記述する。

その他の対応: PCIバイスの無効化

libkrun では PCIバイスを利用しないため、PCI バスのスキャンを行う必要はない。 また、本来は無効なデバイスについては VendorID を 0xFFFF とすべきところ、libkrun では 0 となっているため大量のデバイスが存在する判定となってしまう。 デバイスの過多による panic が発生するため、ビルド時に PCIバイスのサポートを無効化するオプションを追加した。

github.com

その他の対応: シャットダウン

Mewz のシャットダウンでは、IO Port 0x501 に対して書き込みを行う。 これは QEMU の起動時に isa-debug-exit,iobase=0x501,iosize=2 のオプションを指定することで、該当ポートへの書き込み時にシャットダウンを行う設定となっているためである。 しかし、libkrun では同様の機能は提供されず、代わりに i8042 の IO Port (0x64) への書き込みをもって自身を停止させるようになっている。

libkrun/src/devices/src/legacy/i8042.rs at b67a6a06a2c7b4534e9d67fcd9dba8626e4834d9 · containers/libkrun · GitHub

            OFS_STATUS if data[0] == CMD_RESET_CPU => {
                // The guest wants to assert the CPU reset line. We handle that by triggering
                // our exit event fd. Meaning Firecracker will be exiting as soon as the VMM
                // thread wakes up to handle this event.
                if let Err(e) = self.reset_evt.write(1) {
                    error!("Failed to trigger i8042 reset event: {:?}", e);
                }
            }

IO Port (0x501) への書き込み後終了しなかった場合については、 IO Port (0x64) へ 0xFE を書き込みことで終了させるようにした。

github.com

ここまでの動作確認

一旦ここまでの動作を確認する。

$ git clone https://github.com/containers/libkrunfw
$ git clone -b mewz-on-libkrun https://github.com/naoki9911/libkrun
$ git clone -b mewz-on-libkrun --recursive https://github.com/naoki9911/mewz
$ cd mewz
$ curl -o helloworld.wat https://raw.githubusercontent.com/Mewz-project/Wasker/main/helloworld.wat
$ wasker helloworld.wat
$ zig build -Dapp-obj=wasm.o -Dlog-level=info -Denable-pci=false libkrunfw
$ cd ../libkrun
$ make MEWZ=1
$ mv target/release/libkrun-mewz.so* lib/.
$ cd examples
$ make
$ sudo LD_LIBRARY_PATH=../lib ./chroot_vm dummy dummy
[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=0x200a0001 type=1
[LOG INFO]: available memory: 144d7000 - 201a0001
[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

Hello, wasker

このように、helloworld.wat について正しく実行できていることが分かる。