miou-solo5/lib/miou_solo5.ml

470 lines
15 KiB
OCaml
Raw Normal View History

2024-12-05 14:58:46 +00:00
let src = Logs.Src.create "miou.solo5"
module Log = (val Logs.src_log src : Logs.LOG)
type bigstring =
(char, Bigarray.int8_unsigned_elt, Bigarray.c_layout) Bigarray.Array1.t
external bigstring_get_uint8 : bigstring -> int -> int = "%caml_ba_ref_1"
external bigstring_set_uint8 : bigstring -> int -> int -> unit
= "%caml_ba_set_1"
external bigstring_get_int32_ne : bigstring -> int -> int32
= "%caml_bigstring_get32"
external bigstring_set_int32_ne : bigstring -> int -> int32 -> unit
= "%caml_bigstring_set32"
let bigstring_blit_to_bytes bstr ~src_off dst ~dst_off ~len =
let len0 = len land 3 in
let len1 = len lsr 2 in
for i = 0 to len1 - 1 do
let i = i * 4 in
let v = bigstring_get_int32_ne bstr (src_off + i) in
Bytes.set_int32_ne dst (dst_off + i) v
done;
for i = 0 to len0 - 1 do
let i = (len1 * 4) + i in
let v = bigstring_get_uint8 bstr (src_off + i) in
Bytes.set_uint8 dst (dst_off + i) v
done
let bigstring_blit_from_string src ~src_off dst ~dst_off ~len =
let len0 = len land 3 in
let len1 = len lsr 2 in
for i = 0 to len1 - 1 do
let i = i * 4 in
let v = String.get_int32_ne src (src_off + i) in
bigstring_set_int32_ne dst (dst_off + i) v
done;
for i = 0 to len0 - 1 do
let i = (len1 * 4) + i in
let v = String.get_uint8 src (src_off + i) in
bigstring_set_uint8 dst (dst_off + i) v
done
external miou_solo5_net_read :
(int[@untagged])
-> bigstring
-> (int[@untagged])
-> (int[@untagged])
-> bytes
-> int = "unimplemented" "miou_solo5_net_read"
[@@noalloc]
external miou_solo5_net_write :
(int[@untagged])
-> (int[@untagged])
-> (int[@untagged])
-> bigstring
-> (int[@untagged]) = "unimplemented" "miou_solo5_net_write"
[@@noalloc]
external miou_solo5_block_read :
(int[@untagged])
-> (int[@untagged])
-> (int[@untagged])
-> bigstring
-> (int[@untagged]) = "unimplemented" "miou_solo5_block_read"
[@@noalloc]
external miou_solo5_block_write :
(int[@untagged])
-> (int[@untagged])
-> (int[@untagged])
-> bigstring
-> (int[@untagged]) = "unimplemented" "miou_solo5_block_write"
[@@noalloc]
external unsafe_get_int64_ne : bytes -> int -> int64 = "%caml_bytes_get64u"
let invalid_argf fmt = Format.kasprintf invalid_arg fmt
module Block_direct = struct
type t = { handle: int; pagesize: int }
let unsafe_read t ~off bstr =
match miou_solo5_block_read t.handle off t.pagesize bstr with
| 0 -> ()
| 2 -> invalid_arg "Miou_solo5.Block.read"
| _ -> assert false (* AGAIN | UNSPEC *)
let atomic_read t ~off bstr =
if off land (t.pagesize - 1) != 0 then
invalid_argf
"Miou_solo5.Block.atomic_read: [off] must be aligned to the pagesize \
(%d)"
t.pagesize;
if Bigarray.Array1.dim bstr < t.pagesize then
invalid_argf
"Miou_solo5.Block.atomic_read: length of [bstr] must be greater than \
or equal to one page (%d)"
t.pagesize;
unsafe_read t ~off bstr
let unsafe_write t ~off bstr =
match miou_solo5_block_write t.handle off t.pagesize bstr with
| 0 -> ()
| 2 -> invalid_arg "Miou_solo5.Block.write"
| _ -> assert false (* AGAIN | UNSPEC *)
let atomic_write t ~off bstr =
if off land (t.pagesize - 1) != 0 then
invalid_argf
"Miou_solo5.Block.atomic_write: [off] must be aligned to the pagesize \
(%d)"
t.pagesize;
if Bigarray.Array1.dim bstr < t.pagesize then
invalid_argf
"Miou_solo5.Block.atomic_write: length of [bstr] must be greater than \
or equal to one page (%d)"
t.pagesize;
unsafe_write t ~off bstr
end
module Handles = struct
type 'a t = { mutable contents: (int * 'a) list }
let find tbl fd = List.assq fd tbl.contents
let replace tbl fd v' =
let contents =
List.fold_left
(fun acc (k, v) -> if k = fd then (k, v') :: acc else (k, v) :: acc)
[] tbl.contents
in
tbl.contents <- contents
let add tbl k v = tbl.contents <- (k, v) :: tbl.contents
let clear tbl = tbl.contents <- []
let create _ = { contents= [] }
let append t k v =
try
let vs = find t k in
replace t k (v :: vs)
with Not_found -> add t k [ v ]
let fold_left_map fn acc t =
let acc, contents = List.fold_left_map fn acc t.contents in
t.contents <- contents;
acc
let filter_map fn t =
let contents = List.filter_map fn t.contents in
t.contents <- contents
end
type elt = { time: int; syscall: Miou.syscall; mutable cancelled: bool }
module Heapq = struct
include Miou.Pqueue.Make (struct
type t = elt
let dummy = { time= 0; syscall= Obj.magic (); cancelled= false }
let compare { time= a; _ } { time= b; _ } = Int.compare a b
end)
let rec drop heapq = try delete_min_exn heapq; drop heapq with _ -> ()
end
type action = Rd of arguments | Wr of arguments
and arguments = {
t: Block_direct.t
; bstr: bigstring
; off: int
; syscall: Miou.syscall
; mutable cancelled: bool
}
type domain = {
handles: Miou.syscall list Handles.t
; sleepers: Heapq.t
; blocks: action Queue.t
}
let domain =
let rec split_from_parent v =
Handles.clear v.handles;
Heapq.drop v.sleepers;
Queue.clear v.blocks;
make ()
and make () =
{
handles= Handles.create 0x100
; sleepers= Heapq.create ()
; blocks= Queue.create ()
}
in
let key = Stdlib.Domain.DLS.new_key ~split_from_parent make in
fun () -> Stdlib.Domain.DLS.get key
let blocking_read fd =
let syscall = Miou.syscall () in
let domain = domain () in
Log.debug (fun m -> m "append [%d] as a reader" fd);
Handles.append domain.handles fd syscall;
Miou.suspend syscall
module Net = struct
type t = int
let rec read t ~off ~len bstr =
let read_size = Bytes.make 8 '\000' in
let result = miou_solo5_net_read t bstr off len read_size in
let read_size = Int64.to_int (unsafe_get_int64_ne read_size 0) in
match result with
| 0 -> read_size
| 1 -> blocking_read t; read t ~off ~len bstr
| 2 -> invalid_arg "Miou_solo5.Net.read"
| _ -> assert false (* UNSPEC *)
let read_bigstring t ?(off = 0) ?len bstr =
let len =
match len with Some len -> len | None -> Bigarray.Array1.dim bstr - off
in
if len < 0 || off < 0 || off > Bigarray.Array1.dim bstr - len then
invalid_arg "Miou_solo5.Net.read_bigstring: out of bounds";
read t ~off ~len bstr
let read_bytes =
let bstr = Bigarray.(Array1.create char c_layout 0x7ff) in
fun t ?(off = 0) ?len buf ->
let rec go dst_off dst_len =
if dst_len > 0 then begin
let len = Int.min (Bigarray.Array1.dim bstr) dst_len in
let len = read_bigstring t ~off:0 ~len bstr in
bigstring_blit_to_bytes bstr ~src_off:0 buf ~dst_off ~len;
if len > 0 then go (dst_off + len) (dst_len - len) else dst_off - off
end
else dst_off - off
in
let len =
match len with Some len -> len | None -> Bytes.length buf - off
in
if len < 0 || off < 0 || off > Bytes.length buf - len then
invalid_arg "Miou_solo5.Net.read_bytes: out of bounds";
go off len
let write t ~off ~len bstr =
match miou_solo5_net_write t off len bstr with
| 0 -> ()
| 2 -> invalid_arg "Miou_solo5.Net.write"
| _ -> assert false (* AGAIN | UNSPEC *)
let write_bigstring t ?(off = 0) ?len bstr =
let len =
match len with Some len -> len | None -> Bigarray.Array1.dim bstr - off
in
if len < 0 || off < 0 || off > Bigarray.Array1.dim bstr - len then
invalid_arg "Miou_solo5.Net.write_bigstring: out of bounds";
write t ~off ~len bstr
let write_string =
let bstr = Bigarray.(Array1.create char c_layout 0x7ff) in
fun t ?(off = 0) ?len str ->
let rec go src_off src_len =
if src_len > 0 then begin
let len = Int.min (Bigarray.Array1.dim bstr) src_len in
bigstring_blit_from_string str ~src_off bstr ~dst_off:0 ~len;
write_bigstring t ~off:0 ~len bstr;
Miou.yield ();
go (src_off + len) (src_len - len)
end
in
let len =
match len with Some len -> len | None -> String.length str - off
in
if len < 0 || off < 0 || off > String.length str - len then
invalid_arg "Miou_solo5.Net.write_string: out of bounds";
go off len
end
module Block = struct
include Block_direct
let read t ~off bstr =
if off land (t.pagesize - 1) != 0 then
invalid_argf
"Miou_solo5.Block.read: [off] must be aligned to the pagesize (%d)"
t.pagesize;
if Bigarray.Array1.dim bstr < t.pagesize then
invalid_argf
"Miou_solo5.Block.read: length of [bstr] must be greater than or equal \
to one page (%d)"
t.pagesize;
let syscall = Miou.syscall () in
let args = { t; bstr; off; syscall; cancelled= false } in
let domain = domain () in
Queue.push (Rd args) domain.blocks;
Miou.suspend syscall
let write t ~off bstr =
if off land (t.pagesize - 1) != 0 then
invalid_argf
"Miou_solo5.Block.write: [off] must be aligned to the pagesize (%d)"
t.pagesize;
if Bigarray.Array1.dim bstr < t.pagesize then
invalid_argf
"Miou_solo5.Block.write: length of [bstr] must be greater than or \
equal to one page (%d)"
t.pagesize;
let syscall = Miou.syscall () in
let args = { t; bstr; off; syscall; cancelled= false } in
let domain = domain () in
Queue.push (Wr args) domain.blocks;
Miou.suspend syscall
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]
let sleep until =
let syscall = Miou.syscall () in
let domain = domain () in
let elt = { time= clock_monotonic () + until; syscall; cancelled= false } in
Heapq.insert elt domain.sleepers;
Miou.suspend syscall
(* poll part of Miou_solo5 *)
let rec sleeper domain =
match Heapq.find_min_exn domain.sleepers with
| exception Heapq.Empty -> None
| { cancelled= true; _ } ->
Heapq.delete_min_exn domain.sleepers;
sleeper domain
| { time; _ } -> Some time
let in_the_past t = t == 0 || t <= clock_monotonic ()
let rec collect_sleepers domain signals =
match Heapq.find_min_exn domain.sleepers with
| exception Heapq.Empty -> signals
| { cancelled= true; _ } ->
Heapq.delete_min_exn domain.sleepers;
collect_sleepers domain signals
| { time; syscall; _ } when in_the_past time ->
Heapq.delete_min_exn domain.sleepers;
collect_sleepers domain (Miou.signal syscall :: signals)
| _ -> signals
let collect_handles ~handles domain signals =
let fn acc (handle, syscalls) =
if (1 lsl handle) land handles != 0 then
let signals = List.rev_map Miou.signal syscalls in
(List.rev_append signals acc, (handle, []))
else (acc, (handle, syscalls))
in
Handles.fold_left_map fn signals domain.handles
let rec consume_block domain signals =
match Queue.pop domain.blocks with
| Rd { cancelled= true; _ } | Wr { cancelled= true; _ } ->
consume_block domain signals
| Rd { t; bstr; off; syscall; _ } ->
Block.unsafe_read t ~off bstr;
Miou.signal syscall :: signals
| Wr { t; bstr; off; syscall; _ } ->
Block.unsafe_write t ~off bstr;
Miou.signal syscall :: signals
let clean domain uids =
let to_keep syscall =
let uid = Miou.uid syscall in
List.exists (fun uid' -> uid != uid') uids
in
let fn0 (handle, syscalls) =
match List.filter to_keep syscalls with
| [] -> None
| syscalls -> Some (handle, syscalls)
in
let fn1 (({ syscall; _ } : elt) as elt) =
if not (to_keep syscall) then elt.cancelled <- true
in
let fn2 = function
| Rd ({ syscall; _ } as elt) | Wr ({ syscall; _ } as elt) ->
if not (to_keep syscall) then elt.cancelled <- true
in
Handles.filter_map fn0 domain.handles;
Heapq.iter fn1 domain.sleepers;
Queue.iter fn2 domain.blocks
external miou_solo5_yield : (int[@untagged]) -> (int[@untagged])
= "unimplemented" "miou_solo5_yield"
[@@noalloc]
type waiting = Infinity | Yield | Sleep
let wait_for ~block domain =
match (sleeper domain, block) with
| None, true -> Infinity
| (None | Some _), false -> Yield
| Some point, true ->
let until = point - clock_monotonic () in
if until < 0 then Yield else Sleep
(* The behaviour of our select is a little different from what we're used to
seeing. Currently, only a read on a net device can produce a necessary
suspension (the reception of packets on the network).
However, a special case concerns the block device. Reading and writing to it
can take time. It can be interesting to suspend these actions and actually
do them when we should be waiting (as long as a sleeper is active or until
an event appears).
The idea is to suspend these actions so that we can take the opportunity to
do something else and actually do them when we have the time to do so: when
Miou has no more tasks to do and when we don't have any network events to
manage.
The implication of this would be that our unikernels would be limited by I/O
on block devices. They won't be able to go any further than reading and
writing to block devices. As far as I/O on net devices is concerned, we are
only limited by the OCaml code that has to handle incoming packets. Packet
writing, on the other hand, is direct. *)
let select ~block cancelled_syscalls =
let domain = domain () in
clean domain cancelled_syscalls;
let handles = ref 0 in
let rec go signals =
match wait_for ~block domain with
| Infinity ->
(* Miou tells us we can wait forever ([block = true]) and we have no
sleepers. So we're going to: take action on the block devices and ask
Solo5 if we need to manage an event. If we have an event after the
action on the block device ([handles != 0]), we stop and send the
signals to Miou. If not, we take the opportunity to possibly go
further. *)
let signals = consume_block domain signals in
handles := miou_solo5_yield 0;
if !handles == 0 then go signals else signals
| Yield ->
(* Miou still has work to do but asks if there are any events. We ask
Solo5 if there are any and return the possible signals to Miou. *)
handles := miou_solo5_yield 0;
signals
| Sleep ->
(* We have a sleeper that is still active and will have to wait a while
before consuming it. In the meantime, we take action on the block
devices and repeat our [select] if Solo5 tells us that there are no
events ([handle == 0]). *)
let signals = consume_block domain signals in
handles := miou_solo5_yield 0;
if !handles == 0 then go signals else signals
in
let signals = go [] in
let signals = collect_handles ~handles:!handles domain signals in
collect_sleepers domain signals
let events _domain = { Miou.interrupt= ignore; select; finaliser= ignore }
let run ?g fn = Miou.run ~events ?g ~domains:0 fn