Unikernelについての現状調査

2020年3月12日

※本記事の内容は、2020年2月の約1か月間のインターンシップにおける成果です。

Unikernelについて知っていますか? 私は知っています。 Unikernelとは、アプリケーションをライブラリOSの必要最小限の機能のみとリンクさせ、単一アドレス空間で、直接ハードウェア上やハイパーバイザ上にて動作するようにしたマシンイメージのことです。

本記事では、車載向けのマイコンなどの組み込みシステム上において、最小サイズのマシンイメージで目的の機能を実現することができる手段として、Unikernelという方向性に可能性を見出し、その概要と、2020年2月現在に現存しているUnikernelのプロジェクトについて調査しました。

Unikernelとは

Unikernelという呼称は、2013年に発表された論文 Unikernels: Library Operating Systems for the Cloud [1] において、MirageOSというOCamlで実装されたUnikernelのプロジェクトと共に提案されました。

クラウドサービスにおいて、ユーザーは仮想マシン上で動作するVMで単一目的のアプリケーションを動作させるのが一般的になっているのに対し、VMとしてはgeneral-purposeなOSが採用され、イメージサイズや使用されている機能の範囲の観点で無駄が多いということへの着目を背景としています。

つまり [1] においては、クラウドサービスの基盤としてのハイパーバイザー上で、一つのVMとして最小の構成で目的機能を実現するマシンイメージを生成することを、Unikernelの目的としています。

本記事の冒頭でも述べた通り、Unikernelは単一機能のアプリケーションのみを、単一アドレス空間で動作させるマシンイメージであり、用意されたライブラリOSの中から、必要なオブジェクトファイルのみをアプリケーションのオブジェクトファイルとリンクして作られます。

[1] において実装されたMirageOSは、XenやKVMといったハイパーバイザー上で動作することを想定しており、以下の図で示されるように、ハイパーバイザー上でアプリケーションを含んだシングルバイナリとしてのUnikernelが直接動作するという、非常にシンプルな構成になります。

hypervisor上で動作する、従来のappとunikernelによるappのレイヤー比較 [1]

Unikernelは、ライブラリOSとアプリケーションを単にリンクしたシングルバイナリであり、ライブラリOSによる機能とアプリケーションは同じ特権モードで動作します。つまり、汎用的なOS上で動作するアプリケーションはOSの機能を利用するためにシステムコールを発行しますが、Unikernelにおいては、OSの機能は単なる関数の呼び出しによってアクセスされます。

アドレス空間も単一であり、動作の形態としてはベアメタルで動作するアプリケーションと同じです。この観点から言うと、Unikernelとは、ハードウェア抽象化レイヤとOSとしての主要機能を提供するライブラリがある程度用意された状況で、アプリケーションコードのみを書くという手間のみで構築できる、ベアメタルで動作するマシンイメージと見なせそうです。Unikernelが定めるメモリマッピングになるという点で少し違いますが。

アプリケーションに必要な機能のみをリンクするので、general-pusposeなOSによるVMに比べてイメージのサイズは当然小さくなります。また、システムコールやマルチプロセシングによるオーバーヘッドが存在せず、アプリケーションコードとライブラリOSのコードにまたがった最適化を施すこともできるので、各アプリケーションにおけるパフォーマンスの改善が望めます。起動時間も短く、VMを頻繁に起動したい場面に非常に向いています。メモリフットプリントが小さくなる利点もあります。また、不必要なOSの機能を削ぎ落としているため、attack surfaceが非常に少なく、さらにビルドしたアプリケーションごとに異なるため、セキュリティの向上も望めます。

車載システムにおけるUnikernelの可能性

冒頭でも述べた通り、本記事のUnikernelの調査は、車載システムにおける今後のOSレイヤのアーキテクチャの可能性を探るのが目的です。この章では、車載システムにおいて発生している要望と、そこにおけるUnikernelの可能性について述べます。

自動車の制御はかつて機械的な機構で行われていましたが、現在の自動車においては電気的な制御が行われており、ECU(Electric Control Unit)と呼ばれるマイコンがその中心的な役割を果たします。各ECUには特定の役割が与えられ、現代の自動車においては多いもので100を超えるECUが搭載されます。ECU同士はCAN(Control Area Network)などのプロトコルで通信し、全体として自動車の機能を成します。

しかし、高性能化に伴うECUの増加は、コスト面、スペース面、複雑性の面で問題を引き起こすため、ECUを統合化する要求が発生しました [2]。別個のECUに持たせていた機能の単一ECUへの統合には、ハイパーバイザーによる仮想化が利用されます。標準の高レベルAPIを備えるOSを必要とし、ベストエフォート型のシステムであるインフォテイメントから、厳しいリアルタイム性を満たさなくてはならない制御系まで、それぞれ別個のVMとして動作させます。

制御系のVMで動作させるOSは、欧州発の車載OS標準仕様であるAUTOSARに収束しつつあるので、制御系の機能開発の部分が新しいプラットフォームに置き換わることは考えにくいです。しかし、インフォテイメントのように、Linuxのようなgeneral-purposeなOSの高レベルAPIを必要とするアプリケーションを動作させるVMにおいては、Unikernelが導入される可能性があります [3]。

先述したように、Unikernelのイメージのサイズは小さく、メモリフットプリントも小さくなるため、制約の厳しい車載システムのような組み込みシステムと相性が良いです。またこれも先述したように、必要最低限の機能のみをマシンイメージに含めるために、attack surfaceを減らすことができ、セキュリティ面の改善が望めます。

MirageOSを動かしてみる(動かない)

手元にaarch64のマイコン上で動作するXenの環境を構築していたので、domUドメインにてMirageOSを動かしてみることにしました (MirageOSを選んだのは、Unikernelの概念に対する最初の参照実装である上に、現在でも盛んに開発が行われていたためです)。aarch64向けのクロスコンパイル環境を構築し(Qemuのユーザーエミュレーションなど)、MirageOSをインストール、そしてhello world appをxen向けにビルドします。

$ sudo apt-get update
$ sudo apt-get install opam
$ opam init
$ opam switch create <version>
$ eval `opam config env`
$ opam install mirage 
$ git clone git://github.com/mirage/mirage-skeleton.git
$ cd mirage-skeleton/tutorial/hello
$ mirage configure -t xen
$ make depend
$ make

しかし、make dependによる依存パッケージのビルドが通りません。エラーメッセージを見てみると、

start_info;
     ^
In file included from blkfront.c:7:0:
/home/ubuntu/.opam/4.06.0/.opam-switch/build/minios-xen.0.9/arch/arm/include/os.h:155:2: error: #error "Unsupported architecture"
 #error "Unsupported architecture"
  ^
minios.mk:78: recipe for target '/home/ubuntu/.opam/4.06.0/.opam-switch/build/minios-xen.0.9/blkfront.o' failed
make: *** [/home/ubuntu/.opam/4.06.0/.opam-switch/build/minios-xen.0.9/blkfront.o] Error 1

とあり、”Unsupported architecture” という不穏な文字列を残してMiniOSのビルドが失敗しています。

そこで、MirageOSのissueにてaarch64/Xenのサポートについて質問してみたところ、Xen向けのMirageOSが依存しているMiniOSが現段階でaarch64/Xenをサポートしていないという回答をいただきました。。。

どうしても手元にあるaarch64/Xenの環境で動作実験をしたかったので、portingを行った記録の検索、または行うための情報収集をしました。

MiniOSがaarch64/Xenをサポートしない話

情報収集ののち、aarch64/Xenの環境でMiniOSを動作させるportingを行ったリポジトリとそのアナウンスを見つけました。メーリスのアナウンスに従ってビルドを行ってみます。

$ git clone https://github.com/baozich/mini-os.git
$ git submodule add -f https://git.kernel.org/pub/scm/utils/dtc/dtc.git dtc
$ cd dtc
$ git checkout -b tag v.1.4.5
$ cd ..
$ CONFIG_TEST=y CONFIG_START_NETWORK=n CONFIG_BLKFRONT=n CONFIG_NETFRONT=n 
  CONFIG_FBFRONT=n CONFIG_KBDFRONT=n CONFIG_CONSFRONT=n CONFIG_XC=n \
  MINIOS_TARGET_ARCH=arm64 CROSS_COMPILE=aarch64-elf-  make

しかし、ビルドに失敗します。どうして。。。

In file included from /usr/include/newlib/sys/reent.h:15:0,
                 from /usr/include/newlib/stdlib.h:18,
                 from dtc/libfdt/libfdt_env.h:57,
                 from dtc/libfdt/fdt.c:51:
/usr/include/newlib/sys/_types.h:83:9: error: unknown type name '_LOCK_RECURSIVE_T'
 typedef _LOCK_RECURSIVE_T _flock_t;
         ^
minios.mk:69: recipe for target '/home/takahiro_ishikawa/work/mini-os/dtc/libfdt/fdt.o' failed
make: *** [/home/takahiro_ishikawa/work/mini-os/dtc/libfdt/fdt.o] Error 1

色々原因を探していたところ、xenのユーザメーリス(xen-users@lists.xensource.com)の中に、miniosのarmサポートに関する回答を見つけました。その回答によると、

Support for Arm in Mini-OS is in particularly bad step and I don’t expect any update there are as we are focusing to Unikraft.

との言及がありました。どうやら、Xen ProjectにおいてはもうMiniOSには注力しておらず、Unikraftと呼ばれるUnikernelに注力しているとのことです。つらい。ここで、MirageOSの動作実験を諦め、Unikraftについての調査と動作実験をすることに方向転換します。

Unikraftとは

Unikraftは、2017年始めにNEC Labで実験プロジェクトとして始まり、2017年10月にXen Indubator Projectに採択された、Unikernelを構築するためのライブラリ群です。

Unikraftは、既存のUnikernelが一般に普及しない主な理由として、Unikernelとしてビルドするために必要な既存アプリケーションのportingの困難さを挙げ、porting作業をほとんど必要とせずに既存のアプリケーションをUnikernelとしてビルド可能にすることを目的として立ち上がりました。

Unikraftが辿り着く理想的な状態として、

  1. アプリケーションが使用しうるすべてのライブラリが使用可能な状態になっており
  2. ビルド時にそのライブラリ群の中から必要なもののみとリンクし
  3. 自動的にアプリケーションの最小の実行イメージを対象プラットフォーム向けにビルドできる

状態としています。

また、第二の目的として、利用者各々が自分のプロジェクトにおけるportingの作業に時間を費やすのではなく、共通のコードベースにライブラリを提供するという形の貢献を可能にすることも挙げています。

現在Xenコミュニティにおいて、様々に乱立している(特にMiniOSベースの)Unikernelのプロジェクトに分散して注がれている労力を、Unikraftのプロジェクトに集中させたいという目的もあります。

以下に、Unikraftが提供するライブラリ群と、Unikernelのビルドイメージが生成される過程を示した概念図を示します。

Unikraftのライブラリ群とビルドシステム  (Unikraft project)

Unikraftのプロジェクトは、Unikraftのメインリポジトリと、多数の公式ライブラリのリポジトリを提供しています。今回はUnikraftでUnikernelのビルドがどのように行われるのかを見るために、turorialに従い、公式のsample appsの一つとして提供されているhttpreply appをビルドしてみます。

ローカルで、以下のようなディレクトリ構成になるように、Unicraft Project repos からそれぞれのリポジトリをcloneします。

├── apps
│   └── httpreply
├── libs
│   ├── lwip
└── unikraft
// Clone the main Unikraft repo
$ git clone git://xenbits.xen.org/unikraft/unikraft.git
// Clone external library repo (lwip)
$ git clone git://xenbits.xen.org/unikraft/libs/lwip.git
// Clone sample app
$ git clone git://xenbits.xen.org/unikraft/apps/httpreply.git

Unikraftのメインリポジトリの中身を見てみると、大きく分けて、アーキテクチャ依存のコード群、プラットフォーム依存(Xen, KVM, etc..)のコード群、そしてメインのライブラリ群から構成されています。

このメインリポジトリでホストされているライブラリ群に加えて、アプリケーションに必要な外部ライブラリをさらに、上のディレクトリツリーにおけるlibsディレクトリ以下にcloneする必要があります。

必要なライブラリが全てcloneできたら、アプリケーションのディレクトリへ移動し、make menuconfigします。

$ cd /path/to/apps/httpreply
$ make menuconfig

すると、以下のような設定画面が現れます。

Unikraftのビルドシステムは、LinuxのKconfigを利用しており、インタラクティブにビルドイメージの設定を行うことができます。

さて、先ほど述べたように、今回は手元にaarch64/Xenの環境があるので、さっそく設定画面からアーキテクチャとプラットフォームの指定をしてみます。

あれ、、、ない、、、。

aarch64/Xenの組み合わせがない。。aarch32/Xenとaarch64/KVMは対応しているのに。。。どうやら2020年2月現在aarch64/Xenの組み合わせにはUnikraftもまだ対応していないようです。

リサーチをする時間も限られているので、諦めて開発用のマシン上に仮想マシン(VirtualBox)を立ち上げ、その上でXenの環境を作り、現在対応しているx86_64/Xenの組み合わせでUnikernelをビルドして動作させてみることにします。

先ほどの立ち上げたmenuconfigで、x86_64/Xenを指定し、さらに外部ライブラリにlwipを指定していることを確認します。設定し終えてexitすると、カレントディレクトリに.configファイルが生成されているので、この状態で makeします。するとbuildディレクトリ以下に指定したアーキテクチャとプラットフォーム向けのビルドイメージが生成されています。

$ make
$ ls -sh build/

そして、UnikernelをdomUに立ち上げます。

$ xl create -c httpreply_xen-x86_64

httpレスポンスを受け取って返すだけのサーバーが立ち上がりました!

gdbでUnikraftの動作/内部構成を調べる

Unikraftが先ほど示したライブラリ群とビルドシステムのような階層的な構成をどのような実装で実現しているのかを調べるために、gdbでステップ実行してみます。まずは、公式でサポートされているhelloworld appをx86_64/Xenとx86_64/KVM向けに先ほどと同様の手順でビルドします。その際、Optimization LevelをNo optimizationに、Debugging informationをLevel3に指定します

まず、x86_64/Xenのイメージを実行するために、ビルドイメージを仮想マシン上のdom0に送信します。今回はgdbでソースが欲しいので、プロジェクト全体ごと送信します。一般に、dom0からdomUのデバッグをgdbで行いたい場合、gdbsx (gdb server for Xen)をdom0におけるプロセスとして立ち上げます(以下の手順はUnikraft tutorialのDebuggingの章に従っています)。

まず、httpreplyのときと同様に設定ファイルを書き、UnikernelのドメインをdomUとして立ち上げます。このとき、-pオプションによって実行をポーズさせ、さらに xl listでドメインIDを確かめておきます。

$ xl create -c -p helloworld.cfg
$ xl list

別のterminalからdom0に入り、gdbsxを立ち上げます。

$ gdbsx -a <domain id> <architecture bit width(64)> <port (9999)>

さらに別のterminalからdom0に入り、gdbクライアントを接続します。このときdbg拡張子のファイルを指定するようにします。

$ gdb --eval-command="target remote :9999" path/to/helloworld_xen-x86_64.dbg

以上でgdbのセッションが始まります。別のホストでビルドしたイメージを実行しているため、ソースコードへのパスを修正しておきます。

(gdb) set substitute-path <prior path> <current path> 

さて、次は比較のために、KVM上でUnikernelを動かします。以下の手順はUnikraftのリポジトリでホストされているdocに従っています。

まず、kvm上でUnikernelのVMを立ち上げます。-sオプションは-gdb tcp::1234の省略です。

$ qemu-system-x86_64 -s -S -cpu host -enable-kvm -m 128 -nodefaults -no-acpi -display none -serial stdio -device isa-debug-exit -kernel path/to/helloworld_kvm-x86_64 -append verbose

別のterminalからgdbクライアントを接続します。

$ gdb --eval-command="target remote :1234" path/to/helloworld_kvm-x86_64.dgb

bootコード以降(__libkvmplat_start32以降)をデバッグする場合、hardware break pointを設定します。また、CPUアーキテクチャを正確に設定します。

(gdb) hbreak <location>
(gdb) continue
(gdb) disconnect
(gdb) set arch <cpu architecture>
(gdb) tar remote localhost:1234
(gdb) continue

さて、以上でXenとKVMの両方で、x86_64対応のhelloworld appが立ち上がったので、双方のステップ実行を進めてみて、Unikraftの構成を調べてみます。

以下は、printf関数を呼んで、ある程度スタックが深くなったときのスタックトレースの出力です(フルパスだと長くて見にくいので相対パスに書きかえました)

printf() on x86_64/Xen

#0  HYPERVISOR_event_channel_op (cmd=4, op=0x15f9b0)
    at unikraft/plat/xen/include/xen-x86/hypercall64.h:255
#1  0x00000000000074fa in notify_remote_via_evtchn (port=2)
    at unikraft/plat/xen/include/common/events.h:98
#2  0x00000000000076c1 in hv_console_output (str=0x15fa50 "Hello world!\n", len=13)
    at hello_unikraft/unikraft/plat/xen/hv_console.c:179
#3  0x0000000000007404 in ukplat_coutk (str=0x15fa50 "Hello world!\n", len=13)
    at unikraft/plat/xen/console.c:92
#4  0x000000000000c811 in vfprintf (fp=0x1c168 <stdout>, fmt=0x18e46 "Hello world!\n", ap=0x15fea0)
    at unikraft/lib/nolibc/stdio.c:443
#5  0x000000000000c929 in vprintf (fmt=0x18e46 "Hello world!\n", ap=0x15fea0)
    at unikraft/lib/nolibc/stdio.c:466
#6  0x000000000000c9ca in printf (fmt=0x18e46 "Hello world!\n")
    at unikraft/lib/nolibc/stdio.c:475
#7  0x0000000000009246 in main (argc=1, argv=0x60000 <argv.2687>)
    at apps/helloworld/main.c:8

printf() on x86_64/KVM

#0  inb (port=1021)
    at unikraft/plat/common/include/x86/cpu.h:219
#1  0x0000000000108c32 in serial_tx_empty ()
    at unikraft/plat/kvm/x86/serial_console.c:56
#2  0x0000000000108c4d in serial_write (a=72 'H')
    at unikraft/plat/kvm/x86/serial_console.c:61
#3  0x0000000000108c8f in _libkvmplat_serial_putc (a=72 'H')
    at unikraft/plat/kvm/x86/serial_console.c:71
#4  0x0000000000107c71 in ukplat_coutk (buf=0x7f1fa50 "Hello world!\n", len=13)
    at unikraft/plat/kvm/x86/console.c:67
#5  0x000000000010ec85 in vfprintf (fp=0x11f0c8 <stdout>,
    fmt=0x11bc2c "Hello world!\n", ap=0x7f1fea0)
    at unikraft/lib/nolibc/stdio.c:443
#6  0x000000000010ed9d in vprintf (fmt=0x11bc2c "Hello world!\n", ap=0x7f1fea0)
    at unikraft/lib/nolibc/stdio.c:466
#7  0x000000000010ee3e in printf (fmt=0x11bc2c "Hello world!\n")
    at unikraft/lib/nolibc/stdio.c:475
#8  0x000000000010b6ba in main (argc=2, argv=0x1245a0 <argv>)
    at apps/helloworld/main.c:8

printf()の呼び出しでは、Unikraftのメインリポジトリでホストされているライブラリの内の一つ、nolibcのprintf()が呼び出されます。Unikraftプロジェクトは外部ライブラリとして、libcの実装の一つであるnewlibをホストしていますが、newlibを外部ライブラリにcloneし、menuconfigで明示的に指定しない場合は、nolibcをリンクしlibcのAPIが使えるようにしています。

その後、一般的なOSならばシステムコールの発行を通じて呼び出される機能が、普通の関数呼び出しによってアクセスされます。そして、x86_64とKVMで呼び出しが分岐するのは、vprintf()から呼び出されるukplat_coutk()です。それぞれ、platディレクトリ以下のプラットフォーム特有の実装が呼び出されています。分岐した先では、KVMの方ではinb命令が発行されていたり、Xenの方ではhypercallが発行されていたりします。

以上のように、Unikraftではplatディレクトリ以下にプラットフォーム依存の処理がまとめられており、ビルド時に対象プラットフォームに該当するオブジェクトファイルとリンクされ、プラットフォーム依存とそうでないコードがうまく分離されています。libcはデフォルトでは、Unikraftメインrepoのlibs以下でホストされているnolibcがリンクされることも分かりました。

次に、外部ライブラリとどのように連携し、ライブラリの拡張性を担保する実装になっているのかを調べるために、httpreply appでsocketを呼び出した際のスタックトレースを見てみます。環境はx86_64/KVMです。

#0  ukplat_lcpu_restore_irqf (flags=flags@entry=0)
    at unikraft/plat/kvm/x86/lcpu.c:61
#1  0x000000000012647d in uk_waitq_wake_up (wq=0x15e190 <lock_tcpip_core+16>)
    at unikraft/lib/uksched/include/uk/wait.h:139
#2  uk_mutex_unlock (m=m@entry=0x15e180 <lock_tcpip_core>)
    at unikraft/lib/uklock/include/uk/mutex.h:124
#3  sys_mutex_unlock (mtx=mtx@entry=0x15e180 <lock_tcpip_core>)
    at libs/lwip/mutex.c:74
#4  0x000000000013a266 in tcpip_send_msg_wait_sem (
    fn=0x138110 <lwip_netconn_do_newconn>, apimsg=apimsg@entry=0x1dfee0,
    sem=sem@entry=0x7fe3028)
    at apps/httpreply/build/liblwip/origin/lwip-2.1.2/src/api/tcpip.c:443
#5  0x0000000000135d61 in netconn_apimsg (apimsg=0x1dfee0, fn=<optimized out>)
    at apps/httpreply/build/liblwip/origin/lwip-2.1.2/src/api/api_lib.c:131
#6  netconn_new_with_proto_and_callback (t=t@entry=NETCONN_TCP,
    proto=proto@entry=0 '\000', callback=callback@entry=0x13b1a0 <event_callback>)
    at apps/httpreply/build/liblwip/origin/lwip-2.1.2/src/api/api_lib.c:161
#7  0x000000000013cb81 in lwip_socket (domain=domain@entry=2, type=type@entry=1,
    protocol=protocol@entry=0)
    at apps/httpreply/build/liblwip/origin/lwip-2.1.2/src/api/sockets.c:1716
#8  0x000000000012789c in socket (domain=domain@entry=2, type=type@entry=1,
    protocol=protocol@entry=0) at libs/lwip/sockets.c:335
#9  0x000000000014448e in main (argc=<optimized out>, argv=<optimized out>) at apps/httpreply/main.c:65

socket()の呼び出しを追ってみると、大部分の処理はUnikraftプロジェクトの外部からダウンロードされてきたオープンソースのlwipの実装が呼び出されていることが分かります。ただし、socket()の呼び出しインターフェースや、sys_mutex_unlock()は、Unikraftプロジェクトでホストされているunikraft/libs/lwipリポジトリにおけるglue codeによる実装が呼び出されています。

また、uk_mutex_unlock()といったロック機構のような、Unikraft 特有のOS機能は、Unikraftメインレポジトリの実装が呼ばれています。

このように、lwipのような外部の既存ライブラリを利用する場合は、プラットフォーム依存やUnikraft OS依存になりそうな部分の実装のみUnikraftプロジェクトがホストし、Unikraftプロジェクト内における、unikraft/libs/lwipのレポジトリでは、プロジェクト外の既存実装を利用するためのglue codeがホストされることが分かります。

さて、lwipのようにUnikraftプロジェクトのリポジトリで公式にサポートしている機能は労力なしに使えますが、自分のプロジェクトのappがサポート外の機能を使いたい場合は、libsディレクトリ以下に置く外部ライブラリを自力でportingしなくてはなりません。

先程述べたように、Unikraftは、デベロッパーに対してこの外部ライブラリへのコントリビュートを期待しているのだと思われますが、Unikraftプロジェクト外の既存のライブラリをダウンロードしてきて、Unikraftのappから使えるようにするには、kconfigを利用したUnikraftのビルドシステムに乗せなくてはなりません。

以下、Unikraftプロジェクトのunikraft/libs/lwipレポジトリを参考に、Unikraft外の既存ライブラリをUnikraftのライブラリとして使用するのに必要な作業を見てみます。既存ライブラリのUnikraftライブラリ化については、NECによるスライドXen Projectのページを参考にしています。以下にビルドシステムの概念図を示します。

Unikraftのビルドシステム概念図 (from NEC’s slide)

Unikraftのライブラリのトップディレクトリには、主として以下の二つのファイルが必要です。

  • Config.uk
  • Makefile.uk

Config.ukは、menuconfigにおける設定オプションの追加に必要であり、KConfigのシンタックスで記述されます。unikraft/libs/lwipリポジトリにおけるConfig.ukを見て分かる通り、

menuconfig LIBMYLIB
	bool "ukmylib: lib description"
	# libraries are off as default
	default n
	# dependencies
	select LIBNOLIBC if !HAVE_LIBC

if LIBMYLIB
	# list of configuration parameters
	config SETTING_ENTRY
		[type] "[description]"
		default [value]
		select LIBOTHER
endif

といったようなシンタックスで記述されており、menuconfigからライブラリの選択や設定、依存ライブラリの記述などを行うことができるようにします。

Makefile.ukは、ライブラリのビルドに必要な処理を全て記述するファイルであり、Makefileのシンタックスで記述されます。unikraft/libs/lwipリポジトリを見て分かる通り、

  • ライブラリの登録 (ライブラリリストへの追加と各種変数の定義)
  • 外部ライブラリのソースのダウンロードURLとパッチの指定
  • ライブラリのインクルードパスの指定
  • ソースコードファイルの指定

などを記述します(ビルド済みのバイナリをリンク対象に指定することもできます)。

以上の2つのファイル以外には、パッチファイルや、Unikraft OS依存の部分をglue codeとしてホストする必要があります。ホストするglue codeの各ファイルは、Makefile.ukのソースコードファイルの指定によってビルドシステムに登録されます。

以上で見たように、Unikraftのプロジェクト外のライブラリをUnikraftの外部ライブラリに追加するには、Unikraft OS依存の部分のglueコードの記述、ダウンロードした元々のライブラリへのパッチの作成が主に労力を必要とする点です。Unikraft内部への理解があるデベロッパーにとっては、比較的スムーズにportingを行うことができる仕組みであるように思われます。

UKL (Unikernel Linux)

リサーチをしている間に、Unikraftと同じような目標の達成を目指すプロジェクトを見つけました。2019年5月に Unikernels: The Next Stage of Linux’s Dominance [4] という論文において、UKL(Unikernel Linux)という、既存のLinux kernelのソースコードをそのままライブラリとして利用してUnikernelをビルドする仕組みを作ろうとするプロジェクトです。

UKLにおいても、Unikernelの利用者の増加には、既存アプリケーションのportingを容易にすることが重要であるとし、手段として、ライブラリの充実のために既存のLinux kernelのコードベースを利用することを提案しています。

つまり、以下をUKLプロジェクトのゴールとしています。

  1. ほとんどのappやuser libraryは変更なしでunikernel上で動作すべきである。つまり、gcc targetを変更するだけで対象appをunikernelのイメージとしてビルドできるようにする。
  2. ring transitionによるオーバーヘッドを避ける。つまりkernelの機能はシステムコールによるトラップではなく、単なる関数の呼び出しによりアクセス可能にする。
  3. kernelレイヤーとappレイヤーにまたがった最適化を可能にする
  4. upstreamに受け入れられるように、Linuxのソースコードへの変更は最小にするべきである

以上を達成できるかの検討のために、[4]ではプロトタイプが実装されており、プロトタイプにおけるLinux kernelとその周囲への変更点は、以下に挙げる通りです。

  • UKLとしてコンパイルするか選択するkernel configuration optionを追加
  • app codeを呼び出すとき、プロセスを生成せず、直接appのmainを関数呼び出しできるように変更
  • syscall発行ではなく、直接の関数呼び出しにするためのUKL libraryを追加
  • syscall発行の代わりに、上記のUKL libraryを利用するようにglibcを変更
  • kernelのリンカスクリプトを変更
  • appを呼び出す前の初期化コードを少し追加
  • kernelのリンキングを変更し、app / glibc / UKL libraryをリンクして単一のバイナリを生成するようにする

以上の変更により、現時点ではまだLinux kernel全体をイメージに含めてしまっている点と、その他細かい修正点を除き、対象となるappのUnikernelをビルドすることができたと記述されています。さらに、少ないコード変更量を根拠に、upstreamへのmergeが可能であると主張しています。

まとめ

2020年2月現在、特定の用途や特定の言語で記述されたアプリケーションをUnikernelとしてビルドするプロジェクトが乱立しているが、既存のアプリケーションをportingするコストの高さを主な理由として普及が進んでいません。

その課題を解決する意図で、UnikraftやUnikernel Linuxといったようなプロジェクトが進んでいます。Unikraftは、拡張が容易なビルドシステムによりライブラリの充実を目指しますが、Unikraft自身や外部ライブラリのバージョンアップに対応するコストが、公式にホストするライブラリの増加と共に増加するのが懸念です。

対してUnikernel Linuxは、Linuxコミュニティに支えられた豊富な既存コードの資産がありますが、Linuxのupstreamにmergeされなかった場合、patchを持続的に保守するコストが生じます。

UnikraftやUnikernel Linuxが理想とする状態を達成できれば、ハイパーバイザを利用した組み込み開発や、クラウド上で動作するアプリケーションにUnikernelが導入される可能性は十分にあると考えられます。

参考文献 (論文)

[1] Anil Madhavapeddy et al. (2013) “Unikernels: Library Operating Systems for the Cloud” ACM SIGARCH Computer Architecture News

[2] Andr´e Hergenhan and Gernot Heiser. (2008) “Operating Systems Technology for Converged ECUs”

[3] Lee Pike et al. (2015) “Securing the Automobile: a Comprehensive Approach” Galois Inc. Technical Report

[4] Ali Raza et al. (2019) “Unikernels: The Next Stage of Linux’s Dominance” HotOS ’19: Proceedings of the Workshop on Hot Topics in Operating Systems

About Author

IshikawaTakahiro

Leave a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください

Recent Comments

Social Media