Mewz (WebAssembly x Unikernel) を libkrun で動かしてみた - 外部記憶装置 の詳細を記すシリーズ
- その1 libkrun を試す
- その2 libkrun の構造
- その3 Mewz 追加実装(Linux zeropage, kernel cmd params)
- その4 Mewz 追加実装(Virtio MMIO)
- その5 Mewz on libkrun してみた
- その6 Mewz 追加実装(virtio-console)
- その7 virtio-vsock について
- その8 Mewz 追加実装(virtio-vsock) ← この記事
目次
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 デバイスと変わらないため、詳細は省略する。
なお、ホスト側では受け口として /dev/vhost-vsock
を用いている。
これを用いることで、カーネル側で virtio-vsock について接続処理等を行い、ユーザープロセスから AF_VSOCK
を指定することでソケットAPI経由の操作を行うことができる。
ソケットの vsock への対応
vsock ソケットの実装
前回説明したとおり、virtio-vsock においてはドライバそのものよりその上に構築されるソケットの実装が重要となる。 Mewz への実装にあたり、すでに用意されているソケットの仕組みを利用しつつ下図のような vsock 向けのソケットを実装した。
実装については、前回まとめた接続処理に対応する動作を実装すればよい。 vsock の自体は以下のファイルにまとめられている。
構造としては、VsockMuxer が受け取ったパケットを対応する VsockSocket に対して受け渡し、各 vsock ソケットの状態を含めて管理する形となっている。
この時、どのポートに bind されているか、listen, 接続状態にあるかなどを管理し、意図しない挙動であれば VIRTIO_VSOCK_OP_RST
を返すといった基本的なエラー処理も実装している。
詳細については process_rx
関数に記述されたハンドラを理解しやすい。
既存ソケット・WASI API との統合
既存ソケットへ統合は stream.zig
でストリーム系のAPIをまとめて管理している部分に vsock も追加する形で行った。
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 の実装と合わせている。
pub const AddressFamily = enum(i32) { Unspec = 0, INET4 = 1, INET6 = 2, VSOCK = 40, };
WASI API に対応する実装については現状すべては網羅できていない。
例えば、connect(2)
に相当する WASI API である sock_open
では、宛先の CID は "2" つまりホストに限定され、ポートのみ指定する形となっている。
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 についても未対応である。
これらについては、後述するユーザープログラム向けのライブラリも含めて今後対応する必要がある。
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 をフォークして対応した。
対応個所としては AddressFamily
に Vsock
を追加しただけである。
ユーザープログラムはソケット作成時に Vsock
を指定すれば vsock なソケットを利用できる。
アドレスについては std::net::Ipv4Addr
で指定し、前述したようにポート番号のみ利用する。
#[derive(Copy, Clone, Debug)] #[repr(u8, align(1))] pub enum AddressFamily { Unspec, Inet4, Inet6, Vsock = 40, }
ここまでの動作確認
WebAssembly アプリケーションとして、vsock ソケットを利用した簡易的な Echo サーバーを実装した。
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 で記述した。
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
となり、クライアント側のメッセージを受信し正しく送信できていることが分かる。