MirageOS: Lwt loop architecture and block device best practices

Hi everyone,

I have two questions regarding MirageOS architecture and storage:

  1. Lwt event loop: Does the entire unikernel run within a single, global Lwt main loop? Do all jobs share the same event loop?

  2. Block I/O resources: Could you recommend documentation or examples for working directly with block devices (Mirage_block)? I am specifically interested in how to correctly implement:

  • Writing, updating, reading data.
  • Managing sector-level offsets, alignment, and so on.

Thanks in advance!

Hi, and thank you for your interest in MirageOS. To keep it brief and regarding the MirageOS ecosystem, we do use Lwt as a scheduler, which is implemented through a single event loop. You can find the implementation here: mirage-solo5/lib/main.ml at main Ā· mirage/mirage-solo5 Ā· GitHub

Regarding block devices, within the Solo5 context, these are accessible for reading and writing by pages (the page size, usually 512 bytes, is configurable when launching the unikernel with solo5-hvt). You can see the Solo5 implementation here: GitHub - mirage/mirage-block-solo5: Solo5 implementation of MirageOS block interface

Furthermore, we (the Robur cooperative) have just released cachet: a small library that provides read (with cache) access and a write pipeline for so-called ā€œatomicā€ values. This is what we use, among other things, to implement our KV-store as a unikernel (for more details, you can read our blog post on the subject here).

I would also like to mention that our cooperative is exploring a new workflow to develop unikernels, specifically with OCaml 5 and the use of effects. We currently have several (unreleased) projects that already allow us to develop some unikernels as long as we provide the right compilation environment for dune:

  • mkernel is perhaps the fundamental library, providing a scheduler for unikernels based on Miou. The repository contains some examples regarding block devices. We also gave a presentation at the last MirageOS retreat which is available here
  • mnet is the TCP/IP stack we use, which aims to solve issues encountered with mirage-tcpip (specifically regarding memory leaks).
  • vifu is an HTTP server (HTTP/1.1 and h2) for unikernels.

Even though these projects are experimental, we are iterating quickly and already offer:

  • immuable: a unikernel for serving a static website from an archive (also a block device).
  • kevin: a KV-store using our recent bancos project.
  • chaos: an early implementation of an NTP server.

This is a new approach that no longer involves the mirage tool, but we believe the developer experience is much better (no functors, no tools generating crucial files like dune files, and of course, the use of effects and Miou as the scheduler).

You can contact us directly via Discord, for instance, which has a dedicated MirageOS channel. Feel free to reach out whether you want to experiment with the mirage tool, Lwt, and block devices, or to learn more about our new approach to developing unikernels.

Best,

2 Likes

Hi!

For block device usage there is a small game in mirage-skeleton that uses a block device to save game state (high score). Each block represents a game state, so you can have multiple game states.

A more complicated example is Tar_mirage where read and writes are done handling cases where the block device block size is a multiple of the tar block size (512 bytes). It gets fiddly at times, and it might be possible to write it in a neater way. If by ā€œmanaging sector-level offsets, alignmentā€ you mean something else please let me know.

I would be happy to hear if you think we can improve the documentation of mirage-block. Are there things that are unclear? mirage-block 3.0.2 (latest) Ā· OCaml Package

I might have misspoken: the question wasn’t specifically about mirage-block, but rather about general best practices for working with block devices—specifically, how to update data correctly, what to do if the data exceeds the sector size or is smaller than it, and so on

Basically, you have only 2 hypercalls when it’s about block-device:

  • read a page
  • write a page

If you want, for instance, to write a byte, it will be: read a page where the byte exists, set the byte on the page read and write the page then. That’s the reason behind cachet.wr where you can set a byte and persist your modification (cachet.wr will, then, emit the right page via the hypercall to be actually written).

On the reading side, you can add a cache system so that when you would like to read the byte at the offset 0 and then, the byte at the offset 1, you need only one hypercall (read the first page), fill a cache and use it then when it’s about another offset existing into the same page. This is where cachet proposes such abstraction.

A block device can not be extended. So you can not use more than what the block-device can give you. The Solo5 tender can give you the number of pages that your block-device has.

It’s worth noting that cachet (like most of our libraries) is independent of our schedulers. Therefore, you can use it with the Mirage (and Lwt) ecosystem or with mkernel.

1 Like

If my data is page-aligned but I only have a partial page ready, can I just zero-fill it to the full page size and flush it? I don’t mind the extra disk usage—I just need to treat the page as ā€˜complete’ and move on. Is this a viable strategy?

Yes, this works. In tar-mirage there’s quite some code to read a full page, blit in some bytes, and write it back in case we’re at unaligned blocks.

I.e. if you have written 250 bytes and want to append another 50 bytes, we would first read the entire 512 bytes, then blit the new 50 bytes from offset 250 to 300, and write that block back. Of course this can be a bit more complex (for longer data you want to write). We always pad with 0 a partial block to the 512 byte boundary (taking 512 as the default block size, while you can select it at startup time of the tender).

Hope this helps.

1 Like

Should I align writes with the host’s physical sector size instead?