In contrast to classical OS work, which can be roughly split between monolithic kernels (with great performance) versus microkernels that provide great isolation between OS components (at the expense of performance), our work embraces both the monolithic design (no protection between components) and the modularity that microkernels advocated.
Our key observation is that we can obtain performance via careful API design and static linking, rather than short-circuiting API boundaries for performance. To achieve the overarching principle of modularity, Unikraft consists of two main components:
Micro-libraries are software components which implement one of the core Unikraft APIs; we differentiate them from libraries in that they have minimal dependencies and can be arbitrarily small, e.g., a scheduler. All micro-libraries that implement the same API are interchangeable. One such API contains multiple memory allocators that all implement the ukalloc interface. In addition, Unikraft supports libraries that can provide functionality from external library projects (OpenSSL, musl, Protobuf, etc.), applications (SQLite, Redis, etc.), or even platforms (e.g., Solo5, Firecracker, Raspberry Pi 3).
Build system which provides a Kconfig-based menu for users to select which micro-libraries to use in an application build, for them to select which platform(s) and CPU architectures to target, and even configure individual micro-libraries if desired. The build system then compiles all of the micro-libraries, links them, and produces one binary per selected platform.
The figure below shows Unikraft's architecture. All components are
micro-libraries that have their own
Makefile and Kconfig configuration files
Config.uk), and so can be added to the unikernel build independently of each
other. APIs are also micro-libraries that can be easily enabled or disabled via
a Kconfig menu; unikernels can thus compose which APIs to choose to best cater
to an application's needs (e.g., an RCP-style application might turn off the
uksched API in order to implement a high performance, run-to-completion event
Unikraft's architecture also includes components that add POSIX support, making it relatively easy to support existing applications. Unikraft can improve the performance of applications in two ways:
Un modified applications, by eliminating syscall overheads, reducing image size and memory consumption, and by choosing efficient memory allocators.
Specialization, by adapting applications to take advantage of lower level APIs wherever performance is critical (e.g., a database application seeking high disk I/O throughput).
The ability to easily swap components in and out, and to plug applications in at
different levels presents application developers with a wide range of
optimization possibilities. To begin with, unmodified applications (e.g. "Hello
World" and NGINX) can use the posix-compatibility layer with
musl (1️ in the figure above) or
transparently getting low boot times, lower memory consumption and improved
throughput because of the lack of syscall overheads, as Unikraft syscalls are
effectively function calls.
Likewise, the application developer can easily select an appropriate memory
to obtain maximum performance, or to use multiple different ones within the same
unikernel (e.g., a simple, fast memory allocator for the boot code, and a
standard one for the application itself).
Developers interested in fast boot times could further optimize the unikernel by
providing their own boot code (5️) to comply with the
ukboot API; in Unikaft EuroSys'21
paper we show experiments
with two boot code micro-libraries, one with static memory pages and one with
dynamic ones, showing the trade-off between boot time and memory allocation
For network-bound applications, the developers can use the standard socket
interface (2️) or the lower level, higher performance
(7️) in order to significantly improve throughput; we will discuss this API in
greater detail below. Similarly, disk-bound applications such as databases can
follow a standard path through the
micro-library (3️), or optimize throughput by oding against the ukblock API
(8️). Schedulers are also pluggable (4️), and each CPU core can run a
The Unikraft Core The Unikraft core is the actual kernel code, and is comprised of several components:
the architecture code: This defines behaviours and hardware interactions specific to the target architecture (x86_64, ARM, RISC-V). For example, for the x86_64 architecture, this component defines the usable registers, data types sizes and how Thread-Local Storage should happen.
the platform code: This defines interaction with the underlying hardware, depending on whether a hypervisor is present or not, and which hypervisor is present. For example, if the KVM hypervisor is present, Unikraft will behave almost as if it runs bare-metal, needing to initialize the hardware components according to the manufacturer specifications. The difference from bare-metal is made only at the entry, where some information, like the memory layout, the available console, are supplied by the bootloader (Multiboot) and there’s no need to interact with the BIOS or UEFI. In the case of Xen, many of the hardware-related operations must be done through hypercalls, thus reducing the direct interaction of Unikraft with the hardware.
internal libraries: These define behaviour independent of the hardware, like scheduling, networking, memory allocation, basic file systems. These libraries are the same for every platform or architecture, and rely on the platform code and the architecture code to perform the needed actions. The internal libraries differ from the external ones in the implemented functionalities. The internal ones define parts of the kernel, while the external ones define user-space level functionalities. For example, uknetdev and lwip are 2 libraries that define networking components. Uknetdev is an internal library that interacts with the network card and defines how packages are sent using it. Lwip is an external library that defines networking protocols, like IP, TCP, UDP. This library knows that the packages are somehow sent over the NIC, but it is not concerned how. That is the job of the kernel. The core, toghether with the external libraries, applications and platform codes, form the final Unikernel.
Feel free to ask questions, report issues, and meet new people.