Doing Nothing in Mirage
It’s Northern Hemisphere summer right now, and in Wisconsin we’re having one of the loveliest ones I can remember. Today the temperature is hovering right at pleasant, there are high clouds blowing across the sky, the breeze is soothing, and birds are singing all over the place. It is not, in short, programming weather. It is sit-outside, read-a-novel, do-nothing weather.
We don’t often let our programs slack off, even when we let ourselves take a peaceful day. I got to wondering (staring off into space, watching the shadows cast by sun-dappled leaves) what the most trivial, do-nothing Mirage project would look like, and how it could be constructed with a minimum of activity and a maximum of understanding.
[] dothraki@iBook:~$ mkdir trivial
[] dothraki@iBook:~$ cd trivial/
[] dothraki@iBook:~/trivial$ ls -alh
total 16K
drwxrwxr-x 2 dothraki dothraki 4.0K Jul 23 13:17 .
drwxr-xr-x 161 dothraki dothraki 12K Jul 23 13:17 ..
[] dothraki@iBook:~/trivial$ mirage configure --xen
[ERROR] No configuration file config.ml found.
You'll need to create one to let Mirage know what to do.
Okay, we’ll have to do at least one thing to make this work. Mirage uses config.ml
to programmatically generate a Makefile
and main.ml
when you invoke mirage --configure
. main.ml
uses instructions from config.ml
to satisfy module types representing driver requirements for your application, then begins running the threads you requested that it run. That all sounds an awful lot like work; maybe we can get away with not asking for anything.
[] dothraki@iBook:~/trivial$ touch config.ml
[] dothraki@iBook:~/trivial$ mirage configure --xen
Mirage Using scanned config file: config.ml
Mirage Processing: /home/dothraki/trivial/config.ml
Mirage => rm -rf /home/dothraki/trivial/_build/config.*
Mirage => cd /home/dothraki/trivial && ocamlbuild -use-ocamlfind -tags annot,bin_annot -pkg mirage config.cmxs
empty Using configuration: /home/dothraki/trivial/config.ml
empty 0 jobs []
empty => ocamlfind printconf path
empty Generating: main.ml
empty Now run 'make depend' to install the package dependencies for this unikernel.
[] dothraki@iBook:~/trivial$ ls
_build config.ml empty.xl log main.ml Makefile
That seems like a great start! Maybe we can trivially achieve our dream of doing nothing.
[] dothraki@iBook:~/trivial$ make depend
opam install mirage-xen --verbose
[NOTE] Package mirage-xen is already installed (current version is 1.1.1).
Resting on our laurels. Excellent. (In keeping with the lazy theme of this post, I’ll elide the make depend
step from future examples, but if you’re playing along at home you may discover that you need to run it when you introduce new complexity in pursuit of perfect non-action.)
[] dothraki@iBook:~/trivial$ make
ocamlbuild -classic-display -use-ocamlfind -pkgs lwt.syntax,mirage-types.lwt -tags "syntax(camlp4o),annot,bin_annot,strict_sequence,principal" -cflag -g -lflags -g,-linkpkg,-dontlink,unix main.native.o
ocamlfind ocamldep -package mirage-types.lwt -package lwt.syntax -syntax camlp4o -modules main.ml > main.ml.depends
ocamlfind ocamlc -c -g -annot -bin-annot -principal -strict-sequence -package mirage-types.lwt -package lwt.syntax -syntax camlp4o -o main.cmo main.ml
+ ocamlfind ocamlc -c -g -annot -bin-annot -principal -strict-sequence -package mirage-types.lwt -package lwt.syntax -syntax camlp4o -o main.cmo main.ml
File "main.ml", line 8, characters 2-13:
Error: Unbound module OS
Command exited with code 2.
make: *** [main.native.o] Error 10
[] dothraki@iBook:~/trivial$
Oh, bother.
Let’s have a look at the main.ml
generated by mirage configure --xen
with our empty config.ml
.
(* Generated by Mirage (Wed, 23 Jul 2014 18:21:24 GMT). *)
open Lwt
let _ = Printexc.record_backtrace true
let () =
OS.Main.run (join [])
This looks like the right general idea - OS.Main.run
to invoke a thread, join []
(from Lwt
) to operate on an empty list of work to do. We have no work to do. It’s a nice day.
Unfortunately, we can’t accomplish our goal of doing nothing if we can’t do anything (in other words, we can’t run our zero threads if we have no idea how to run anything), so we’ll have to do a little more work first.
If we can figure out how to get OS.Main.run
included, we’ll be off to the races. Let’s have a look at the Makefile
, which is also programmatically generated by mirage configure
and shows which libraries the invocation of make
will link against.
[] dothraki@iBook:~/trivial$ grep ^LIBS Makefile
LIBS = -pkgs lwt.syntax,mirage-types.lwt
Mirage applications will always get mirage-types.lwt
and lwt.syntax
, and might get more libraries included if they’re requested in config.ml
or required by a driver requested there. Unfortunately, neither of these gets us OS
.
Some snooping on utop
with strace
reveals that an implementation for OS
lives in the mirage-xen
and mirage-unix
libraries. If we add mirage-xen
to LIBS
manually in the Makefile
(or mirage-unix
, if we generated the Makefile
with mirage configure --unix
) and then make
, we do get something runnable. But that’s not good enough! We don’t want to manually edit files that a computer could generate for us! That sounds like work, and that’s not what today is all about.
One possible solution is to do is write a few lines into our config.ml
to request that an appropriate library be included in LIBS
. We could ask for mirage-unix
directly, but that wouldn’t work when we want to do nothing in Xen, and mirage-xen
has the same problem when we want to do nothing in Unix. Every time we wanted to do nothing on a different platform, we’d have to change our config.ml
! No way.
Instead, let’s request a driver that imports the OS
interface. Numerous drivers are available, but the simplest one is CONSOLE
, which provides basic text output on a screen. Let’s pick that one.
In order to use CONSOLE
, we’ll have to add some code to our config.ml
, disturbing its state of complete relaxation. Minimally, we need a call to register
, which is defined in mirage.ml
:
val register: string -> job impl list -> unit
(** [register name jobs] registers the application named by [name]
which will executes the given [jobs]. *)
It appears we’ll have to define at least one job impl
, where we’ll do the hard work of defining doing nothing, then register
it with some name. Say, out_to_lunch
. Mirage expects us to define the job in another file, then tell config.ml
where to find it with foreign
(source):
val foreign: string -> ?libraries:string list -> ?packages:string list -> 'a typ -> 'a impl
(** [foreign name libs packs constr typ] states that the module named
by [name] has the module type [typ]. If [libs] is set, add the
given set of ocamlfind libraries to the ones loaded by default. If
[packages] is set, add the given set of OPAM packages to the ones
loaded by default. *)
So we’ll have to make another file, say maybe loaf.ml
, with our top-secret and highly-valuable instructions for slacking off. (Before we get too high and mighty about all the instructions our project needs just to not do anything, remember that sometimes humans need help with this too.) loaf.ml
needs to have at least one module specified that can take a CONSOLE
module argument; traditionally unikernels call this module Main
, but we can call it anything. Say, for example, Relax
.
Modules in OCaml can’t be completely empty, and we’ll need a start
function for the program to link, so we’ll just get all that hard work out of the way and define it right now. Since Mirage will want to pass information on how to use the specific instance of a console to the program, start
will have to accept an argument of type console_impl
. Luckily, there’s no requirement that we actually do anything of consequence in there, so we can finally define what it means to do nothing - Lwt.return ()
. A full explanation of Lwt is outside the scope of our laziness today, but there is great documentation both through the Mirage website and at the Ocsigen site for Lwt.
module Relax (C: V1_LWT.CONSOLE) = struct
let start console =
Lwt.return ()
end
We define Relax
as a module parameterized by a V1_LWT.CONSOLE
module (which we have no plan to use, but config.ml
won’t be able to make a sensible program for us unless we’re ready to accept it). start
takes a console
argument representing a particular implementation of a console. If we had any plans to output anything, we’d do it by calling a function with console
as an argument, but we’re not going to do that. We’re just going to relax today.
Now that we know how to relax, we can point config.ml
at this code with foreign
, so that we’ll eventually be able to register
it. The code we want to run is in the file loaf.ml
, in a module called Relax
, so our first argument to foreign
will be "Loaf.Relax"
. (This works so simply because config.ml
and loaf.ml
are in the same directory - if you structure things differently, you’ll have to work harder, so I recommend against it.)
Our second argument to foreign
, according to the type signature, should be a 'a typ
, and if we do so, the function will return a 'a impl
. In order to call register
, we need at least one job impl
to put in the list, so we need to somehow make 'a
be job
in our call to foreign
. Mirage provides a job impl
called, simply, job
.
We can’t just pass the results of foreign "Loaf.Relax" job
off to register
, though, because if we do that, mirage configure
won’t know that we wanted a console, and we’ll still have no libraries that know about OS.Main.run
in our Makefile
. What we really need is a (console -> job) typ
, so we can get a (console -> job) impl
, and then pass it a console impl
to get a job impl
out.
Mirage provides two combinators for making this work - @->
for composing a 'a typ
and 'b typ
into a ('a -> 'b) typ
, and a combinator $
for applying a ('a -> 'b) impl
to a 'a impl
and getting a 'b impl
back. We can use foreign "Loaf.Relax" (console @-> job)
to get a (console -> job) typ
, let-bind this value to a variable (say, relax
), and use this along with a Mirage-provided console impl
as an argument to register
.
open Mirage
let () =
let relax = foreign "Loaf.Relax" (console @-> job) in
register "out_to_lunch" [ relax $ default_console ]
A mirage configure --xen
and make
get us a small, very lazy unikernel named mir-out_to_lunch.xen
and an automatically generated configuration file for Xen named out_to_lunch.xl
. (Both names are taken from the string we pass as the first argument to register
.) If we start it up this unikernel with sudo xm create -c out_to_lunch.xl
, we get the following (somewhat disappointing) output:
[] dothraki@iBook:~/trivial$ sudo xm create -c out_to_lunch.xl
[sudo] password for dothraki:
Using config file "./out_to_lunch.xl".
Started domain out_to_lunch (id=2)
Domain has already finished
Could not start console
Our unikernel finishes so quickly that Xen can’t attach a console to it. If we start it paused and then unpause it after our console attaches, we can watch it boot:
[] dothraki@iBook:~/trivial$ sudo xm create -c out_to_lunch.xl -p
Using config file "./out_to_lunch.xl".
Started domain out_to_lunch (id=3)
and, in another window, sudo xm unpause out_to_lunch
, to let out_to_lunch
start booting. We then see the output sent to the console by Mirage as it’s booting, before control is handed over to our user program - in this case, return ()
, which executes immediately, returns from the main program, and informs Xen that the virtual machine is shutting down.
Started domain out_to_lunch (id=3)
kernel.c: Mirage OS!
kernel.c: start_info: 0x11c1000(VA)
kernel.c: nr_pages: 0x10000
...
x86_mm.c: Demand map pfns at 10001000-2010001000.
Initialising timer interface
main returned 0
[] dothraki@iBook:~/trivial$
Doing Nothing with Different Drivers
We’ve succesfully done nothing with this simple config.ml
:
open Mirage
let () =
let relax = foreign "Loaf.Relax" (console @-> job) in
register "out_to_lunch" [ relax $ default_console ]
What if we want to do nothing over the network? Or do nothing with a filesystem? Or do nothing with all of these things?
Including support for a driver requires us to have two things: a typ
representation to include in the arguments to foreign
, and an impl
to include in the list of arguments to register
. The available typ
s and impl
s are defined in mirage.mli
; some typ
s have more than one corresponding impl
, or have parameterized constructors for impl
s.
For example, there are two ways to get a network impl
, the impl
representing raw device-level access to a network card:
val tap0: network impl
val netif: string -> network impl
tap0
just returns the first available network interface; netif
takes a string argument and attempts to find a matching network interface, then makes that interface available in the returned value. The same network typ
applies for both:
open Mirage
let () =
let relax = foreign "Loaf.Relax" (network @-> network @-> job) in
register "out_to_lunch" [ relax $ tap0 $ netif "1" ]
This config.ml
will build against a loaf.ml
that has a Relax
module parameterized by two V1_LWT.NETWORK
modules, and a start
function that expects two network impl
arguments:
module Relax (IGNORED: V1_LWT.NETWORK) (ALSO_IGNORED: V1_LWT.NETWORK) = struct
let start default string_parameterized =
Lwt.return ()
end
(To actually run this in Xen, we need to make a couple alterations to the autogenerated out_to_lunch.xl
so that two network interfaces are actually provided, but this can be done once and saved off somewhere so as not to be overwritten by subsequent rebuilds.)
We can do nothing with multiple kinds of drivers, too:
open Mirage
let () =
let relax = foreign "Loaf.Relax" (network @-> console @-> job) in
register "out_to_lunch" [ relax $ tap0 $ default_console ]
If we modify loaf.ml
have a Relax
module parameterized by a V1_LWT.NETWORK
and V1_LWT.CONSOLE
, and provide a start
with network impl
and console impl
parameters, we’re off to the races.
For The Ambitious
The start
function in Loaf.Relax
can do more than just immediately return, of course. Programmers with ambition, gumption, and Tasks To Accomplish can define programs that Get Things Done, and launch them from start
. Programs that plan to use, say, a network interface, can call functions provided by V1_LWT.NETWORK
on the network impl
provided to start
to generate and receive network traffic; console programs can write to their provided console impl
. The “Hello Mirage World” tutorial gives great examples and instructions for making things happen - perfect for a rainy day.
Today’s more of a return ()
type of day, though.