概要
本記事は、MirageOS Unikernel アプリケーションを自分で書くための第一歩です。まずは例題アプリケーションを題材にして、仕組みの理解が出来ることを目的としています。
MirageOS Unikernel アプリケーションの記述言語は OCaml です。OCaml は関数型言語のためこれまで手続き型言語しか利用してこなかった方には苦痛ではあります。しかし一度覚えてみると世界が広がるので、これを機に頑張ってみるのもいいかもしれません。
「OCaml なんぞサッパリ分からん」という方には、Real World OCaml というオライリー本の Web 版を参考にすると良いかと思います。Real World OCaml は OCaml のバイブル的な書籍で、若干癖があるものの Web 版を無料で利用できます。また、日本語のコミュニティサイトも参考になります。
以降は、こちらを参考に MirageOS Unikernel 環境をインストールし、mirage-skeleton をすでに git clone
してあることを想定します。
事前準備
今回はネットワークアプリケーションを利用するため、bridge と tap を作成しておきます。これにより、ホストOS(Linux) から MirageOS Unikernel ネットワークアプリケーションへ通信できるようにします。MirageOS Unikernel ではネットワーク通信に tap デバイスを利用するため、利用可能な tap デバイスが存在する環境であれば好きなネットワークを組んでもらって構いません。
$ sudo brctl addbr mirage_br
$ sudo ip link set mirage_br up
$ sudo ip tuntap add tap0 mode tap
$ sudo ip link set dev tap0 up
$ sudo brctl addif mirage_br tap0
$ sudo ip addr add 192.168.100.1/24 dev mirage_br
$ ip addr show
...
4: mirage_br: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default qlen 1000
link/ether 3e:6b:2d:7d:d5:77 brd ff:ff:ff:ff:ff:ff
inet 192.168.100.1/24 scope global mirage_br
valid_lft forever preferred_lft forever
5: tap0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel master br0 state DOWN group default qlen 1000
link/ether 3e:6b:2d:7d:d5:77 brd ff:ff:ff:ff:ff:ff
...
mirage_br
tap0
ともにステータスがUPとなり、mirage_br
にはIPアドレス192.168.100.1が付与されました。(tap0にはIPアドレスが必要ないため、ここでは付与しません)
お題のアプリケーション
mirage-skeleton で提供されているネットワークのアプリケーションコードを利用します。
$ cd ./mirage-skeleton/device-usage/network/
$ ls
config.ml unikernel.ml
open Lwt.Infix
module Main (S: Mirage_stack_lwt.V4) = struct
let start s =
let port = Key_gen.port () in
S.listen_tcpv4 s ~port (fun flow ->
let dst, dst_port = S.TCPV4.dst flow in
Logs.info (fun f -> f "new tcp connection from IP %s on port %d"
(Ipaddr.V4.to_string dst) dst_port);
S.TCPV4.read flow >>= function
| Ok `Eof -> Logs.info (fun f -> f "Closing connection!"); Lwt.return_unit
| Error e -> Logs.warn (fun f -> f "Error reading data from established connection: %a" S.TCPV4.pp_error e); Lwt.return_unit
| Ok (`Data b) ->
Logs.debug (fun f -> f "read: %d bytes:\n%s" (Cstruct.len b) (Cstruct.to_string b));
S.TCPV4.close flow
);
S.listen s
end
open Mirage
let port =
let doc = Key.Arg.info ~doc:"The TCP port on which to listen for incoming connections." ["port"] in
Key.(create "port" Arg.(opt int 8080 doc))
let main = foreign ~keys:[Key.abstract port] "Unikernel.Main" (stackv4 @-> job)
let stack = generic_stackv4 default_network
let () =
register "network" [
main $ stack
]
このプログラムの機能は、サービスプログラムとしてネットワークから流れてきた文字列データをコンソール上に表示するだけのものです。
しかしながら...そのままだと文字列データが表示されないため、下記のようにunikernel.mlファイルを修正します。具体的には、15行目にて Logs.debug
関数をコールしている箇所を Logs.info
関数に変更します。Logs
モジュールについてはここにコードがあります。
(* 変更前 *)
15: Logs.debug (fun f -> f "read: %d bytes:\n%s" (Cstruct.len b) (Cstruct.to_string b));
(* 変更後 *)
15: Logs.info (fun f -> f "read: %d bytes:\n%s" (Cstruct.len b) (Cstruct.to_string b));
コンパイル
早速コンパイルします。
# 固定IPアドレスの場合
$ mirage configure -t hvt --ipv4=192.168.100.10/24
$ make depend
$ make
# DHCPの場合
$ mirage configure -t hvt --dhcp=true
$ make depend
$ make
最初のmirage configure
コマンドにて、MirageOS Unikernel アプリケーションへ付与するIPアドレスを設定します。固定IP(IPV4)とするには --ipv4
オプションを使います。もしtapデバイスがDHCPサービスのあるネットワークにつながっている場合は、動的アドレスとすることも可能です。この場合は、--dhcp
オプションを使います。
mirage configure
コマンドの詳細は mirage configure --help
で参照可能です。例えば、デフォルトゲートウェイアドレスの設定方法などもここで確認できます。
$ mirage configure --help
mirage-configure(1) Mirage Manual mirage-configure(1)
...
...
--ipv4=IPV4 (absent=10.0.0.2/24)
The network of the unikernel specified as an IP address and
netmask, e.g. 192.168.0.1/16 .
--ipv4-gateway=IPV4-GATEWAY (absent=10.0.0.1)
The gateway of the unikernel.
...
プログラムバイナリができたので、動かしてみます。必要なのは、solo5-hvt
と network.hvt
です。仮想CPU上にてMirageOS Unikernel アプリケーションを動作させるための仮想マシンレイヤを提供するプログラムがsolo5-hvt
で、MirageOS Unikernel アプリケーションバイナリがnetwork.hvt
です。
アプリケーション実行
アプリケーションの実行はそんなに難しくありません。事前準備のところで作成していた tap0
をオプションに指定して実行します。
# 実行
$ sudo ./solo5-hvt --net=tap0 ./network.hvt
| ___|
__| _ \ | _ \ __ \
\__ \ ( | | ( | ) |
____/\___/ _|\___/____/
Solo5: Memory map: 512 MB addressable:
Solo5: unused @ (0x0 - 0xfffff)
Solo5: text @ (0x100000 - 0x239fff)
Solo5: rodata @ (0x23a000 - 0x269fff)
Solo5: data @ (0x26a000 - 0x36afff)
Solo5: heap >= 0x36b000 < stack < 0x20000000
2019-01-12 14:03:02 -00:00: INF [netif] Plugging into 0 with mac b6:a0:fe:12:4c:00
2019-01-12 14:03:02 -00:00: INF [ethif] Connected Ethernet interface b6:a0:fe:12:4c:00
2019-01-12 14:03:02 -00:00: INF [arpv4] Connected arpv4 device on b6:a0:fe:12:4c:00
2019-01-12 14:03:02 -00:00: INF [udp] UDP interface connected on 192.168.100.10
2019-01-12 14:03:02 -00:00: INF [tcpip-stack-direct] stack assembled: mac=b6:a0:fe:12:4c:00,ip=192.168.100.10
違うコンソールウィンドウを開き、ping
コマンドによる通信確認とnc
コマンドによるデータ送信を行います。
# 通信確認
$ ping 192.168.100.10
PING 192.168.100.10 (192.168.100.10) 56(84) bytes of data.
64 bytes from 192.168.100.10: icmp_seq=1 ttl=38 time=1.42 ms
64 bytes from 192.168.100.10: icmp_seq=2 ttl=38 time=1.02 ms
64 bytes from 192.168.100.10: icmp_seq=3 ttl=38 time=1.04 ms
^C
--- 192.168.100.10 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 1.029/1.166/1.424/0.186 ms
# 文字列"hogehoge"の送信
$ echo -en "hogehoge" | nc 192.168.100.10 8080
すると、MirageOS Unikernel アプリケーション側のコンソールに文字列"hogehoge"が表示されます。
2019-01-12 14:21:38 -00:00: INF [application] new tcp connection from IP 192.168.100.1 on port 59798
2019-01-12 14:21:38 -00:00: INF [application] read: 8 bytes:
hogehoge
今回のような自動的に終了しないサービス形式の MirageOS Unikernel アプリケーションを停止させるには、ctrl+c キーを使います。
^Csolo5-hvt: Exiting on signal 2
以上により、MirageOS Unikernelを使ったネットワークアプリケーションを試すことができました!
コンパイルの仕組み
最初は config.ml
と unikernel.ml
の2ファイルしかディレクトリには存在していませんでしたが、make
コマンド実行後はかなりファイルが増えています。
$ ls
_build config.ml main.ml Makefile.solo5-hvt myocamlbuild.ml solo5-hvt
_build-solo5-hvt key_gen.ml Makefile mirage-unikernel-network-hvt.opam network.hvt unikernel.ml
まず、OCamlソースコードである拡張子 .ml
を持つファイルが5つになりました。
- config.ml
- unikernel.ml
- main.ml
- key_gen.ml
- myocamlbuild.ml (0byteなのでコンパイルには無影響)
config.ml
と unikernel.ml
以外のファイルは mirage configure -t hvt
を実行すると生成されるものです。実は config.ml
の内容をベースにそれらのファイルが自動生成されます。特に、main.ml
はOSで言うところのkernel部分のソースコードに相当します。
Makefile
と Makefile.solo5-hvt
も mirage configure -t hvt
実行により生成されます。前者は MirageOS Unikernel アプリケーションバイナリ、後者は仮想マシンレイヤの Solo5-hvt バイナリを生成するための Makefile です。
最後に make
を実行しますが、そのときの出力ログを眺めるとどんな感じでバイナリが生成されるか分かります。
(下記は mirage build -v
の出力結果の一部です。make
は単に mirage build
を実行するだけなのですが、それだと出力結果が不十分なので -v
を足して verbose モードにしています)
...
# 各.mlのオブジェクトファイル生成
ocamlfind ocamlopt -c -g -g -bin-annot -safe-string -principal -strict-sequence -package tcpip.udp -package tcpip.tcp -package tcpip.stack-direct -package tcpip.ipv4 -package tcpip.icmpv4 -package tcpip.ethif -package tcpip.arpv4 -package tcpip -package mirage-types-lwt -package mirage-types -package mirage-solo5 -package mirage-runtime -package mirage-random-stdlib -package mirage-net-solo5 -package mirage-logs -package mirage-clock-freestanding -package mirage-bootvar-solo5 -package lwt -package functoria-runtime -predicates mirage_solo5 -w A-4-41-42-44 -color always -o key_gen.cmx key_gen.ml
ocamlfind ocamlopt -c -g -g -bin-annot -safe-string -principal -strict-sequence -package tcpip.udp -package tcpip.tcp -package tcpip.stack-direct -package tcpip.ipv4 -package tcpip.icmpv4 -package tcpip.ethif -package tcpip.arpv4 -package tcpip -package mirage-types-lwt -package mirage-types -package mirage-solo5 -package mirage-runtime -package mirage-random-stdlib -package mirage-net-solo5 -package mirage-logs -package mirage-clock-freestanding -package mirage-bootvar-solo5 -package lwt -package functoria-runtime -predicates mirage_solo5 -w A-4-41-42-44 -color always -o unikernel.cmx unikernel.ml
ocamlfind ocamlopt -c -g -g -bin-annot -safe-string -principal -strict-sequence -package tcpip.udp -package tcpip.tcp -package tcpip.stack-direct -package tcpip.ipv4 -package tcpip.icmpv4 -package tcpip.ethif -package tcpip.arpv4 -package tcpip -package mirage-types-lwt -package mirage-types -package mirage-solo5 -package mirage-runtime -package mirage-random-stdlib -package mirage-net-solo5 -package mirage-logs -package mirage-clock-freestanding -package mirage-bootvar-solo5 -package lwt -package functoria-runtime -predicates mirage_solo5 -w A-4-41-42-44 -color always -o main.cmx main.ml
# Unikernelのオブジェクトファイル(プラットフォームに依存しない部分だけ)生成
ocamlfind ocamlopt -g -dontlink unix -dontlink str -dontlink num -dontlink threads -linkpkg -output-obj -package tcpip.udp -package tcpip.tcp -package tcpip.stack-direct -package tcpip.ipv4 -package tcpip.icmpv4 -package tcpip.ethif -package tcpip.arpv4 -package tcpip -package mirage-types-lwt -package mirage-types -package mirage-solo5 -package mirage-runtime -package mirage-random-stdlib -package mirage-net-solo5 -package mirage-logs -package mirage-clock-freestanding -package mirage-bootvar-solo5 -package lwt -package functoria-runtime -predicates mirage_solo5 key_gen.cmx unikernel.cmx main.cmx -o main.native.o
# 他のライブラリ(プラットフォーム依存オブジェクトファイルを含む)とのリンク
mirage: [INFO] using ld as ld (pkg-config solo5-bindings-hvt --variable=ld)
mirage: [INFO] linking with ld -nostdlib -z max-page-size=0x1000 -static -T
/home/imada/.opam/4.07.0/lib/pkgconfig/../../lib/solo5-bindings-hvt/solo5_hvt.lds
/home/imada/.opam/4.07.0/lib/pkgconfig/../../lib/solo5-bindings-hvt/solo5_hvt.o
_build/main.native.o
-L/home/imada/.opam/4.07.0/lib/mirage-entropy
-lmirage-entropy_stubs+mirage-freestanding
-L/home/imada/.opam/4.07.0/lib/pkgconfig/../../lib/ocaml-freestanding
/home/imada/.opam/4.07.0/share/pkgconfig/../../lib/mirage-solo5/libmirage-solo5_bindings.a
-lasmrun -lnolibc -lopenlibm
/usr/lib/gcc/aarch64-linux-gnu/7/libgcc.a -o
network.hvt
# solo5-hvtバイナリの生成
mkdir -p _build-solo5-hvt
cc -Wall -Werror -std=c99 -O2 -g -DHVT_MODULE_NET -c /home/imada/.opam/4.07.0/lib/solo5-bindings-hvt/src/hvt_core.c -o _build-solo5-hvt/hvt_core.o
cc -Wall -Werror -std=c99 -O2 -g -DHVT_MODULE_NET -c /home/imada/.opam/4.07.0/lib/solo5-bindings-hvt/src/hvt_elf.c -o _build-solo5-hvt/hvt_elf.o
cc -Wall -Werror -std=c99 -O2 -g -DHVT_MODULE_NET -c /home/imada/.opam/4.07.0/lib/solo5-bindings-hvt/src/hvt_main.c -o _build-solo5-hvt/hvt_main.o
cc -Wall -Werror -std=c99 -O2 -g -DHVT_MODULE_NET -c /home/imada/.opam/4.07.0/lib/solo5-bindings-hvt/src/hvt_module_net.c -o _build-solo5-hvt/hvt_module_net.o
cc -Wall -Werror -std=c99 -O2 -g -DHVT_MODULE_NET -c /home/imada/.opam/4.07.0/lib/solo5-bindings-hvt/src/hvt_kvm.c -o _build-solo5-hvt/hvt_kvm.o
cc -Wall -Werror -std=c99 -O2 -g -DHVT_MODULE_NET -c /home/imada/.opam/4.07.0/lib/solo5-bindings-hvt/src/hvt_kvm_aarch64.c -o _build-solo5-hvt/hvt_kvm_aarch64.o
cc -Wall -Werror -std=c99 -O2 -g -DHVT_MODULE_NET -c /home/imada/.opam/4.07.0/lib/solo5-bindings-hvt/src/hvt_cpu_aarch64.c -o _build-solo5-hvt/hvt_cpu_aarch64.o
cc -o solo5-hvt _build-solo5-hvt/hvt_core.o _build-solo5-hvt/hvt_elf.o _build-solo5-hvt/hvt_main.o _build-solo5-hvt/hvt_module_net.o _build-solo5-hvt/hvt_kvm.o _build-solo5-hvt/hvt_kvm_aarch64.o _build-solo5-hvt/hvt_cpu_aarch64.o
ざっくりと何をしているかというと、
- 各.mlファイルのオブジェクトファイル生成
- 1.で生成したオブジェクトファイルからUnikernelの基本となる部分(Xen、KVMなどプラットフォームに依存しない実行部分)のオブジェクトファイル生成
- 2.で生成したオブジェクトファイルと、その他必要なライブラリを
ld
コマンドでリンクさせて最終バイナリを生成
となります。
_build
と _build-solo5-hvt
は、make
実行によって生成される中間オブジェクトファイルが保持するためのディレクトリです。
最後に、mirage-unikernel-network-hvt.opam
は本プログラムで生成されたバイナリをopamパッケージ化するときに使われるファイルです。つまり自作プログラムをパッケージ化するためのパッケージ構成ファイルが自動的に生成されるので、自分で書かなくてよいのです。(今回は詳細を割愛します)
ソースコードを深追いする
次は、自分で MirageOS Unikernel アプリケーションを実装するためのはじめの一歩として、コードの中身を見ていきます。
上記の作業でファイルがたくさん生成されているため、一旦ディレクトリを綺麗にします。これにより、git clone
した直後の状態に戻ります。
$ make clean
$ ls
config.ml unikernel.ml
元々置いてあるファイルは、config.ml
と unikernel.ml
の2つです。一般的に、MirageOS Unikernel アプリケーションをコンパイルするのに必要なのは、
- アプリケーション実行内容がOCaml言語で書かれた、幾つかの ml ファイル
- config.ml ファイル
です。
unikernel.ml の中身
まずは、アプリケーション実行内容が書かれたunikernel.mlファイルを見ていきます。
1: open Lwt.Infix
2:
3: module Main (S: Mirage_stack_lwt.V4) = struct
4:
5: let start s =
6: let port = Key_gen.port () in
7: S.listen_tcpv4 s ~port (fun flow ->
8: let dst, dst_port = S.TCPV4.dst flow in
9: Logs.info (fun f -> f "new tcp connection from IP %s on port %d"
10: (Ipaddr.V4.to_string dst) dst_port);
11: S.TCPV4.read flow >>= function
12: | Ok `Eof -> Logs.info (fun f -> f "Closing connection!"); Lwt.return_unit
13: | Error e -> Logs.warn (fun f -> f "Error reading data from established connection: %a" S.TCPV4.pp_error e); Lwt.return_unit
14: | Ok (`Data b) ->
15: Logs.debug (fun f -> f "read: %d bytes:\n%s" (Cstruct.len b) (Cstruct.to_string b));
16: S.TCPV4.close flow
17: );
18:
19: S.listen s
20:
21: end
上から順に説明していくと、
1: open Lwt.Infix
1行目は、関数を順次実行させるために使われる演算子(>>=)を利用するためにLwt.Infix
というモジュールを読み込んでいます。この演算子は11行目のS.TCPV4.read flow >>= function
にて使われています。
3: module Main (S: Mirage_stack_lwt.V4) = struct
...
21: end
3行目と21行目のコードブロックによって、Main という名前のモジュールを定義しています。モジュール名の1文字目は、英数大文字で始めなければいけません。また、このモジュール Main は引数として別のモジュール(モジュール名:S)をとります。このような別のモジュール引数によってパラメタ化されるようなモジュールを OCaml ではファンクタ(functor)と呼びます。
モジュール S の型は Mirage_stack_lwt.V4 であり、これは IPV4 と Ethernet レイヤに関わるネットワーク機能が定義されています。
5: let start s =
...
...
5行目から始まるのは start
関数 の定義です。この関数のコードブロックは19行目の listen
関数コール までとなります。MirageOS Unikernel アプリケーションにおいては、「アプリケーションコードのエントリポイントとなる関数は、あるモジュールに定義されているstart
関数でないといけない」 と定められており、本プログラムではここに定義されています。
また、start
関数の引数として、Main
ファンクタの引数として指定されていたMirage_stack_lwt.V4
のインスタンスが格納されている変数s
が引数として設定されています。この変数s
は、Ethernetデバイスと各種ネットワークスタックの集合体に相当します。加えてEthernetデバイスとしては、アプリケーション実行時に指定するtapデバイス(実行例ではtap0)にマッピングされています。主な使い方としては、「パケット受信時のコールバック関数を登録するための関数や、listenするときの関数の引数として変数s
をとる」となります。
では、このstart関数の中で何が行われているのかを見ていきます。
6: let port = Key_gen.port () in
6行目では、config.mlの5行目Key.(create "port" Arg.(opt int 8080 doc))
によって作成された変数値を格納する領域から、本ネットワークサービスプログラムがListenするためのport番号を取得し、変数port
へ格納しています。なお、変数port
は let ... in
によって定義されているため、start関数のコードブロック内でしか参照することができません。
7: S.listen_tcpv4 s ~port (fun flow ->
...
17: );
7行目から17行目は、listen_tcpv4関数によってTCP/IP接続があったときのコールバック関数を登録しています。具体的には、7行目のlisten_tcpv4関数への引数 s
と port
によってlistenすべきMirage_stack_lwt.V4
のインスタンス(ここではtap0へのマッピングを有する)とポート番号を指定しています。さらに、その先の17行目まで続く (fun flow -> ...)
がコールバック関数の定義です。このコールバック関数定義の大まかな解釈は、「TCPのコネクションが確立されたあとにクライアントから送られてくるTCPのペイロードとその他のメタデータが flow
としてコールバック関数の引数に与えられ、 fun flow ->
の右側に定義された処理がデータ到着毎に実行される」というものです。このとき、コールバック関数のスコープは、
8: let dst, dst_port = S.TCPV4.dst flow in
...
16: S.TCPV4.close flow
ということになります。
8: let dst, dst_port = S.TCPV4.dst flow in
9: Logs.info (fun f -> f "new tcp connection from IP %s on port %d"
10: (Ipaddr.V4.to_string dst) dst_port);
具体的に何をやっているかというと、引数であるflow
からTCPパケットの宛先IPアドレスとポート番号を取得し、コンソールに出力しています。(8-10行目)
11: S.TCPV4.read flow >>= function
12: | Ok `Eof -> Logs.info (fun f -> f "Closing connection!"); Lwt.return_unit
13: | Error e -> Logs.warn (fun f -> f "Error reading data from established connection: %a" S.TCPV4.pp_error e); Lwt.return_unit
14: | Ok (`Data b) ->
15: Logs.debug (fun f -> f "read: %d bytes:\n%s" (Cstruct.len b) (Cstruct.to_string b));
16: S.TCPV4.close flow
さらに、S.TCPV4.read
関数によってクライアントから送信されたTCPペイロードのreadを試み、その結果に応じてその後の処理を変えています。結果による場合分けは、 1)クライアントからのConnection close、2)エラー発生、3)ペイロード取得完了 の3つです。正常パスである3)のケースでは、S.TCPV4.read
の戻り値が Ok (`Data b)
となってペイロードは変数 b
(型はCstruct)に格納されます。ペイロードが取得できたので、その長さとペイロード内容をコンソールに出力し、TCPコネクションをcloseします。(11-16行目)
14行目に記載の戻り値に利用する変数名はb
である必要はありません。x
とかでも構いません。その場合は15行目にて (Cstruct.len x) (Cstruct.to_string x)
と記述し、x
を参照する必要があります。
config.mlの中身
続けて、config.mlファイルを見ていきます。config.mlファイルはアプリケーションをコンパイルするために必要な情報を記述したりするためのもので、必須のファイルです。このファイルは拡張子が ml であり、こちらもOCaml言語で記述します。
1: open Mirage
2:
3: let port =
4: let doc = Key.Arg.info ~doc:"The TCP port on which to listen for incoming connections." ["port"] in
5: Key.(create "port" Arg.(opt int 8080 doc))
6:
7: let main = foreign ~keys:[Key.abstract port] "Unikernel.Main" (stackv4 @-> job)
8:
9: let stack = generic_stackv4 default_network
10:
11: let () =
12: register "network" [
13: main $ stack
14: ]
こちらも1行目から順に見ていきます。
1: open Mirage
1行目は、MirageOS Unikernel アプリケーションを記述するために必要な各種モジュール、関数、変数が定義されたMirageモジュールを読み込むためのものです。MirageOS Unikernel アプリケーションコードには必須の文です。
3: let port =
4: let doc = Key.Arg.info ~doc:"The TCP port on which to listen for incoming connections." ["port"] in
5: Key.(create "port" Arg.(opt int 8080 doc))
3-5行目では、Key を利用してサーバプログラムで listen するためのポート番号を構成しています。具体的には、4行目にてコマンドオプション引数名 port
とその説明文("The TCP port ... connections.")をからなる変数 doc
を定義し、その doc
とポート番号のデフォルト値(int) 8080
の情報を持つ Key
を5行目で生成し、その生成結果を3行目の変数port
に格納しています。5行目の Key.create
の引数として渡されている "port"
は、プログラム中(本例ではunikernel.ml)にてその情報を参照するときに call するメンバ関数の名前となります。
ちょっとだけ元に戻って unikernel.ml の6行目を思い出してみましょう。
6: let port = Key_gen.port () in
Key_gen.port()
という関数を呼ぶことで生成されるポート番号値を変数 port
に格納していますが、config.mlの5行目にて設定している文字列 "port"
がそのままメンバ関数名となっていることが分かります。基本的に、config.mlにて文字列"XXXX"
を引数としてcreateされたKey情報は、Key_gen.XXXX()
を呼ぶことで取り出すことが出来ます。
これによりこのプログラムはオプション名 --port
vの引数をとることが出来るようになり、unikernel.ml の6行目にある変数 port
に代入されることになります。もし省略された場合には自動的に8080が代入されます。
7: let main = foreign ~keys:[Key.abstract port] "Unikernel.Main" (stackv4 @-> job)
7行目では変数 main
を定義しています。名前は何でもいいので、main
である必要はないので自由に変更可能です。ここでは作成する Unikernel アプリケーションがどのような構成となるかを、foreign関数を用いて記述します。この例では
(1) 6行目で定義されたポート番号情報(Keyモジュールのリスト)
(2) start関数を含むモジュール名(=Unikernel.Main
モジュール)
(3) Unikernel.Main
モジュールの型定義
を引数として、MirageOSアプリケーション構成を定義するために利用される foreign
関数を呼んでいます。
(2) は start
関数を含むモジュールの明示的指定です。Unikernel.Main
モジュールがどこから現れたかというと、unikernl.ml ソースコードファイルです。このファイルには Main
モジュールが定義されていました。そしてこのファイルはコンパイルされると、ソースコードファイル内で定義された変数やクラス、モジュール等を Unikernel
モジュールとして利用することが出来ます。ちなみにコンパイル後のモジュールの命名規則は簡単です。abc.ml をコンパイルすると、拡張子を除いた文字列 "abc" の先頭文字を大文字にした "Abc" がモジュール名となります。
(3) の型定義については、最後は必ず job
になります。今回は Unikernel.Main
モジュールがネットワークデバイスモジュール(Mirage_stack_lwt.V4
)を引数にとるので、その型である stackv4
を左側に書いて @->
でつないでいきます。引数を必要としない場合には単に job
とだけ書けばOKです。なお、どのような型があるかは Mirageモジュールの定義に記載があります。例えば Key-Value ストアデバイスであれば下記の2つです。
kv_ro (Read only, 型定義はここ)
kv_rw (Read/Write, 型定義はここ)
MirageOSアプリケーション構成の定義は、Functoria を使います。Functoria はファンクタを構成するための DSL(Domain Specific Language) で、FunctoriaのREADME.md に少し説明があります。また、使われている foreign
関数の定義はFunctoriaのソースコードにあります。
9: let stack = generic_stackv4 default_network
続いて9行目では generic_stackv4
関数を呼んで、IPV4 ネットワークスタックのインスタンスを生成して変数 stack
に格納しています。これにより、Ethernet プロトコルと IPV4 プロトコル(ARP、DHCP、TCP、UDPなど)の基本的なパケット処理を実施するためのオブジェクトが生成されます。この行をさらに深追いしようとすると情報量がとても多くなってしまうので割愛しますが、
項目 | 説明 |
---|---|
generic_stackv4 | 引数のインスタンスに対してEthernetプロトコルとIPV4プロトコル(ARP、DHCP、TCP、UDPなど)の基本的なパケット処理を設定するための関数 |
default_network | id番号0を有するMirageOS Unikernel内のEthernetデバイスインスタンス(hvt利用時には、solo5-hvt コマンドの引数--net で指定されるtapデバイスにマッピングされる) |
と考えて頂ければいいかと思います。
ちなみに、2019年4月現在では solo5-hvt
コマンドの引数 --net
を1つしかとれません。したがって利用できる tap デバイスは1つだけということになります。
さあ、最後に11-14行目です!
11: let () =
12: register "network" [
13: main $ stack
14: ]
ここの処理では生成するアプリケーションの名前を "network" として、7行目で宣言した 変数 main
および9行目で宣言した変数 stack
を構成要素に OCaml のエントリポイントを定義しています。
書き方としては、まずエントリポイント情報を含む foreign
関数で生成された変数(ここでは main
)を一番左側に記述します。unikernel.ml
にて引数がある場合には、対応する config.ml
の変数(ここでは stack
)を引数の記載順に $
で区切りながら追加します。このあたりの書き方は、mirage-skeleton のWebサーバアプリを参考にするといいかと思います。
ここで設定したアプリケーションの名前は、生成される Unikernel アプリケーションバイナリのファイル名に利用されます。(そういえば、network.hvt
というファイルが生成されていました)
最後に
お題のネットワークアプリケーションの例を通じて、MirageOS Unikernel アプリケーションの中身を少しだけ理解できるようになったかと思います。
とはいえ「サンプルアプリを改変してみる」や「一から自分でアプリを実装してみる」はまだ難しいと思いますので、それらのお助けになるような記事を増やしていきたいと思います。