LoginSignup
6
3

More than 5 years have passed since last update.

MirageOS Unikernel アプリケーションの中身を理解する(その1)

Last updated at Posted at 2019-04-10

概要

本記事は、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 デバイスが存在する環境であれば好きなネットワークを組んでもらって構いません。
network_bridge.png

bridgeと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 で提供されているネットワークのアプリケーションコードを利用します。

networkディレクトリ
$ cd ./mirage-skeleton/device-usage/network/
$ ls
config.ml  unikernel.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
config.ml
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 モジュールについてはここにコードがあります。

unikernel.ml
(* 変更前 *)
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-hvtnetwork.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と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.mlunikernel.ml の2ファイルしかディレクトリには存在していませんでしたが、make コマンド実行後はかなりファイルが増えています。

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.mlunikernel.ml 以外のファイルは mirage configure -t hvt を実行すると生成されるものです。実は config.ml の内容をベースにそれらのファイルが自動生成されます。特に、main.ml はOSで言うところのkernel部分のソースコードに相当します。

MakefileMakefile.solo5-hvtmirage configure -t hvt 実行により生成されます。前者は MirageOS Unikernel アプリケーションバイナリ、後者は仮想マシンレイヤの Solo5-hvt バイナリを生成するための Makefile です。

最後に make を実行しますが、そのときの出力ログを眺めるとどんな感じでバイナリが生成されるか分かります。
(下記は mirage build -vの出力結果の一部です。make は単に mirage buildを実行するだけなのですが、それだと出力結果が不十分なので -v を足して verbose モードにしています)

mirage build -vの出力内容
...
# 各.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

ざっくりと何をしているかというと、

  1. 各.mlファイルのオブジェクトファイル生成
  2. 1.で生成したオブジェクトファイルからUnikernelの基本となる部分(Xen、KVMなどプラットフォームに依存しない実行部分)のオブジェクトファイル生成
  3. 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.mlunikernel.ml の2つです。一般的に、MirageOS Unikernel アプリケーションをコンパイルするのに必要なのは、

  1. アプリケーション実行内容がOCaml言語で書かれた、幾つかの ml ファイル
  2. config.ml ファイル

です。

unikernel.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

上から順に説明していくと、

unikernel.mlの1行目
 1: open Lwt.Infix

1行目は、関数を順次実行させるために使われる演算子(>>=)を利用するためにLwt.Infixというモジュールを読み込んでいます。この演算子は11行目のS.TCPV4.read flow >>= functionにて使われています。

unikernel.mlの3,21行目
 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 レイヤに関わるネットワーク機能が定義されています。

unikernel.mlの5行目
 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関数の中で何が行われているのかを見ていきます。

unikernel.mlの6行目
 6:    let port = Key_gen.port () in

6行目では、config.mlの5行目Key.(create "port" Arg.(opt int 8080 doc))によって作成された変数値を格納する領域から、本ネットワークサービスプログラムがListenするためのport番号を取得し、変数portへ格納しています。なお、変数portlet ... in によって定義されているため、start関数のコードブロック内でしか参照することができません。

unikernel.mlの7〜17行目
 7:     S.listen_tcpv4 s ~port (fun flow ->
        ...
17:     );

7行目から17行目は、listen_tcpv4関数によってTCP/IP接続があったときのコールバック関数を登録しています。具体的には、7行目のlisten_tcpv4関数への引数 sport によって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言語で記述します。

config.ml
 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行目から順に見ていきます。

config.mlの1行目
 1: open Mirage

1行目は、MirageOS Unikernel アプリケーションを記述するために必要な各種モジュール、関数、変数が定義されたMirageモジュールを読み込むためのものです。MirageOS Unikernel アプリケーションコードには必須の文です。

config.mlの3〜5行目
 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行目を思い出してみましょう。

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()を呼ぶことで取り出すことが出来ます。

これによりこのプログラムはオプション名 --portvの引数をとることが出来るようになり、unikernel.ml の6行目にある変数 port に代入されることになります。もし省略された場合には自動的に8080が代入されます。

config.mlの7行目
 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のソースコードにあります。

config.mlの9行目
 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行目です!

config.mlの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 アプリケーションの中身を少しだけ理解できるようになったかと思います。

とはいえ「サンプルアプリを改変してみる」や「一から自分でアプリを実装してみる」はまだ難しいと思いますので、それらのお助けになるような記事を増やしていきたいと思います。

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3