109 lines
5.2 KiB
OCaml
109 lines
5.2 KiB
OCaml
(** A simple scheduler for Solo5 in OCaml.
|
||
|
||
Solo5 has 5 hypercalls, 2 for reading and writing to a net device and 2 for
|
||
reading and writing to a block device. The last hypercall stops the program.
|
||
This library is an OCaml scheduler (based on Miou) that allows you to
|
||
interact with these devices. However, the behaviour of these hypercalls
|
||
needs to be specified in order to understand how to use them properly when
|
||
it comes to creating a unikernel in OCaml.
|
||
|
||
{2 Net devices.}
|
||
|
||
A net device is a TAP interface connected between your unikernel and the
|
||
network of your host system. It is through this device that you can
|
||
communicate with your system's network and receive packets from it. The
|
||
TCP/IP stack is also built from this device.
|
||
|
||
The user can read and write packets on such a device. However, you need to
|
||
understand how reading and writing behave when developing an application as
|
||
a unikernel using Solo5.
|
||
|
||
Writing a packet to the net device is direct and failsafe. In other words,
|
||
we don't need to wait for anything to happen before writing to the net
|
||
device (if an error occurs on your host system, the Solo5 tender will fail
|
||
- and by extension, so will your unikernel). So, from the scheduler's point
|
||
of view, writing to the net device is atomic and is never suspended by the
|
||
scheduler in order to have the opportunity to execute other tasks.
|
||
|
||
However, this is not the case when reading the net device. You might expect
|
||
to read packages, but they might not be available at the time you try to
|
||
read them. Miou_solo5 will make a first attempt at reading and if it fails,
|
||
the scheduler will ‘suspend’ the reading task (and everything that follows
|
||
from it) to observe at another point in the life of unikernel whether a
|
||
packet has just arrived.
|
||
|
||
Reading the net device is currently the only operation where suspension is
|
||
necessary. In this way, the scheduler can take the opportunity to perform
|
||
other tasks if reading failed in the first place. It is at the next
|
||
iteration of the scheduler (after it has executed at least one other task)
|
||
that Miou_solo5 will ask the tender if a packet has just arrived. If this is
|
||
the case, the scheduler will resume the read task, otherwise it will keep it
|
||
in a suspended state until the next iteration.
|
||
|
||
{2 Block devices.}
|
||
|
||
Block devices are different in that there is no expectation of whether or
|
||
not there will be data. A block device can be seen as content to which the
|
||
user has one access per page (generally 4096 bytes). It can be read and
|
||
written to. However, the read and write operation can take quite a long time
|
||
\- depending on the file system and your hardware on the host system.
|
||
|
||
There are therefore two types of read/write. An atomic read/write and a
|
||
scheduled read/write.
|
||
|
||
An atomic read/write is an operation where you can be sure that it is not
|
||
divisible (and that something else can be tried) and that the operation is
|
||
currently being performed. Nothing else can be done until this operation has
|
||
finished. It should be noted that once the operation has finished, the
|
||
scheduler does not take the opportunity to do another task. It continues
|
||
with what needs to be done after the read/write as you have implemented in
|
||
OCaml.
|
||
|
||
This approach is interesting when you want to have certain invariants (in
|
||
particular the state of the memory) that other tasks cannot alter despite
|
||
such an operation. The problem is that this operation can take a
|
||
considerable amount of time and we can't do anything else at the same time.
|
||
|
||
This is why there is the other method, the read/write operation, which is
|
||
suspended by default and will be performed when the scheduler has the best
|
||
opportunity to do so - in other words, when it has nothing else to do.
|
||
|
||
This type of operation can be interesting when reading/writing does not
|
||
depend on assumptions and when these operations can be carried out at a
|
||
later date without the current time at which the operation is carried out
|
||
having any effect on the result. For example, scheduling reads on a block
|
||
device that is read-only is probably more interesting than using atomic
|
||
reads (whether the read is done at time T0 or T1, the result remains the
|
||
same). *)
|
||
|
||
type bigstring =
|
||
(char, Bigarray.int8_unsigned_elt, Bigarray.c_layout) Bigarray.Array1.t
|
||
|
||
module Net : sig
|
||
type t
|
||
|
||
val read_bigstring : t -> ?off:int -> ?len:int -> bigstring -> int
|
||
val read_bytes : t -> ?off:int -> ?len:int -> bytes -> int
|
||
val write_bigstring : t -> ?off:int -> ?len:int -> bigstring -> unit
|
||
val write_string : t -> ?off:int -> ?len:int -> string -> unit
|
||
end
|
||
|
||
module Block : sig
|
||
type t
|
||
|
||
val atomic_read : t -> off:int -> bigstring -> unit
|
||
val atomic_write : t -> off:int -> bigstring -> unit
|
||
val read : t -> off:int -> bigstring -> unit
|
||
val write : t -> off:int -> bigstring -> unit
|
||
end
|
||
|
||
external clock_monotonic : unit -> (int[@untagged])
|
||
= "unimplemented" "miou_solo5_clock_monotonic"
|
||
[@@noalloc]
|
||
|
||
external clock_wall : unit -> (int[@untagged])
|
||
= "unimplemented" "miou_solo5_clock_wall"
|
||
[@@noalloc]
|
||
|
||
val sleep : int -> unit
|
||
val run : ?g:Random.State.t -> (unit -> 'a) -> 'a
|