Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Operating Systems  Module 1 – Foundations: what an operating system does

Kernel designs: monolithic to microkernel

Monolithic, modular, microkernel, hybrid, layered, exokernel and unikernel designs compared, with the crossings and message costs that decide between them.

  • Intermediate
  • 15 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Clang 21.0.0 and GCC 16.2.0
  • By MySmartCoPilot
Free: 15 minutes

What you will learn

  • Compare monolithic, layered, modular, microkernel, hybrid, exokernel and unikernel designs
  • Explain the performance and reliability trade-offs of moving services out of the kernel
  • Describe how Linux loads modules and where Rust fits in the kernel
  • Choose a kernel design for a system from its requirements

Before you start

What you’ll learn · Before you start

What you will learn

  • Compare monolithic, layered, modular, microkernel, hybrid, exokernel and unikernel designs
  • Explain the performance and reliability trade-offs of moving services out of the kernel
  • Describe how Linux loads modules and where Rust fits in the kernel
  • Choose a kernel design for a system from its requirements

Before you start

On this page

The Linux kernel is of the order of 20 million lines of source code, all of it written to run in kernel mode, where one bad write can bring down the whole machine; seL4’s kernel is about 10,000 lines and keeps everything else in ordinary processes. How much code to trust with full privilege is the first decision in designing a kernel, and every design answers it differently.

This lesson compares the main designs: monolithic and modular, microkernel, hybrid, layered, exokernel and unikernel. A model counts the crossings one read request makes under three of them, a measurement prices a message between two processes, and a table that answers back helps choose a design for a system’s requirements.

A monolithic kernel runs every service in one privileged address space

In a monolithic kernel: A kernel that runs all its services, such as file systems, networking and drivers, in kernel mode in one address space, as Linux and the BSDs do., the scheduler, memory management, file systems, the network stack and the drivers all run in kernel mode, in one shared address space. Linux and the BSDs are built this way. A request from a file system to a driver is an ordinary function call, as fast as a call can be. The figure sets it beside the opposite design:

Monolithic kernel against microkernel: the monolithic one runs every service in kernel mode, the microkernel only a small core.Monolithic:user mode holdsapplications onlyMicrokernel:user mode holdsapps, file server,network server,driversKernel mode:scheduler, memory,file systems,network, drivers(Linux: about 20million lines)Kernel mode:address spaces,threads, messages,scheduling (seL4:about 10,000 lines)system callmessages

Where the services run: monolithic kernel against microkernel

Text description of the diagram

The figure has two columns, with user mode in the top row and kernel mode in the bottom row.

  1. The left column is a monolithic kernel. In user mode there are only the applications. In kernel mode there is everything else: the scheduler, memory management, file systems, the network stack and the drivers, about 20 million lines of code in Linux. An arrow labelled system call leads from the applications down into the kernel.
  2. The right column is a microkernel. In user mode there are the applications and also the services: a file server, a network server and the drivers, each a process of its own. In kernel mode there is only a small core of address spaces, threads, messages and scheduling, about 10,000 lines of code in seL4. An arrow labelled messages leads from the processes down into the core, which carries every message between them.

The price is on the left of the figure: every line in that box is trusted. Microsoft’s driver documentation draws the consequence for Windows: because kernel-mode code shares one address space, nothing protects the rest of the system from a faulty driver there, and a crash in such a driver brings down the whole operating system. The seL4 whitepaper puts numbers on the exposure. It counts the kernel in the system’s trusted computing base, the part that must work correctly for the system to be secure, puts Linux at about 20 million lines against about 10,000 in seL4, three orders of magnitude, and argues that the attack surface shrinks accordingly.

Checkpoint

Check: The monolithic kernel

  1. Question 1 of 2 Why can one bug in a driver crash a whole system with a monolithic kernel?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The driver runs in kernel mode in the same address space as the rest of the kernel, so nothing stops a bad write

    Privilege is all or nothing inside the kernel. The design saves crossings (a read cost 2 mode switches in the model), and pays for it in fault isolation.

  2. Question 2 of 2 The seL4 whitepaper puts the Linux kernel at about 20 million lines and seL4 at about 10,000. Why does that comparison matter for security?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: All of those lines run with full privilege, so each bug in them can be a way to take over the whole system

    The whitepaper counts the kernel in the trusted computing base, the part that must work correctly for the system to be secure, and points out that Linux's kernel is three orders of magnitude larger than seL4's, with an attack surface to match.

Loadable modules extend a running kernel, with full privilege

Linux is monolithic, yet it adds and removes code while it runs. A loadable module: Kernel code, often a driver, compiled separately and loaded into a running kernel. Once loaded it runs in kernel mode with full privilege., often a driver, is compiled separately and loaded on demand. init_module(2) describes what loading does: it loads an ELF image into kernel space, relocates its symbols, sets its parameters and runs its init function. Follow a module from its build to that function:

Interactive · stage by stage How a loadable module becomes part of the running kernel Step from the module's build to its init function. Notice where the kernel checks it, and what it can do once it is in.
How a loadable module becomes part of the running kernel, step 8 of 8. Showing Build against the kernel, modprobe's dependencies, The finit_module call, The kernel checks it, Relocate, set parameters, Run the module's init, Part of the kernel. In focus: Part of the kernel.versions differBuild against the kernelkbuild, headers and configmodprobe's dependenciesmodules.dep.bin from depmodThe finit_module callneeds CAP_SYS_MODULEThe kernel checks itsymbol versions must matchRelocate, set parametersRun the module's initin kernel modePart of the kernelfull privilege, no isolation

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. Build. A module is compiled against a prepared kernel tree, with that kernel's configuration and header files, by the kernel's own build system, kbuild.
  2. Dependencies. modprobe adds a module together with the modules it depends on, which it reads from modules.dep.bin, the file that depmod writes.
  3. The system call. Loading is itself a system call, finit_module or init_module, and only a privileged process may make it, one that holds the CAP_SYS_MODULE capability.
  4. The version check. With module versioning on, the kernel compares the checksums of the kernel interfaces the module was built against with its own, and if they are not equal it refuses to load the module.
  5. Refused. A module built for another kernel is turned away at this point and has to be built again against the running kernel.
  6. Relocate. When the check passes, the kernel copies the module's ELF image into kernel memory, fixes up its references to kernel symbols, and sets the parameters the caller gave.
  7. Init. The kernel runs the module's init function. From here on the module's code runs in kernel mode, as if it had been part of the kernel from the start.
  8. No wall inside. Nothing separates the module from the rest of the kernel. A bug in a loaded driver can corrupt any kernel data or crash the machine, exactly as a bug in the kernel's own code can.

The checks happen before the module runs, never after. Only a process holding the CAP_SYS_MODULE capability may load one, and kbuild’s guide notes that, with module versioning on, the kernel refuses a module whose interface checksums differ from its own. Once loaded, though, a module is simply kernel code. Modules change when code arrives, not where it runs, which is why Linux is both monolithic and modular.

Rust entered the same picture as a safer way to write such code. The kernel’s Rust documentation describes abstractions that wrap the kernel’s C interfaces in an as-safe-as-possible API; drivers and file systems written in Rust use those abstractions rather than the raw C bindings, so code built on sound wrappers, whose own unsafe blocks keep their safety contracts, cannot introduce undefined behaviour. The code still runs in kernel mode with full privilege; what Rust removes is a large class of memory errors.

Linux 6.1

Linux 6.1 added the first support for writing kernel code in Rust; the KernelNewbies notes on that release describe it as initial support, to be improved in later releases.

Checkpoint

Check: Loadable modules and Rust in Linux

  1. Question 1 of 2 Which processes may load a module into a running Linux kernel?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Only privileged ones, holding the CAP_SYS_MODULE capability

    A module becomes part of the kernel, so the right to load one is the right to run any code in kernel mode. The kernel gives it only to processes with the matching capability.

  2. Question 2 of 2 Linux loads a network driver as a module. Is the driver isolated from the rest of the kernel?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: No; once loaded it runs in kernel mode in the kernel's address space, exactly like built-in code

    That is why Linux is called monolithic yet modular: modules change when code arrives in the kernel, not where it runs.

A microkernel moves services into processes and pays for every message

A microkernel: A kernel that keeps only address spaces, threads, scheduling and message passing in kernel mode, and runs file systems and drivers as processes, as seL4 and MINIX 3 do. keeps only what must be privileged: address spaces, threads, scheduling and a way to pass messages. File systems, network stacks and drivers become ordinary processes. MINIX 3, for example, keeps a tiny core in kernel mode and runs the other parts of the system as separate, protected processes in user mode. A crashing driver now ends one process, not the system.

Every request to a service then becomes inter-process communication (IPC): Ways for separate processes to exchange data, such as pipes, sockets, shared memory and messages, all set up or carried by the kernel.. The model below follows one read, from an application through a file system and a disk driver and back, under three designs, and counts the crossings:

One program · 14 languages One read request under three kernel designs

Python 3.14 · main.py

PATH = [
    "app",
    "file system",
    "disk driver",
    "file system",
    "app",
]  # one read and its reply
DESIGNS = [  # each design and the parts it runs inside the kernel
    ("monolithic kernel", {"file system", "disk driver"}),
    ("driver in user space", {"file system"}),
    ("microkernel", set()),
]


def trace(design: str, in_kernel: set[str]) -> None:
    print(f"{design}:")
    modes = contexts = 0
    current = PATH[0]  # the process whose address space is in use
    for a, b in zip(PATH, PATH[1:]):
        if a in in_kernel and b in in_kernel:
            how = "function call"
        elif b in in_kernel:
            how, modes = "trap into the kernel", modes + 1
        elif a in in_kernel:  # back to user mode, perhaps in another process
            modes += 1
            if b == current:
                how = "return to user mode"
            else:
                how, contexts, current = "switch to its process", contexts + 1, b
        else:  # two processes: a message goes through the kernel
            how, modes, contexts, current = (
                "message through the kernel",
                modes + 2,
                contexts + 1,
                b,
            )
        print(f"  {a} -> {b}: {how}")
    crash = (
        "stops the whole kernel"
        if "disk driver" in in_kernel
        else "ends only its process"
    )
    print(
        f"  {modes} mode switches, {contexts} context switches; a driver crash {crash}"
    )


for design, in_kernel in DESIGNS:
    trace(design, in_kernel)

In Python: A set holds the parts each design runs inside the kernel, and zip(PATH, PATH[1:]) walks the request one hop at a time.

Recorded with CPython 3.14.8 on macOS 26 arm64. It runs in your browser with Pyodide 314.0.7 (CPython 3.14.2). To run it yourself: python3 main.py

JavaScript Node 24 · main.mjs

const PATH = ["app", "file system", "disk driver", "file system", "app"]; // one read and its reply
const DESIGNS = [
  // each design and the parts it runs inside the kernel
  ["monolithic kernel", new Set(["file system", "disk driver"])],
  ["driver in user space", new Set(["file system"])],
  ["microkernel", new Set()],
];

function trace(design, inKernel) {
  console.log(`${design}:`);
  let modes = 0;
  let contexts = 0;
  let current = PATH[0]; // the process whose address space is in use
  for (let i = 0; i + 1 < PATH.length; i++) {
    const [a, b] = [PATH[i], PATH[i + 1]];
    let how;
    if (inKernel.has(a) && inKernel.has(b)) {
      how = "function call";
    } else if (inKernel.has(b)) {
      how = "trap into the kernel";
      modes += 1;
    } else if (inKernel.has(a)) {
      // back to user mode, perhaps in another process
      modes += 1;
      if (b === current) {
        how = "return to user mode";
      } else {
        how = "switch to its process";
        contexts += 1;
        current = b;
      }
    } else {
      // two processes: a message goes through the kernel
      how = "message through the kernel";
      modes += 2;
      contexts += 1;
      current = b;
    }
    console.log(`  ${a} -> ${b}: ${how}`);
  }
  const crash = inKernel.has("disk driver") ? "stops the whole kernel" : "ends only its process";
  console.log(`  ${modes} mode switches, ${contexts} context switches; a driver crash ${crash}`);
}

for (const [design, inKernel] of DESIGNS) trace(design, inKernel);

In JavaScript: The designs keep their in-kernel parts in a Set, so has answers each test in one step.

Recorded with Node.js 24.21.0 LTS (V8 13.6) on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0). To run it yourself: node main.mjs

Java 25 LTS · Main.java

import java.util.List;
import java.util.Set;

public class Main {
  static final List<String> PATH =
      List.of("app", "file system", "disk driver", "file system", "app"); // a read and its reply

  record Design(String name, Set<String> inKernel) {} // the parts it runs inside the kernel

  static void trace(Design design) {
    System.out.println(design.name() + ":");
    Set<String> inKernel = design.inKernel();
    int modes = 0;
    int contexts = 0;
    String current = PATH.get(0); // the process whose address space is in use
    for (int i = 0; i + 1 < PATH.size(); i++) {
      String a = PATH.get(i);
      String b = PATH.get(i + 1);
      String how;
      if (inKernel.contains(a) && inKernel.contains(b)) {
        how = "function call";
      } else if (inKernel.contains(b)) {
        how = "trap into the kernel";
        modes += 1;
      } else if (inKernel.contains(a)) { // back to user mode, perhaps in another process
        modes += 1;
        if (b.equals(current)) {
          how = "return to user mode";
        } else {
          how = "switch to its process";
          contexts += 1;
          current = b;
        }
      } else { // two processes: a message goes through the kernel
        how = "message through the kernel";
        modes += 2;
        contexts += 1;
        current = b;
      }
      System.out.println("  " + a + " -> " + b + ": " + how);
    }
    String crash =
        inKernel.contains("disk driver") ? "stops the whole kernel" : "ends only its process";
    System.out.println(
        "  "
            + modes
            + " mode switches, "
            + contexts
            + " context switches; a driver crash "
            + crash);
  }

  public static void main(String[] args) {
    trace(new Design("monolithic kernel", Set.of("file system", "disk driver")));
    trace(new Design("driver in user space", Set.of("file system")));
    trace(new Design("microkernel", Set.of()));
  }
}

In Java: Design is a record holding its name and the Set of parts it runs inside the kernel.

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. It runs in your browser with a TeaVM 0.16.0 build in QuickJS (quickjs-emscripten 0.32.0). To run it yourself: java Main.java

C++23 · main.cpp

#include <print>
#include <set>
#include <string>
#include <vector>

// one read and its reply
const std::vector<std::string> PATH = {"app", "file system", "disk driver",
                                       "file system", "app"};

struct Design {
  std::string name;
  std::set<std::string> in_kernel; // the parts it runs inside the kernel
};

void trace(const Design &design) {
  std::println("{}:", design.name);
  const auto &in_kernel = design.in_kernel;
  int modes = 0;
  int contexts = 0;
  std::string current = PATH[0]; // the process whose address space is in use
  for (std::size_t i = 0; i + 1 < PATH.size(); i++) {
    const std::string &a = PATH[i];
    const std::string &b = PATH[i + 1];
    std::string how;
    if (in_kernel.contains(a) && in_kernel.contains(b)) {
      how = "function call";
    } else if (in_kernel.contains(b)) {
      how = "trap into the kernel";
      modes += 1;
    } else if (in_kernel.contains(a)) { // back to user mode, perhaps elsewhere
      modes += 1;
      if (b == current) {
        how = "return to user mode";
      } else {
        how = "switch to its process";
        contexts += 1;
        current = b;
      }
    } else { // two processes: a message goes through the kernel
      how = "message through the kernel";
      modes += 2;
      contexts += 1;
      current = b;
    }
    std::println("  {} -> {}: {}", a, b, how);
  }
  const char *crash = in_kernel.contains("disk driver")
                          ? "stops the whole kernel"
                          : "ends only its process";
  std::println("  {} mode switches, {} context switches; a driver crash {}",
               modes, contexts, crash);
}

int main() {
  const std::vector<Design> designs = {
      {"monolithic kernel", {"file system", "disk driver"}},
      {"driver in user space", {"file system"}},
      {"microkernel", {}},
  };
  for (const auto &design : designs) {
    trace(design);
  }
}

In C++: std::set's contains, new in C++20, does the membership tests, and Design is a plain aggregate.

Recorded with Apple clang 21.0.0 (C++23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.cpp -o main && ./main

C# 14 · .NET 10 · app.cs

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

string[] path = ["app", "file system", "disk driver", "file system", "app"]; // a read and its reply
(string Name, string[] InKernel)[] designs = // each design and the parts it runs inside the kernel
[
    ("monolithic kernel", ["file system", "disk driver"]),
    ("driver in user space", ["file system"]),
    ("microkernel", []),
];
foreach (var (name, inKernel) in designs)
{
    Trace(name, inKernel, path);
}

static void Trace(string design, string[] inKernel, string[] path)
{
    Console.WriteLine($"{design}:");
    int modes = 0;
    int contexts = 0;
    string current = path[0]; // the process whose address space is in use
    for (int i = 0; i + 1 < path.Length; i++)
    {
        var (a, b) = (path[i], path[i + 1]);
        string how;
        if (inKernel.Contains(a) && inKernel.Contains(b))
        {
            how = "function call";
        }
        else if (inKernel.Contains(b))
        {
            how = "trap into the kernel";
            modes += 1;
        }
        else if (inKernel.Contains(a)) // back to user mode, perhaps in another process
        {
            modes += 1;
            if (b == current)
            {
                how = "return to user mode";
            }
            else
            {
                (how, contexts, current) = ("switch to its process", contexts + 1, b);
            }
        }
        else // two processes: a message goes through the kernel
        {
            how = "message through the kernel";
            (modes, contexts, current) = (modes + 2, contexts + 1, b);
        }
        Console.WriteLine($"  {a} -> {b}: {how}");
    }
    string crash = inKernel.Contains("disk driver")
        ? "stops the whole kernel"
        : "ends only its process";
    Console.WriteLine($"  {modes} mode switches, {contexts} context switches;"
        + $" a driver crash {crash}");
}

In C#: Each design is a named tuple; the tuple assignments update several counters at once.

Recorded with C# 14 on .NET 10.0.12 (SDK 10.0.401) on macOS 26 arm64. It runs in your browser with the .NET runtime for WebAssembly 10.0.12. To run it yourself: dotnet run app.cs

C23 · main.c

#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>

// one read and its reply
static const char *const PATH[] = {"app", "file system", "disk driver",
                                   "file system", "app"};

typedef struct {
  const char *name;
  const char *in_kernel[2]; // the parts it runs inside the kernel
} Design;

static bool contains(const Design *d, const char *part) {
  for (size_t i = 0; i < 2; i++) {
    if (d->in_kernel[i] != NULL && strcmp(d->in_kernel[i], part) == 0) {
      return true;
    }
  }
  return false;
}

static void trace(const Design *d) {
  printf("%s:\n", d->name);
  int modes = 0;
  int contexts = 0;
  const char *current = PATH[0]; // the process whose address space is in use
  for (size_t i = 0; i + 1 < sizeof PATH / sizeof PATH[0]; i++) {
    const char *a = PATH[i];
    const char *b = PATH[i + 1];
    const char *how;
    if (contains(d, a) && contains(d, b)) {
      how = "function call";
    } else if (contains(d, b)) {
      how = "trap into the kernel";
      modes += 1;
    } else if (contains(d, a)) { // back to user mode, perhaps elsewhere
      modes += 1;
      if (strcmp(b, current) == 0) {
        how = "return to user mode";
      } else {
        how = "switch to its process";
        contexts += 1;
        current = b;
      }
    } else { // two processes: a message goes through the kernel
      how = "message through the kernel";
      modes += 2;
      contexts += 1;
      current = b;
    }
    printf("  %s -> %s: %s\n", a, b, how);
  }
  const char *crash = contains(d, "disk driver") ? "stops the whole kernel"
                                                 : "ends only its process";
  printf("  %d mode switches, %d context switches; a driver crash %s\n", modes,
         contexts, crash);
}

int main(void) {
  const Design designs[] = {
      {"monolithic kernel", {"file system", "disk driver"}},
      {"driver in user space", {"file system", NULL}},
      {"microkernel", {NULL, NULL}},
  };
  for (size_t i = 0; i < sizeof designs / sizeof designs[0]; i++) {
    trace(&designs[i]);
  }
  return 0;
}

In C: Each design lists at most two parts in the kernel, with NULL for an empty place, and contains compares them with strcmp.

Recorded with Apple clang 21.0.0 (C23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.c -o main && ./main

PHP 8.5 · main.php

<?php

declare(strict_types=1);

const PATH = ['app', 'file system', 'disk driver', 'file system', 'app']; // a read and its reply

/** @param list<string> $inKernel the parts the design runs inside the kernel */
function trace(string $design, array $inKernel): void
{
    echo "{$design}:\n";
    $modes = 0;
    $contexts = 0;
    $current = PATH[0]; // the process whose address space is in use
    for ($i = 0; $i + 1 < count(PATH); $i++) {
        [$a, $b] = [PATH[$i], PATH[$i + 1]];
        $aIn = in_array($a, $inKernel, true);
        $bIn = in_array($b, $inKernel, true);
        if ($aIn && $bIn) {
            $how = 'function call';
        } elseif ($bIn) {
            $how = 'trap into the kernel';
            $modes += 1;
        } elseif ($aIn) { // back to user mode, perhaps in another process
            $modes += 1;
            if ($b === $current) {
                $how = 'return to user mode';
            } else {
                [$how, $contexts, $current] = ['switch to its process', $contexts + 1, $b];
            }
        } else { // two processes: a message goes through the kernel
            $how = 'message through the kernel';
            [$modes, $contexts, $current] = [$modes + 2, $contexts + 1, $b];
        }
        echo "  {$a} -> {$b}: {$how}\n";
    }
    $crash = in_array('disk driver', $inKernel, true)
        ? 'stops the whole kernel'
        : 'ends only its process';
    echo "  {$modes} mode switches, {$contexts} context switches; a driver crash {$crash}\n";
}

trace('monolithic kernel', ['file system', 'disk driver']);
trace('driver in user space', ['file system']);
trace('microkernel', []);

In PHP: in_array with true as its third argument compares strictly; a list assignment updates the counters together.

Recorded with PHP 8.5.11 (CLI) on macOS 26 arm64. It runs in your browser with php-wasm 0.2.0 (PHP 8.5.2). To run it yourself: php main.php

Rust 1.99 · main.rs

const PATH: [&str; 5] = ["app", "file system", "disk driver", "file system", "app"]; // read, reply

/// Traces one read for a design; `in_kernel` lists the parts it runs inside the kernel.
fn trace(design: &str, in_kernel: &[&str]) {
    println!("{design}:");
    let (mut modes, mut contexts) = (0, 0);
    let mut current = PATH[0]; // the process whose address space is in use
    for pair in PATH.windows(2) {
        let (a, b) = (pair[0], pair[1]);
        let how = match (in_kernel.contains(&a), in_kernel.contains(&b)) {
            (true, true) => "function call",
            (false, true) => {
                modes += 1;
                "trap into the kernel"
            }
            (true, false) if b == current => {
                modes += 1;
                "return to user mode"
            }
            (true, false) => {
                // back to user mode, in another process
                modes += 1;
                contexts += 1;
                current = b;
                "switch to its process"
            }
            (false, false) => {
                // two processes: a message goes through the kernel
                modes += 2;
                contexts += 1;
                current = b;
                "message through the kernel"
            }
        };
        println!("  {a} -> {b}: {how}");
    }
    let crash = if in_kernel.contains(&"disk driver") {
        "stops the whole kernel"
    } else {
        "ends only its process"
    };
    println!("  {modes} mode switches, {contexts} context switches; a driver crash {crash}");
}

fn main() {
    trace("monolithic kernel", &["file system", "disk driver"]);
    trace("driver in user space", &["file system"]);
    trace("microkernel", &[]);
}

In Rust: windows(2) walks the request one hop at a time, and the match on two booleans names the four cases, with a guard for the return to the same process.

Recorded with rustc 1.99.0 (edition 2024) on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: rustc --edition 2024 -O main.rs && ./main

TypeScript 6.0 · main.ts

const PATH = ["app", "file system", "disk driver", "file system", "app"]; // one read and its reply
const DESIGNS: [string, Set<string>][] = [
  // each design and the parts it runs inside the kernel
  ["monolithic kernel", new Set(["file system", "disk driver"])],
  ["driver in user space", new Set(["file system"])],
  ["microkernel", new Set()],
];

function trace(design: string, inKernel: Set<string>): void {
  console.log(`${design}:`);
  let modes = 0;
  let contexts = 0;
  let current = PATH[0]; // the process whose address space is in use
  for (let i = 0; i + 1 < PATH.length; i++) {
    const [a, b] = [PATH[i], PATH[i + 1]];
    let how: string;
    if (inKernel.has(a) && inKernel.has(b)) {
      how = "function call";
    } else if (inKernel.has(b)) {
      how = "trap into the kernel";
      modes += 1;
    } else if (inKernel.has(a)) {
      // back to user mode, perhaps in another process
      modes += 1;
      if (b === current) {
        how = "return to user mode";
      } else {
        how = "switch to its process";
        contexts += 1;
        current = b;
      }
    } else {
      // two processes: a message goes through the kernel
      how = "message through the kernel";
      modes += 2;
      contexts += 1;
      current = b;
    }
    console.log(`  ${a} -> ${b}: ${how}`);
  }
  const crash = inKernel.has("disk driver") ? "stops the whole kernel" : "ends only its process";
  console.log(`  ${modes} mode switches, ${contexts} context switches; a driver crash ${crash}`);
}

for (const [design, inKernel] of DESIGNS) trace(design, inKernel);

In TypeScript: A tuple type pairs each design's name with its Set; everything else is the JavaScript program.

Recorded with TypeScript 6.0.3 (strict), run by Node.js 24.21.0 on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0) after Sucrase 3.35.1. To run it yourself: npx -p typescript@6.0.3 tsc --noEmit --strict main.ts && node main.ts

Go 1.27 · main.go

package main

import (
	"fmt"
	"slices"
)

// one read and its reply
var path = []string{"app", "file system", "disk driver", "file system", "app"}

// trace follows one read; inKernel lists the parts the design runs inside the kernel.
func trace(design string, inKernel []string) {
	fmt.Printf("%s:\n", design)
	modes, contexts := 0, 0
	current := path[0] // the process whose address space is in use
	for i := 0; i+1 < len(path); i++ {
		a, b := path[i], path[i+1]
		aIn, bIn := slices.Contains(inKernel, a), slices.Contains(inKernel, b)
		var how string
		switch {
		case aIn && bIn:
			how = "function call"
		case bIn:
			how = "trap into the kernel"
			modes++
		case aIn: // back to user mode, perhaps in another process
			modes++
			if b == current {
				how = "return to user mode"
			} else {
				how, contexts, current = "switch to its process", contexts+1, b
			}
		default: // two processes: a message goes through the kernel
			how, modes, contexts, current = "message through the kernel", modes+2, contexts+1, b
		}
		fmt.Printf("  %s -> %s: %s\n", a, b, how)
	}
	crash := "ends only its process"
	if slices.Contains(inKernel, "disk driver") {
		crash = "stops the whole kernel"
	}
	fmt.Printf("  %d mode switches, %d context switches; a driver crash %s\n",
		modes, contexts, crash)
}

func main() {
	trace("monolithic kernel", []string{"file system", "disk driver"})
	trace("driver in user space", []string{"file system"})
	trace("microkernel", nil)
}

In Go: A tagless switch takes the cases in order, and slices.Contains does the membership tests.

Recorded with Go 1.27.1 on macOS 26 arm64. It runs in your browser with the Go compiler in WebAssembly 1.27.1. To run it yourself: go run main.go

Swift 6.4 · main.swift

let path = ["app", "file system", "disk driver", "file system", "app"]  // a read and its reply

/// Traces one read; `inKernel` holds the parts the design runs inside the kernel.
func trace(_ design: String, inKernel: Set<String>) {
  print("\(design):")
  var (modes, contexts) = (0, 0)
  var current = path[0]  // the process whose address space is in use
  for (a, b) in zip(path, path.dropFirst()) {
    let how: String
    switch (inKernel.contains(a), inKernel.contains(b)) {
    case (true, true):
      how = "function call"
    case (false, true):
      how = "trap into the kernel"
      modes += 1
    case (true, false) where b == current:
      how = "return to user mode"
      modes += 1
    case (true, false):  // back to user mode, in another process
      how = "switch to its process"
      (modes, contexts, current) = (modes + 1, contexts + 1, b)
    case (false, false):  // two processes: a message goes through the kernel
      how = "message through the kernel"
      (modes, contexts, current) = (modes + 2, contexts + 1, b)
    }
    print("  \(a) -> \(b): \(how)")
  }
  let crash = inKernel.contains("disk driver") ? "stops the whole kernel" : "ends only its process"
  print("  \(modes) mode switches, \(contexts) context switches; a driver crash \(crash)")
}

trace("monolithic kernel", inKernel: ["file system", "disk driver"])
trace("driver in user space", inKernel: ["file system"])
trace("microkernel", inKernel: [])

In Swift: zip(path, path.dropFirst()) gives each hop, and the switch on two Booleans covers the four cases, with where for the same process.

Recorded with Apple Swift 6.4 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: swiftc -O main.swift -o main && ./main

Ruby 4.0 · main.rb

# frozen_string_literal: true

PATH = ['app', 'file system', 'disk driver', 'file system', 'app'].freeze # a read and its reply

# Follows one read through a design whose in_kernel parts run inside the kernel.
class Trace
  def initialize(in_kernel)
    @in_kernel = in_kernel
    @modes = 0
    @contexts = 0
    @current = PATH.first # the process whose address space is in use
  end

  def run
    PATH.each_cons(2) { |from, to| puts "  #{from} -> #{to}: #{hop(from, to)}" }
    crash = inside?('disk driver') ? 'stops the whole kernel' : 'ends only its process'
    puts "  #{@modes} mode switches, #{@contexts} context switches; a driver crash #{crash}"
  end

  private

  def inside?(part) = @in_kernel.include?(part)

  def hop(from, to)
    return 'function call' if inside?(from) && inside?(to)
    return trap if inside?(to)
    return back_to_user(to) if inside?(from)

    message(to)
  end

  def trap
    @modes += 1
    'trap into the kernel'
  end

  # Back to user mode, perhaps in another process.
  def back_to_user(to)
    @modes += 1
    return 'return to user mode' if to == @current

    switch_to(to)
    'switch to its process'
  end

  # Two processes: a message goes through the kernel.
  def message(to)
    @modes += 2
    switch_to(to)
    'message through the kernel'
  end

  def switch_to(process)
    @contexts += 1
    @current = process
  end
end

[
  ['monolithic kernel', ['file system', 'disk driver']],
  ['driver in user space', ['file system']],
  ['microkernel', []]
].each do |design, in_kernel|
  puts "#{design}:"
  Trace.new(in_kernel).run
end

In Ruby: A small Trace class keeps the counters, so that each method stays as short as RuboCop asks, and each_cons(2) gives the hops.

Recorded with CRuby 4.0.7 on macOS 26 arm64. It runs in your browser with ruby.wasm 2.10.1 (Ruby 4.0.0). To run it yourself: ruby main.rb

Kotlin 2.4 · Main.kt

val path = listOf("app", "file system", "disk driver", "file system", "app") // a read and its reply

// Traces one read; inKernel holds the parts the design runs inside the kernel.
fun trace(
    design: String,
    inKernel: Set<String>,
) {
    println("$design:")
    var modes = 0
    var contexts = 0
    var current = path[0] // the process whose address space is in use
    for ((a, b) in path.zipWithNext()) {
        val how =
            when {
                a in inKernel && b in inKernel -> {
                    "function call"
                }

                b in inKernel -> {
                    modes += 1
                    "trap into the kernel"
                }

                a in inKernel -> {
                    // back to user mode, perhaps in another process
                    modes += 1
                    if (b == current) {
                        "return to user mode"
                    } else {
                        contexts += 1
                        current = b
                        "switch to its process"
                    }
                }

                else -> {
                    // two processes: a message goes through the kernel
                    modes += 2
                    contexts += 1
                    current = b
                    "message through the kernel"
                }
            }
        println("  $a -> $b: $how")
    }
    val crash = if ("disk driver" in inKernel) "stops the whole kernel" else "ends only its process"
    println("  $modes mode switches, $contexts context switches; a driver crash $crash")
}

fun main() {
    trace("monolithic kernel", setOf("file system", "disk driver"))
    trace("driver in user space", setOf("file system"))
    trace("microkernel", emptySet())
}

In Kotlin: zipWithNext gives the hops, and the when expression's branches return the label for each one.

Recorded with Kotlin 2.4.20 on Java 25.0.4.1 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar

Dart 3.13 · main.dart

const path = [
  'app',
  'file system',
  'disk driver',
  'file system',
  'app',
]; // a read and its reply

/// Traces one read; [inKernel] holds the parts the design runs inside the kernel.
void trace(String design, Set<String> inKernel) {
  print('$design:');
  var modes = 0;
  var contexts = 0;
  var current = path[0]; // the process whose address space is in use
  for (var i = 0; i + 1 < path.length; i++) {
    final (a, b) = (path[i], path[i + 1]);
    final String how;
    if (inKernel.contains(a) && inKernel.contains(b)) {
      how = 'function call';
    } else if (inKernel.contains(b)) {
      how = 'trap into the kernel';
      modes += 1;
    } else if (inKernel.contains(a)) {
      // back to user mode, perhaps in another process
      modes += 1;
      if (b == current) {
        how = 'return to user mode';
      } else {
        how = 'switch to its process';
        contexts += 1;
        current = b;
      }
    } else {
      // two processes: a message goes through the kernel
      how = 'message through the kernel';
      modes += 2;
      contexts += 1;
      current = b;
    }
    print('  $a -> $b: $how');
  }
  final crash = inKernel.contains('disk driver')
      ? 'stops the whole kernel'
      : 'ends only its process';
  print(
    '  $modes mode switches, $contexts context switches; a driver crash $crash',
  );
}

void main() {
  trace('monolithic kernel', {'file system', 'disk driver'});
  trace('driver in user space', {'file system'});
  trace('microkernel', {});
}

In Dart: final String how is assigned exactly once on every path, which the compiler checks.

Recorded with Dart 3.13.5 on macOS 26 arm64. It runs in your browser with a WebAssembly build by dart2wasm 3.13.5. To run it yourself: dart run main.dart

What every language printed

monolithic kernel:
  app -> file system: trap into the kernel
  file system -> disk driver: function call
  disk driver -> file system: function call
  file system -> app: return to user mode
  2 mode switches, 0 context switches; a driver crash stops the whole kernel
driver in user space:
  app -> file system: trap into the kernel
  file system -> disk driver: switch to its process
  disk driver -> file system: trap into the kernel
  file system -> app: switch to its process
  4 mode switches, 2 context switches; a driver crash ends only its process
microkernel:
  app -> file system: message through the kernel
  file system -> disk driver: message through the kernel
  disk driver -> file system: message through the kernel
  file system -> app: message through the kernel
  8 mode switches, 4 context switches; a driver crash ends only its process

The monolithic kernel served the read with 2 mode switches and no context switch, the design with the driver in user space needed 4 and 2, and the microkernel 8 and 4, one for each of its four messages. In return, only the monolithic kernel loses everything when the driver crashes. What one message costs decides how heavy those four are. This program measures a round trip between two processes through a pair of pipes:

A message round trip between two processes, against a function call Python · ipc_cost.py
import os
import time

ROUNDS = 20_000  # round trips timed in each run
RUNS = 3  # the best of 3 runs: other programs can only slow a run


def function_call(value: int) -> int:
    return value + 1


def main() -> None:
    to_child = os.pipe()
    to_parent = os.pipe()
    pid = os.fork()
    if pid == 0:  # the child sends every byte straight back
        for _ in range(ROUNDS * RUNS):
            os.write(to_parent[1], os.read(to_child[0], 1))
        os._exit(0)
    best_trip = best_call = float("inf")
    for _ in range(RUNS):
        start = time.perf_counter()
        for _ in range(ROUNDS):
            os.write(to_child[1], b"x")  # a message to the other process ...
            os.read(to_parent[0], 1)  # ... and its answer
        best_trip = min(best_trip, (time.perf_counter() - start) / ROUNDS)
        start = time.perf_counter()
        for i in range(ROUNDS):
            function_call(i)
        best_call = min(best_call, (time.perf_counter() - start) / ROUNDS)
    os.waitpid(pid, 0)
    print(f"a Python function call: {best_call * 1e9:.0f} ns")
    print(f"a round trip to another process: {best_trip * 1e6:.1f} microseconds")
    print(f"one round trip took as long as {best_trip / best_call:,.0f} function calls")


main()

Output

a Python function call: 44 ns
a round trip to another process: 42.3 microseconds
one round trip took as long as 960 function calls

This output changes from run to run: the times depend on the machine and on what else it is running

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ipc_cost.py

The round trip took some tens of microseconds, hundreds of times a Python function call. Pipes in a general-purpose kernel are slow messengers, though, and kernels built around messages do far better. The seL4 whitepaper reports that performance has always been the hallmark of the L4 family it belongs to, with a round trip around 720 cycles on x86, and cites a comparison that puts seL4’s message cost about 10 to 20 % above the hardware’s own limit for entering the kernel, switching address spaces and leaving it.

Key idea

Moving a service out of the kernel buys isolation and a smaller trusted base, and costs a message, with its mode and context switches, on every request to that service.

Checkpoint

Check: Microkernels and the cost of messages

  1. Question 1 of 2 In the crossings model, how many context switches does one read take on the microkernel?

    Type a number.

    A hint

    Count the messages between processes; each one switches to another process.

    Show the answer: Question 1 of 2

    Answer: 4 context switches

    The request and the reply each cross two process boundaries: app to file server, file server to driver, and back again. Four messages make 4 context switches and 8 mode switches, against none and 2 in the monolithic kernel.

  2. Question 2 of 2 Why did L4-family microkernels such as seL4 put so much work into making messages fast?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Every request to a service is a message, so a microkernel's speed is mostly the speed of its messages

    The seL4 whitepaper says performance has always been the hallmark of L4 microkernels, and reports seL4's IPC at about 10 to 20 % above the hardware limit of entering the kernel, switching address spaces and leaving it.

Hybrid, layered, exokernel and unikernel designs

The two poles leave room between and beyond them. CS2023 lists monolithic, layered, modular, microkernel and unikernel models among the core ideas every computer-science student should meet:

Design The idea An example
Layered Each level builds only on the levels below it Dijkstra’s THE system: processor allocation at level 0, memory at 1, the console at 2, input and output at 3, user programs at 4
Modular A monolithic kernel that loads parts at run time Linux and its loadable modules
Hybrid A microkernel’s structure, with services kept in the kernel for speed XNU, which its README calls a hybrid of the Mach kernel, components of FreeBSD and the IOKit driver framework
Exokernel A minimal kernel that only multiplexes the hardware securely, with abstractions in libraries The research exokernel of Engler, Kaashoek and O’Toole
Unikernel One application compiled with the library operating system it needs, as one specialised image MirageOS, whose unikernels run under a Xen or KVM hypervisor

A hybrid behaves like a monolithic kernel wherever code runs inside it: a driver that crashes inside XNU’s kernel is a kernel crash. Exokernels and unikernels go the other way, moving abstractions out of the kernel into libraries linked with each application, so the protection boundary moves too: a unikernel relies on the hypervisor to keep applications apart.

Checkpoint

Check: Hybrid, layered, exokernel and unikernel designs

  1. One question What does the XNU kernel of macOS and iOS combine, according to its README?

    Choose one answer.

    Show the answer: One question

    Answer: The Mach kernel, components from FreeBSD and a C++ driver framework, IOKit

    A hybrid kernel borrows a microkernel's structure and keeps services in the kernel for speed, so its behaviour on a crash is closer to a monolithic kernel's.

Choosing a design from what the system must do

No design wins everywhere, so the choice starts from the system’s requirements. Answer the four questions in the table below; each answer rules designs out or adds a caveat, with the reason and the lesson’s own numbers:

Interactive · answer and compare Which kernel design fits your system? Answer four questions about the system you are building. Each answer rules designs out or adds a caveat, with the reason.
Must the system keep running when a driver crashes?
Must the privileged code be small enough to verify or audit?
Will the machine run many different programs?
Must it run on a wide range of off-the-shelf hardware?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Four kernel designs against the cost of a request, a crashing driver, the privileged code and hardware support
Design Cost of a request A driver crash Privileged code Hardware support
Monolithic (Linux, the BSDs) Runs in kernel mode: every service and driver Good a read in the model: 2 mode switches, no context switch Poor a driver runs in kernel mode, so its crash stops the system Poor Linux: about 20 million lines, all of it privileged Good drivers for a very wide range of hardware
Microkernel (seL4, MINIX 3) Runs in kernel mode: address spaces, threads, messages Mixed a read in the model: 8 mode and 4 context switches; fast messages help Good a driver is a process, so its crash ends only that process Good seL4: about 10,000 lines, with a proof of correctness Mixed fewer drivers, written as processes
Hybrid (XNU) Runs in kernel mode: Mach plus BSD services and drivers Good busy services stay in the kernel, so requests stay cheap Mixed drivers inside the kernel can still bring it down Poor a large kernel of Mach, BSD parts and drivers Good the platform's full set of drivers
Unikernel (MirageOS) Runs in kernel mode: one application and its library OS Good the application and its library OS share one address space Mixed one application per machine; the hypervisor keeps machines apart Good only the library code the application links in Poor a few virtual devices under a hypervisor such as Xen or KVM
How the answers decide
  • “Must the system keep running when a driver crashes?” Yes, a driver may fail without stopping it: rules out Monolithic (Linux, the BSDs) and adds a caveat to Hybrid (XNU). A crashing driver in kernel mode stops the whole system. A monolithic kernel runs every driver there; a hybrid runs many of them there too.
  • “Must the privileged code be small enough to verify or audit?” Yes, it has to be proved or pass an audit: rules out Monolithic (Linux, the BSDs) and Hybrid (XNU). Millions of lines of privileged code cannot be verified. seL4's proof covers a kernel of about 10,000 lines, and a unikernel keeps the privileged code to what one application links.
  • “Will the machine run many different programs?” Yes, many programs and users: rules out Unikernel (MirageOS). A unikernel is one application linked with the library operating system it needs; it does not run other programs.
  • “Must it run on a wide range of off-the-shelf hardware?” Yes, whatever hardware users have: rules out Unikernel (MirageOS) and adds a caveat to Microkernel (seL4, MINIX 3). Unikernels run on a hypervisor's few virtual devices, and microkernels have fewer drivers than Linux or a vendor's own kernel.
  • “Must the system keep running when a driver crashes?” No, a reboot is acceptable and “Must the privileged code be small enough to verify or audit?” No, testing is enough: adds a caveat to Microkernel (seL4, MINIX 3). Without a need for fault isolation or a small trusted base, the extra messages of a microkernel buy little.

Try two systems. A laptop answers no, no, yes and yes: the table rules out the unikernel, adds a caveat to the microkernel and leaves a monolithic or hybrid kernel. A controller that must keep running and whose kernel must pass a safety audit, on known hardware, answers yes, yes, no and no, and only the microkernel and the unikernel remain.

In short: a system that must survive a crashing driver, or prove its privileged code correct, points to a microkernel; one application on a hypervisor suits a unikernel; and many programs on whatever hardware users own favour a monolithic kernel such as Linux, or a hybrid such as XNU on Apple’s machines.

Checkpoint

Check: Choosing a design

  1. One question A team builds the controller of a medical device and must show that the privileged code is correct. Which design fits best?

    Choose one answer.

    Show the answer: One question

    Answer: A microkernel such as seL4, whose small kernel has a machine-checked proof of correctness

    The table's verification question rules out monolithic and hybrid kernels for this reason. A unikernel keeps privileged code small too, but a device controller usually runs several tasks and real drivers.

Practise: load a module after the modules it needs

modprobe adds a module together with the modules it depends on, and each one must be loaded before the modules that use it. Write the order it needs, and catch the dependencies that can never be satisfied:

Exercise · Medium · Python

Load a module after the modules it needs

A tool such as modprobe adds a module together with the modules it depends on, and each one has to be in the kernel before any module that uses it. Write load_order(module, depends) in loader.py.

  • depends maps a module's name to the list of modules it needs; a module that is not in depends needs nothing.
  • Return the list of modules to load for module, in order: every module after all the modules it needs, each module once, and module itself last. Visit a module's needs in the order its list gives them.
  • If a module ends up needing itself, through any chain of dependencies, raise ValueError: no order can work.

For example, with {"camera": ["video-core", "usb-core"], "video-core": ["media-core"]}, load_order("camera", ...) is ["media-core", "video-core", "usb-core", "camera"]. The names are invented for the exercise.

Starter code · loader.py

def load_order(module, depends):
    """The modules to load for module, each after the modules it needs, each once."""
    # Replace this line with your code.
    return [module]
The sample tests · test_loader.py
from loader import load_order


def raises_value_error(call):
    try:
        call()
    except ValueError:
        return True
    return False


def test_the_example():
    """loads every dependency before the module that needs it"""
    depends = {"camera": ["video-core", "usb-core"], "video-core": ["media-core"]}
    assert load_order("camera", depends) == ["media-core", "video-core", "usb-core", "camera"]


def test_shared_dependency():
    """loads a module that two others need only once, before both"""
    depends = {"vpn": ["net", "crypto"], "net": ["core"], "crypto": ["core"]}
    assert load_order("vpn", depends) == ["core", "net", "crypto", "vpn"]


def test_no_dependencies():
    """loads a module that needs nothing on its own"""
    assert load_order("solo", {}) == ["solo"]
    assert load_order("solo", {"solo": []}) == ["solo"]


def test_order_of_needs():
    """follows the order in which a module lists its needs"""
    assert load_order("x", {"x": ["b", "a"]}) == ["b", "a", "x"]
    assert load_order("x", {"x": ["a", "b"]}) == ["a", "b", "x"]


def test_long_chain():
    """loads a chain from its far end"""
    depends = {"d": ["c"], "c": ["b"], "b": ["a"]}
    assert load_order("d", depends) == ["a", "b", "c", "d"]


def test_cycles():
    """refuses a module that needs itself, directly or through others"""
    for depends in [{"a": ["a"]}, {"a": ["b"], "b": ["a"]}, {"a": ["b"], "b": ["c"], "c": ["a"]}]:
        assert raises_value_error(lambda: load_order("a", depends)), f"{depends} should raise ValueError"
A hint

Use a depth-first walk: before you add a module to the result, visit each module it needs. Keep a set of the modules you are in the middle of visiting; meeting one of them again means a cycle.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Then try the lesson’s questions on kernel designs; the idea you know least well comes up first:

Practice that adapts to you

Practise this lesson

11 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 11 What defines a monolithic kernel?

    Choose one answer.

    Show the answer: Question 1 of 11

    Answer: Its services, file systems, network stack and drivers all run in kernel mode in one address space

    Monolithic means one privileged address space for every service. Calls between services are cheap function calls, and a fault in any of them is a fault in the kernel.

  2. Question 2 of 11 In the crossings model, how many mode switches does one read take in the monolithic kernel?

    Type a number.

    Show the answer: Question 2 of 11

    Answer: 2 mode switches

    One trap into the kernel and one return to user mode. The file system and the driver call each other as functions inside the kernel, with no switch at all.

  3. Question 3 of 11 What do Rust drivers in Linux use to reach the kernel's C code?

    Choose one answer.

    Show the answer: Question 3 of 11

    Answer: Safe abstractions that wrap the kernel's C interfaces, so drivers do not call the raw bindings themselves

    Rust support arrived in Linux 6.1. Its design wraps unsafe access to the C side in safe abstractions, so that code built on them avoids undefined behaviour as long as the wrappers are sound and its own unsafe blocks keep their safety contracts.

  4. Question 4 of 11 A module built for an older kernel is loaded into a newer one with module versioning on. What happens when the interfaces' checksums differ?

    Choose one answer.

    Show the answer: Question 4 of 11

    Answer: The kernel refuses to load the module

    The check exists because a module compiled against different structures or functions would corrupt the kernel the moment it ran.

  5. Question 5 of 11 In a microkernel, how does the application's request reach the file server?

    Choose one answer.

    Show the answer: Question 5 of 11

    Answer: As a message that the kernel carries from one process to the other

    Moving services into processes turns calls into messages, which is the whole trade: isolation for the price of more crossings.

  6. Question 6 of 11 ipc_cost.py measured a round trip between two processes through pipes in some tens of microseconds. Why does that not tell you what a message costs in seL4?

    Choose one answer.

    Show the answer: Question 6 of 11

    Answer: Pipes are a general-purpose channel of a monolithic kernel, while L4-family kernels make a message close to the hardware's own cost

    The count of crossings is the design's; their price depends on the kernel. A pipe round trip shows the shape of the cost, and seL4's numbers show how far a kernel built for messages can cut it.

  7. Question 7 of 11 In Dijkstra's layered THE system, what did the lowest level take care of?

    Choose one answer.

    Show the answer: Question 7 of 11

    Answer: Allocating the processor among processes

    A layered design builds each level only on the levels below it, which makes each level easier to reason about, and makes a call that must cross several layers slower.

  8. Question 8 of 11 MirageOS builds unikernels. What is a unikernel?

    Choose one answer.

    Show the answer: Question 8 of 11

    Answer: One application compiled together with the library operating system it needs, running on a hypervisor such as Xen or KVM

    Exokernels and unikernels push the operating system's abstractions into libraries; the unikernel goes furthest, building one specialised machine image per application.

  9. Question 9 of 11 A driver that runs inside XNU's kernel crashes. Which design does the outcome resemble, and why?

    Choose one answer.

    Show the answer: Question 9 of 11

    Answer: A monolithic kernel's, because the driver ran in kernel mode in the kernel's address space

    What decides the effect of a crash is the mode and address space the code ran in, not the family name of the kernel around it.

  10. Question 10 of 11 A network appliance must keep running even if its Wi-Fi driver crashes. Which design does the trade-off table rule out first?

    Choose one answer.

    Show the answer: Question 10 of 11

    Answer: The monolithic kernel, because its drivers run in kernel mode

    The first question of the table decides it: if a driver must fail without stopping the system, it must run as a process, which only the microkernel guarantees.

  11. Question 11 of 11 A cloud server must run many customers' programs on ordinary hardware, and a reboot after a rare driver crash is acceptable. Which design is the usual choice, and why?

    Choose one answer.

    Show the answer: Question 11 of 11

    Answer: A monolithic kernel such as Linux, for its speed, its drivers and its support for many programs

    Each design wins somewhere. The requirements decide: isolation and verification favour microkernels, a single application favours unikernels, and many programs on ordinary hardware favour monolithic kernels.

Interview questions

A warm-up comes next, with the answer an interviewer hopes to hear. The bank of this module goes further, asking whether a monolithic kernel with modules is a contradiction and which kernel suits a medical device.

  1. Fresher · Warm-up

    What is the difference between a monolithic kernel and a microkernel?

    A monolithic kernel runs all of its services, the scheduler, memory management, file systems, the network stack and the drivers, in kernel mode in one address space; Linux and the BSDs work this way. Calls between services are cheap function calls, but every line is privileged, so a bug in any driver can crash the whole system. A microkernel keeps only the minimum in kernel mode, address spaces, threads, scheduling and message passing, and runs file systems and drivers as ordinary processes; seL4 and MINIX 3 are examples. A crashing driver then ends only its own process, and the privileged code is small, about 10,000 lines in seL4 against millions in Linux. The price is that every request to a service becomes messages between processes, so a microkernel lives or dies by how fast it makes them.

Key takeaways

  • A monolithic kernel runs every service in one privileged address space: requests are cheap function calls, and a bug anywhere can crash the system; Linux has about 20 million such lines.
  • Linux’s loadable modules are checked before they load (privilege, interface versions) but then run as ordinary kernel code; Rust adds memory safety inside the kernel, not isolation.
  • A microkernel keeps about 10,000 lines privileged in seL4 and turns every service request into messages: 8 mode and 4 context switches for one read in the model, against 2 and none in a monolithic kernel.
  • A pipe round trip between two processes cost some tens of microseconds, while seL4’s round trip is near the hardware’s limit, so the price of each message decides how much a microkernel pays.
  • Hybrid, layered, exokernel and unikernel designs trade the same things differently, and the requirements of the system, not the fashion, should pick one.

Terms in this lesson

inter-process communication (IPC)
Ways for separate processes to exchange data, such as pipes, sockets, shared memory and messages, all set up or carried by the kernel.
loadable module
Kernel code, often a driver, compiled separately and loaded into a running kernel. Once loaded it runs in kernel mode with full privilege.
microkernel
A kernel that keeps only address spaces, threads, scheduling and message passing in kernel mode, and runs file systems and drivers as processes, as seL4 and MINIX 3 do.
monolithic kernel
A kernel that runs all its services, such as file systems, networking and drivers, in kernel mode in one address space, as Linux and the BSDs do.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.