retrage.github.io

Linux kernelをUnikernelのライブラリ化する

Linux kernelをUnikraftのライブラリ化した. これにより機能の少ないUnikernelの資源として Linux kernelの持つ機能を利用することが可能となる.

背景

Linux Kernel Library

Linux kernelはよくメンテナンスされている成熟した オープンソースなOSの代表であるが, 近年その機能を効率よく再利用しようとする動きがある. Linux Kernel Library (LKL) はその一つであり, 最小限のコストでLinux kernelをanykernelと呼ばれる 形態のLibrary OSとして利用するものである. LKLは現在公式のLinux kernelには含まれていないが 活発に開発がなされており, 現時点でv4.19がサポートされている. 以下にLKLの構成を示す.

LKLの構成

LKLはlklというホスト非依存なアーキテクチャを定義し, 実際に各ホストに依存する部分は分離されている,

LKLはarch/lkl以下にホスト非依存のコードを配置し, tools/lkl/以下にホスト依存のコードが配置されている. 特にtools/lkl/lib/posix-host.cには POSIXなホスト環境で利用されるコードが置かれている.

Unikraft

Unikraft はUnikernelを分割し, アプリケーションが必要とする機能のみを提供することにより イメージサイズが小さくかつ軽量なUnikernelを実現する Xen Projectの実験プロジェクトである. 以下にUnikraftの構成を示す.

Unikraftの構成

Unikraft本体は main libs,platform libs,architecture libsの 大きく3つに分けることができる. main libsにはアーキテクチャやプラットフォームに依存しない ライブラリ群が含まれる. platform libsには対応するプラットフォーム依存のコードが ライブラリとして提供されている. 現時点でpaltform libsには Xen.KVM,Linux userspaceの3つが存在し, architecture libsには x86, arm, arm64の3つが存在する. Unikraftはビルド時にKconfigにより ターゲットとなるアーキテクチャとプラットフォームを指定し それぞれのホストに対応したUnikernelのイメージを生成する.

Unikraftでは本体に含まれるライブラリ以外に アプリケーションや外部ライブラリが導入できる. アプリケーションは Application Development and Porting に記述されている通りのMakefileを書くことにより 少ない労力でUnikraft向けのアプリケーションをビルドできる. Unikraft本体以外の外部ライブラリについても External Library Development の記述通りにMakefile.ukを作成することで ライブラリを導入できる.

Unikraftでは現在公式の外部ライブラリとして newlib, lwip, compiler-rt, eigen, libcxx, libcxxabi, libunwind, libuuidが公開されている.

LKL on Unikraft

Unikraftは前述の通り本体にライブラリを持っているが, Linuxのような成熟したネットワークスタックや ファイルシステムなどを持っていない. そこで,LKLをUnikraftの外部ライブラリとして移植することにより, この問題点を解消することを目指す. ここでは二種類の移植を紹介する.

LKL on Unikraft v1

最初にUnikraftのビルドシステムのみを利用したLKLの移植を行った. 以下にその構成を示す.

LKL on Unikraft v1の構成

この構成ではLKL向けの Unikraftのアーキテクチャとプラットフォームを新たに追加した. このアーキテクチャとプラットフォームはstubであり, Kconfigで指定する際に利用するのみである. また,この設計では他のアーキテクチャやプラットフォームを指定して LKLをビルドすることは不可能となっている. また,LKLをUnikraftのライブラリと連携させることも不可能となっている.

この構成のUnikraftは以下で公開している.

LKL on Unikraft v2

次に実際にLKLをUnikraftに移植したv2を紹介する. 以下にその構成を示す.

LKL on Unikraft v2の構成

この構成ではLKLをUnikraftの外部ライブラリとして移植するため, アーキテクチャやプラットフォームはUnikraft由来のものが利用可能である. また,v1では不可能だったUnikraftの他のライブラリとの連携も可能となっている. LKL側から見た場合,Unikraftは新しいホスト環境となるため, uk-host.cという新しいホスト依存コードを作成している.

実装上,LKLのホスト依存コードは mutex, semaphore, thread, timerなどを ホスト側でサポートしている必要がある. 一方Unikraftでは現在cooporative schedulerのみ実装されており, LKLの期待するpreemptive schedulerを持っていない. また,Unikraftの持っているmutexやsemaphoreなども貧弱なため, これらを利用することができない. そこで,littlekernel という組み込み向けOSよりこれらの機能を移植することにより この問題を解決する. LKLに対してlittlekernelのコードを移植し. ホスト側では一定時間にコールバック関数を呼び出しさえすれば preemptive schedulerが動作するようにした. これについてはUnikraftとは分離し独立した実装となるようにした. このLKLの実装は以下で公開している.

実装

この実装ではターゲットのアーキテクチャをx86_64のみとし, プラットフォームはKVMのみとしている.

Unikraftは外部ライブラリとして libcの実装の一つであるnewlibを持っている. 一方でUniraftはnewlibを利用しない場合でも 一般的なlibcの機能が利用できるように lib/nolibcという内部ライブラリを提供している. LKL on Unikraft v2でもnewlibに依存しないようにするため, このnolibcを用いるように設計している. 一方でLKLの期待するlibcの関数や定数をnolibcが持っていなかったため, Unikraftに対して次のような関数や定数をnolibcに追加している.

  • stdbool
  • fputc, putchar
  • STD{IN,OUT,ERR}_FILENO
  • strncat
  • strtok_r
  • setjmp/longjmp

また,前述の通りLKLは最低限一定時間ごとに あるコールバック関数が呼び出される必要があるが, Unikraftは起動時にperiodic timerを起動するものの, ライブラリやアプリケーションからはコールバック関数を登録できないため, コールバック関数を登録するようなインターフェースを追加している.

またKVMでは独自のリンカスクリプトを用いてリンクを行うが,デフォルトではLinux kernel由来のシンボルが明示的に参照されないために必要なシンボルが削除されてしまう.このためリンカスクリプトに対してLKL向けの記述を追加している.

以上の修正を行ったUnikraftとUnikraft向けライブラリ化したLKLは以下で公開している.

ビルドと実行

Unikraft向けのLKLをビルドしてみる. ここではLKLの機能を確認するためのテストアプリケーションである tools/lkl/tests/bootをUnikraftアプリケーション化した uk-lkl/boot を用いる.

以下のようにビルドを行う.

$ mkdir unikraft && cd unikraft
$ git clone git@github.com:uk-lkl/unikraft.git --branch=retrage/lkl-v2
$ mkdir libs && cd libs
$ git clone --recursive git@github.com:uk-lkl/lkl.git
$ cd ..
$ mkdir apps && cd apps
$ git clone git@github.com:uk-lkl/boot.git
$ cd boot
$ make menuconfig

ここでKconfigでアーキテクチャにx86を, プラットフォームにKVM guestかLinux user spaceを選択する. 最後に保存してKconfigを抜ける. 以下のようにmakeによりビルドを実行する.

$ make

ビルドが成功するとbuild/boot_kvm-x86_64が生成される.

KVM guestでは用意されたrun.shにより実行できる.

$ ./run.sh

実行結果は以下のようになる.

[    0.721134] ERR:  [libukboot] boot.c @ 88   : Failed to initialize bus driver 0x56b060: -1
Welcome to  _ __             _____
 __ _____  (_) /__ _______ _/ _/ /_
/ // / _ \/ /  '_// __/ _ `/ _/ __/
\_,_/_//_/_/_/\_\/_/  \_,_/_/ \__/
                  Titan 0.2~ebcb42a
1..33 # boot
* 1 mutex
ok 1 mutex
 ---
 time_us: 0
 log: |
 ...
* 2 semaphore
ok 2 semaphore
 ---
 time_us: 0
 log: |
 ...
* 3 join
ok 3 join
 ---
 time_us: 1
 log: |
  joined 7909384
 ...
* 4 start_kernel
ok 4 start_kernel
 ---
 time_us: 9281
 log: |
  [    0.000000] Linux version 4.19.0+ (akira@akira-Z270) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.11)) #1 Thu May 30 23:10:09 JST 2019
  [    0.000000] bootmem address range: 0x1c001000 - 0x1d000000
  [    0.000000] On node 0 totalpages: 4095
  [    0.000000]   Normal zone: 56 pages used for memmap
  [    0.000000]   Normal zone: 0 pages reserved
  [    0.000000]   Normal zone: 4095 pages, LIFO batch:0
  [    0.000000] pcpu-alloc: s0 r0 d32768 u32768 alloc=1*32768
  [    0.000000] pcpu-alloc: [0] 0 
  [    0.000000] Built 1 zonelists, mobility grouping off.  Total pages: 4039
  [    0.000000] Kernel command line: mem=16M loglevel=8
  [    0.000000] Dentry cache hash table entries: 2048 (order: 2, 16384 bytes)
  [    0.000000] Inode-cache hash table entries: 1024 (order: 1, 8192 bytes)
  [    0.000000] Memory available: 16088k/16380k RAM
  [    0.000000] SLUB: HWalign=32, Order=0-3, MinObjects=0, CPUs=1, Nodes=1
  [    0.000000] NR_IRQS: 4096
  [    0.000000] lkl: irqs initialized
  [    0.000000] clocksource: lkl: mask: 0xffffffffffffffff max_cycles: 0x1cd42e4dffb, max_idle_ns: 881590591483 ns
  [    0.198000] lkl: time and timers initialized (irq1)
  [    2.093000] pid_max: default: 4096 minimum: 301
  [   15.926000] Mount-cache hash table entries: 512 (order: 0, 4096 bytes)
  [   18.024000] Mountpoint-cache hash table entries: 512 (order: 0, 4096 bytes)
  [ 2722.817000] console [lkl_console0] enabled
  [ 2732.709000] clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 19112604462750000 ns
  [ 2760.874000] random: get_random_u32 called from bucket_table_alloc.isra.6+0x9b/0x250 with crng_init=0
  [ 2774.823000] NET: Registered protocol family 16
  [ 3121.319000] clocksource: Switched to clocksource lkl
  [ 3181.520000] NET: Registered protocol family 2
  [ 3261.648000] tcp_listen_portaddr_hash hash table entries: 256 (order: 0, 4096 bytes)
  [ 3263.125000] TCP established hash table entries: 512 (order: 0, 4096 bytes)
  [ 3264.198000] TCP bind hash table entries: 512 (order: 0, 4096 bytes)
  [ 3265.141000] TCP: Hash tables configured (established 512 bind 512)
  [ 3286.743000] UDP hash table entries: 128 (order: 0, 4096 bytes)
  [ 3287.830000] UDP-Lite hash table entries: 128 (order: 0, 4096 bytes)
  [ 3456.331000] workingset: timestamp_bits=62 max_order=12 bucket_order=0
  [ 4041.077000] SGI XFS with ACLs, security attributes, no debug enabled
  [ 6032.987000] jitterentropy: Initialization failed with host not compliant with requirements: 2
  [ 6038.248000] io scheduler noop registered
  [ 6038.908000] io scheduler deadline registered
  [ 6067.383000] io scheduler cfq registered (default)
  [ 6068.093000] io scheduler mq-deadline registered
  [ 6068.764000] io scheduler kyber registered
  [ 7894.266000] NET: Registered protocol family 10
  [ 7979.259000] Segment Routing with IPv6
  [ 7988.492000] sit: IPv6, IPv4 and MPLS over IPv4 tunneling driver
  [ 8097.859000] Warning: unable to open an initial console.
  [ 8106.456000] This architecture does not have kernel memory protection.
  [ 8107.119000] Run /init as init process
  lkl_start_kernel(&lkl_host_ops, "mem=16M loglevel=8") = 0 
 ...
* 5 getpid
ok 5 getpid
 ---
 time_us: 5
 log: |
  lkl_sys_getpid() = 1 
 ...
* 6 syscall_latency
ok 6 syscall_latency
 ---
 time_us: 126
 log: |
  avg/min/max: lkl:107822000/104000000/450000000 native:6788000/6000000/77000000
 ...
* 7 umask
ok 7 umask
 ---
 time_us: 0
 log: |
  lkl_sys_umask(0777) = 18 
 ...
* 8 umask2
ok 8 umask2
 ---
 time_us: 0
 log: |
  lkl_sys_umask(0) = 511 
 ...
* 9 creat
ok 9 creat
 ---
 time_us: 9
 log: |
  lkl_sys_creat("/file", access_rights) = 0 
 ...
* 10 close
ok 10 close
 ---
 time_us: 0
 log: |
  lkl_sys_close(0) = 0 
 ...
* 11 failopen
ok 11 failopen
 ---
 time_us: 9
 log: |
  lkl_sys_open("/file2", 0, 0) = -2 No such file or directory
 ...
* 12 open
ok 12 open
 ---
 time_us: 7
 log: |
  lkl_sys_open("/file", LKL_O_RDWR, 0) = 0 
 ...
* 13 write
ok 13 write
 ---
 time_us: 4
 log: |
  lkl_sys_write(0, wrbuf, sizeof(wrbuf)) = 5 
 ...
* 14 lseek_cur
ok 14 lseek_cur
 ---
 time_us: 0
 log: |
  lkl_sys_lseek(0, 0, LKL_SEEK_CUR) = 5 
 ...
* 15 lseek_end
ok 15 lseek_end
 ---
 time_us: 0
 log: |
  lkl_sys_lseek(0, 0, LKL_SEEK_END) = 5 
 ...
* 16 lseek_set
ok 16 lseek_set
 ---
 time_us: 0
 log: |
  lkl_sys_lseek(0, 0, LKL_SEEK_SET) = 0 
 ...
* 17 read
ok 17 read
 ---
 time_us: 1
 log: |
  lkl_sys_read=5 buf=test
 ...
* 18 fstat
ok 18 fstat
 ---
 time_us: 1
 log: |
  lkl_sys_fstat=0 mode=100721 size=5
 ...
* 19 mkdir
ok 19 mkdir
 ---
 time_us: 8
 log: |
  lkl_sys_mkdir("/mnt", access_rights) = 0 
 ...
* 20 stat
ok 20 stat
 ---
 time_us: 7
 log: |
  lkl_sys_stat("/mnt")=0 mode=40721
 ...
* 21 pipe2
ok 21 pipe2
 ---
 time_us: 9
 log: |
 ...
* 22 epoll
ok 22 epoll
 ---
 time_us: 5
 log: |
 ...
* 23 mount_fs_proc
ok 23 mount_fs_proc
 ---
 time_us: 18
 log: |
  lkl_mount_fs("proc") = 0 
 ...
* 24 chdir_proc
ok 24 chdir_proc
 ---
 time_us: 7
 log: |
  lkl_sys_chdir("proc") = 0 
 ...
* 25 open_cwd
ok 25 open_cwd
 ---
 time_us: 7
 log: |
 ...
* 26 getdents64
ok 26 getdents64
 ---
 time_us: 6
 log: |
  4 . .. fs bus irq net sys tty kmsg maps misc stat iomem crypto driver 
  mounts uptime vmstat cmdline cpuinfo devices ioports loadavg meminfo version 
  consoles kallsyms slabinfo softirqs zoneinfo buddyinfo diskstats interrupts 
  partitions timer_list 
 ...
* 27 close_dir_fd
ok 27 close_dir_fd
 ---
 time_us: 0
 log: |
  lkl_sys_close(dir_fd) = 0 
 ...
* 28 chdir_root
ok 28 chdir_root
 ---
 time_us: 7
 log: |
  lkl_sys_chdir("/") = 0 
 ...
* 29 umount_fs_proc
ok 29 umount_fs_proc
 ---
 time_us: 11
 log: |
  lkl_umount_timeout("proc", 0, 1000) = 0 
 ...
* 30 lo_ifup
ok 30 lo_ifup
 ---
 time_us: 44
 log: |
  lkl_if_up(1) = 0 
 ...
* 31 gettid
ok 31 gettid
 ---
 time_us: 0
 log: |
  7893000
 ...
* 32 many_syscall_threads
ok 32 many_syscall_threads
 ---
 time_us: 26
 log: |
 ...
* 33 stop_kernel
ok 33 stop_kernel
 ---
 time_us: 531
 log: |
  [145272.871000] reboot: Restarting system
  lkl_sys_halt() = 0 
 ...

問題点

現状のLKL on Unikraftには次のような問題点がある.

  • Unikraft本体に対する追加修正が必要
  • ディスクやネットワークのホスト依存コードが存在しない
  • ある条件下においてLKLがハングする

一つ目の問題点は前述の通りの修正についてである. 二つ目の問題点では開発時点においてUnikraft側に 有効なファイルシステムやネットワークのサポートがなかったためである. 三つ目の問題点はLKL on Unikraftのみではなく retrage/linux:retrage/fiber の問題となっている.

また.設計上の問題点として UnikraftはUnikernelを複数のライブラリに分割することにより 必要なライブラリのみをビルドするという方向性であるのに対し, LKLでは既存のLinux kernelを再利用するために LKLを取り込んだ場合にイメージサイズが大きくなりがちであり 必要な機能のみを取り込むというUnikraftの方向性に反するものである という点が挙げられる.

参考文献