Lesson 6 · Operating Systems

Microkernels

The **microkernel structure of operating system** design keeps only what genuinely requires kernel privilege — address spaces, threads, and IPC — and moves drivers, file systems, and networking into ordinary user processes. Isolation improves; every service call becomes a message.

Microkernels concept diagramA visual explanation of the layout and operations shown in this lesson.only three things need kernel privilege — everything else is a processfile systemdisk drivernetwork stackdisplay serverabove the line: ordinary user processesbelow: the only privileged codekerneladdress spaces · threads · IPCevery service call is a message through the kernel, not a function calla driver fault kills one process — Mach IPC ≈ 115 µs, L4 ≈ 5 µs, and that 20× is why this became practical
1

The Minimality Principle

A microkernel operating system answers one question: what must run in kernel mode? Liedtke's minimality principle gives the test — a feature belongs in the kernel only if moving it out would prevent the system from implementing required functionality at all. Everything that survives that test is roughly three things: address spaces, threads with a scheduler, and inter-process communication.

Everything else leaves. File systems become user processes. Device drivers become user processes. The network stack, the display server, even the pager that handles page faults — all ordinary programs with no special privilege, talking over IPC. The kernel shrinks from millions of lines to somewhere between 8,000 and 15,000.

The payoff is fault containment, and it is not marketing. In a monolithic kernel a null-pointer dereference in a Wi-Fi driver executes with full privilege and takes the machine down. In a microkernel that driver is a process: it faults, it dies, a supervisor restarts it, and the rest of the system keeps running. MINIX 3 demonstrated driver restarts transparent to running applications.

  • In kernel: address spaces, threads, IPC
  • Out of kernel: drivers, file systems, networking
  • Kernel shrinks to roughly 10,000 lines
  • A driver fault kills a process, not the machine
2

Why IPC Cost Decides Everything

The first-generation microkernels — Mach most famously — were slow, and the reason was structural. Work that a monolithic kernel does with a function call becomes a message crossing two protection domains. A single read() might cross the kernel boundary four times: application to file system, file system to driver, and back again. If each crossing costs 100 microseconds, the design is dead.

Mach's IPC cost around 115 microseconds on the hardware of the day. Jochen Liedtke's L4, published in 1993, got the same operation to roughly 5 — a 20x improvement — by treating IPC as the one thing worth optimising to the instruction level: registers for short messages instead of memory buffers, no unnecessary capability checks on the fast path, and a design deliberately tuned to the cache and TLB behaviour of the actual CPU.

That is the whole history of the microkernel argument in one number. The 1990s Tanenbaum-Torvalds debate happened when microkernel IPC was slow enough to be disqualifying. Once L4 fixed the constant factor, the architectural argument could be had on its merits.

  • Every service call becomes a message
  • One read can cross protection domains several times
  • Mach IPC ≈ 115 µs; L4 IPC ≈ 5 µs
  • Registers instead of buffers on the fast path
3

Microkernel vs Monolithic Kernel

Microkernel vs monolithic kernel is a trade of performance against isolation, and the honest summary is that monolithic wins on raw throughput while microkernel wins on containment and verifiability. Linux, Windows NT internals, and the BSDs are monolithic (Windows and macOS are hybrids that kept graphics and drivers in kernel space precisely for speed).

The monolithic advantage is that a subsystem call is a function call — no context switch, no message marshalling, shared data structures reachable by pointer. The cost is that every one of those subsystems can corrupt any other, and the kernel's trusted computing base is measured in tens of millions of lines that no one can verify.

seL4 is the strongest counter-argument the microkernel side has. It is a formally verified L4 descendant with a machine-checked mathematical proof that the implementation matches its specification, that it enforces integrity and confidentiality, and that its WCET bounds hold. Roughly 10,000 lines of C, and it is deployed in military avionics and secure phones — the point being that verification is only tractable because the kernel is small.

  • Monolithic wins throughput; microkernel wins containment
  • A monolithic subsystem call is a function call
  • Microkernel TCB is small enough to verify
  • seL4 carries a machine-checked correctness proof
Implementation

One file read, counted in protection-domain crossings

# Counting protection-domain crossings for one read(), both ways.
# The kernel in a microkernel only routes -- it implements nothing.

def monolithic_read():
    crossings = 0
    crossings += 1                      # user -> kernel
    # VFS -> block layer -> driver are all ORDINARY FUNCTION CALLS here:
    # one address space, arguments in registers, data shared by pointer.
    crossings += 1                      # kernel -> user
    return crossings

def microkernel_read():
    crossings = 0
    def ipc(a, b):                      # every service call is a message
        nonlocal crossings
        crossings += 2                  # sender -> kernel -> receiver
    ipc("app", "fs")                    # ask the file system
    ipc("fs", "driver")                 # the FS has no disk capability
    crossings += 1                      # the IRQ arrives as a message too
    ipc("driver", "fs")                 # reply with the block
    crossings += 1                      # fs -> app reply completes the last pair
    return crossings

mono, micro = monolithic_read(), microkernel_read()
print("monolithic: %d crossings" % mono)
print("microkernel: %d crossings (%dx)" % (micro, micro // mono))
print("Mach IPC ~115us made this fatal; L4 at ~5us made it payable")
// L4-style IPC: the one operation worth optimising to the instruction.
// Short messages travel in REGISTERS -- no memory buffer, no copy.
#include <iostream>
#include <array>
#include <cstdint>
struct Message {
    std::array<std::uintptr_t, 8> w {
    };
}; // message registers
// A capability names both the destination and the right to send to it.
using Cap = std::uintptr_t;
int crossings = 0;
// The fast path: send and wait for a reply in ONE system call, so a
// round trip costs one kernel entry rather than two.
void ipc_call(Cap dest, Message& m) {
    crossings += 2; // sender -> kernel -> receiver
    m.w[0] = 0; // the reply lands back in a register
}
int main() {
    Message m;
    ipc_call(1, m); // app  -> file system
    ipc_call(2, m); // fs   -> disk driver
    crossings += 1; // the IRQ arrives as a message too
    ipc_call(3, m); // driver -> fs reply
    crossings += 1; // fs   -> app reply
    std::cout << "microkernel: " << crossings << " crossings\n"
    << "monolithic:  2 crossings\n"
    << "Mach IPC ~115us made this fatal; L4 at ~5us made it payable\n";
}
// The microkernel contract expressed as interfaces. Each implementation
// is a SEPARATE PROCESS; every call below is a message, not a jump.
interface Kernel {
    void send(Cap dest, Message m); // address spaces
    Message receive(); // threads + scheduling
    Cap grant(Cap c, Task to); // capability transfer
}
// that is the whole kernel -- ~10k lines in seL4
interface FileSystem {   // user process
    byte[] read(int fd, int count);
}
interface DiskDriver {   // user process, no privilege
    byte[] readBlock(long lba);
}
// Fault containment is the payoff and it is structural, not a policy:
// DiskDriver runs in its own address space with no ability to write
// kernel memory. A null dereference kills one process. A supervisor
// restarts it. MINIX 3 demonstrated driver restarts that running
// applications never observed.
Watch it run

Step through it

Running on read(fd, buf, 4096) on a microkernel vs the same call on a monolithic kernel

Output
Read all 14 Steps
  1. the monolithic path, for comparison On Linux the application calls read(). One transition into kernel mode, then VFS calls the filesystem which calls the block driver — all ordinary function calls inside one address space, sharing data by pointer. One transition back. Two protection-domain crossings for the whole operation.
  2. the same call on a microkernel — crossing 1 The application sends an IPC message to the filesystem server and blocks. That is a crossing: application to kernel. The kernel does not implement read — it only routes the message to the destination the capability names.
  3. crossing 2 — kernel delivers to the FS server The kernel transfers the message into the filesystem server's address space and schedules it. Second crossing. The FS server is an ordinary user process with no more privilege than the application that called it.
  4. the FS server cannot touch the disk itself It resolves the inode to a block number and then hits its limit: it has no capability for the disk controller's I/O ports. In a monolithic kernel this would be a function call into the block layer. Here it must send another message.
  5. crossings 3 and 4 — FS to driver The filesystem sends to the disk driver: one crossing out to the kernel, one more in to the driver process. Four so far, and no data has moved yet. This is the structural cost the design accepts.
  6. the driver programs the hardware The disk driver holds the capability for these specific I/O ports and nothing else. It programs a DMA transfer and blocks waiting for the interrupt. Note what it cannot do: touch kernel memory, touch another driver's device, or take the system down.
  7. the interrupt arrives as a message The kernel receives the hardware interrupt and converts it into an IPC message delivered to the driver, because the driver is a user process and cannot install a hardware handler. This is another crossing, and it is the cost of drivers not running privileged.
  8. crossings 6 and 7 — the data comes back The driver replies to the filesystem with the block. Out to the kernel, in to the FS server. Seven crossings, and the data is one hop from home.
  9. crossing 8 — the reply reaches the application The filesystem replies to the application. Eight protection-domain crossings against the monolithic kernel's two. If each crossing cost 115 microseconds — Mach's number on 1990s hardware — this single read would cost nearly a millisecond in overhead alone. That is what killed the first generation.
  10. what L4 changed Liedtke's L4 in 1993 attacked the constant factor rather than the count. Short messages travel in registers rather than memory buffers. The fast path skips capability checks that can be proven unnecessary. The whole IPC path was hand-tuned to the actual cache and TLB behaviour of the target CPU. Result: roughly 5 microseconds instead of 115 — about 20x.
  11. what the crossings buy — the driver faults Now the payoff. Suppose the disk driver dereferences a null pointer. In a monolithic kernel that instruction executes with full privilege inside the kernel's address space: kernel panic, machine down, possibly filesystem corruption on the way out.
  12. on the microkernel the driver simply dies The driver is a process. A null dereference is a page fault in its own address space, and the kernel kills it exactly as it would kill any misbehaving program. The filesystem server, the application, and the kernel are untouched — none of them shared an address space with it.
  13. a supervisor restarts it A reincarnation server notices the driver died and starts a fresh copy, which re-acquires its capabilities and resumes serving requests. MINIX 3 demonstrated exactly this: driver restarts that running applications never observed. The in-flight request fails and is retried; nothing else notices.
  14. the honest trade Eight crossings against two is a real cost and no amount of optimisation removes it — it is structural, not an implementation defect. What the microkernel buys is that the trusted computing base shrinks from tens of millions of lines to roughly ten thousand, and that a fault in any service is contained to that service. seL4 shows where this leads: a machine-checked mathematical proof that the implementation matches its specification, which is only tractable because the kernel is small enough to verify. The deployments follow the logic exactly — QNX in over 200 million vehicles, seL4 in defence systems, L4 variants in smartphone basebands, MINIX on the Intel Management Engine. Microkernels dominate where a crash is unacceptable and the workload is known. Monolithic kernels dominate general-purpose computing, where the workload is unknown and throughput is what users feel.
4

Where They Actually Run

Microkernel examples are far more common than desktop users assume. QNX, a true microkernel, runs in over 200 million vehicles. seL4 ships in defence and secure-communications hardware. L4 variants run inside the baseband processor of a large share of the world's smartphones — the radio firmware, not the application OS. Google's Fuchsia is built on the Zircon microkernel.

MINIX 3 has the strangest deployment of all: a variant runs on the Intel Management Engine present in most modern Intel chipsets, which by some counts makes it the most widely deployed operating system on earth, running below the OS the user actually installed.

The pattern is consistent. Microkernels dominate where a crash is unacceptable and the workload is known — cars, avionics, basebands, security hardware. Monolithic kernels dominate general-purpose computing where the workload is unknown and throughput is the metric users feel.

  • QNX ships in over 200 million vehicles
  • seL4 in defence and secure communications
  • L4 variants inside smartphone basebands
  • MINIX runs on the Intel Management Engine