libkrunで遊ぶ

Red Hatでコンサルタントをしている織です。本記事ではibkrunについて紹介します。Red Hat Advent Calender 2023の12/3のエントリです (だいぶ過ぎてますが、空いたままだったので急遽埋めました)。

libkrunについては、inductorさんによるKernel/VM探検隊online part2講演が有名かもしれません。本記事(および続編...を書く予定です)では、別の観点でlibkrunをご紹介できればと思います。

libkrunとは

libkrun とは動的ライブラリとして提供される仮想マシンモニター(VMM)です。とても簡単に言うと、このライブラリを使うとプロセスをLinux KVMの仮想マシンとして隔離した状態で実行することができます。CのAPIを提供していますが、中の実装はほとんどRustで書かれています。Firecrackerやrust-vmmのコードも取り込んでいます。

可能な限り軽量かつシンプルな実装を目指しており、使用している仮想デバイスも最小限です。

  • virtio-console
  • virtio-vsock (specialized for TSI, Transparent Socket Impersonation)
  • virtio-fs
  • virtio-baloon (only free-page reporting)
  • virtio-rng
  • virtio-block (for AMD SEV)

libkrunを動かしてみる

さっそく使ってみましょう。最近のFedoraだと、必要なコンポーネントは全てrpmで提供されています。以下の検証はFedora 38が稼働するノートPCで行いました。

$ sudo dnf install -y libkrun libkrun-devel libkrunfw

最小限のコードは以下のような感じになります。

#include <libkrun.h>

void main()
{
    char *const envp[] = {0};
    int ctx_id = krun_create_ctx();
    krun_set_vm_config(ctx_id, 1, 512);
    krun_set_root(ctx_id, "rootfs");
    krun_set_exec(ctx_id, "/bin/bash", 0, &envp[0]);
    krun_start_enter(ctx_id);
}

minimal.c として保存し、コンパイルします。

$ gcc -o minimal miniaml.c -lkrun

rootfs というディレクトリを掘って、そこにLinuxとして稼働できる最小限のファイルツリーを作ります。まずはfedoraイメージでコンテナを実行します。

$ podman run --name fedora fedora uname

コンテナのファイル群をrootfs以下にコピーします。

$ mkdir rootfs
$ podman export fedora | tar xf - -C rootfs

また、後の実験で使ういくつかのファイルもコピーしておきます。

$ sudo cp /etc/resolv.conf ./rootfs/etc/
$ sudo cp /usr/sbin/ip ./rootfs/usr/sbin/
$ sudo cp /lib64/libbpf.so.1 ./rootfs/lib64/
$ sudo cp /lib64/libmnl.so.0 ./rootfs/lib64/

これで準備は整いました。libkrunをリンクした実行ファイル minimal を実行すると、rootfs以下のファイルを使って仮想マシンが作られ、その中でプロセスが起動します。

$ ./minimal
bash-5.2#

minimal.cの krun_set_exec() で呼び出したbashが起動しました。minimalの中では、Fedora 39 のコンテナイメージのファイルを使った仮想マシンが動いています。

bash-5.2# cat /etc/fedora-release
Fedora release 39 (Thirty Nine)

NICは何もついていません。

bash-5.2# /usr/sbin/ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever

が、外と通信はできてしまいます。まずはminimalの中からインターネットにアクセスしてみます。

bash-5.2# curl ftp.jaist.ac.jp
lrwxrwxrwx    1 ftp      ftp            17 Feb 15  2016 debian -> pub/Linux/debian/
lrwxrwxrwx    1 ftp      ftp            27 Feb 15  2016 debian-backports -> pub/Linux/debian-backports/
lrwxrwxrwx    1 ftp      ftp            20 Feb 15  2016 debian-cd -> pub/Linux/debian-cd/
drwxr-xr-x   30 ftp      ftp            32 May 16  2021 pub
lrwxrwxrwx    1 ftp      ftp            36 Feb 15  2016 raspbian -> pub/Linux/raspbian-archive/raspbian/
lrwxrwxrwx    1 ftp      ftp            17 Feb 15  2016 ubuntu -> pub/Linux/ubuntu/

また、minimalを実行したKVMホストからminimalの中にアクセスしてみましょう。 まずはminimalの中で、ポート34000番でWebサーバを起動します。

bash-5.2# python3 -m http.server 34000
Serving HTTP on 0.0.0.0 port 34000 (http://0.0.0.0:34000/) ...

次に別のターミナルを開き、KVMホストから該当ポートにアクセスします。

$ curl localhost:34000
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory listing for /</title>
</head>
<body>
...

libkrunが外と通信する仕組みについては、日を改めて紹介したいと思います こちらに書きました。

Podmanのコンテナを仮想マシンで隔離して実行する

最近のFedoraでは、Podmanを実行する際のOCIランタイムとしてcrunを使用します。crunは、コンテナ実行時に run.oci.handler というアノテーションが渡されると、その情報に応じて実行するハンドラを切り替える仕組みを持っています。この仕組みを使うことで、crunはコンテナを実行する際に、dlopen(3)でlibkrun.soをロードしlibkrunの仕組みを使ってコンテナプロセスを起動します。

$ podman run -d \
-v /dev/kvm:/dev/kvm \
-p 8080:80 \
--annotation=run.oci.handler=krun \
--name nginx
nginx

これで、nginxのコンテナがKVM仮想マシンの中に隔離された状態で起動しました。

$ curl localhost:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
...

プロセスIDから見える情報を確認してみます。

$ pid=$(podman inspect nginx | jq '.[].State.Pid')
$ ps -p ${pid} f
    PID TTY      STAT   TIME COMMAND
2839261 ?        Ssl    0:01 [libcrun:krun] /docker-entrypoint.sh nginx -g daemon off;

なんとなくlibkrunを使って実行されている感があります。 このPIDで使用しているファイルディスクリプタを確認すると、実際KVM仮想マシン内で実行されていることを確認することができます。

$ ls -l /proc/${pid}/fd | grep kvm
lrwx------. 1 ori ori 64 Dec  9 01:24 19 -> anon_inode:kvm-vm
lrwx------. 1 ori ori 64 Dec  9 01:29 29 -> anon_inode:kvm-vcpu:0
lrwx------. 1 ori ori 64 Dec  9 01:29 31 -> anon_inode:kvm-vcpu:1
lrwx------. 1 ori ori 64 Dec  9 01:29 33 -> anon_inode:kvm-vcpu:2
lrwx------. 1 ori ori 64 Dec  9 01:29 35 -> anon_inode:kvm-vcpu:3
lrwx------. 1 ori ori 64 Dec  9 01:29 37 -> anon_inode:kvm-vcpu:4
lrwx------. 1 ori ori 64 Dec  9 01:29 39 -> anon_inode:kvm-vcpu:5
lrwx------. 1 ori ori 64 Dec  9 01:29 41 -> anon_inode:kvm-vcpu:6
lrwx------. 1 ori ori 64 Dec  9 01:29 43 -> anon_inode:kvm-vcpu:7

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。