The Linux 2.2 kernel runs drivers, filesystems and the network stack in the same protected address space as the rest of the kernel, and at the same time it loads and unloads that code while the machine is running. The two things look as if they clash: the first is the definition of a monolithic kernel, the second is what microkernel projects claimed as their own. I want to look closely at how the two hold together, because much of why Linux ended up on internet-connected servers, rather than staying a university exercise, sits right there.
The 1992 argument
The difference between monolithic kernels and microkernels was already public material when Linux was only a few months old. On 29 January 1992 Andrew Tanenbaum, author of MINIX, opened a thread on comp.os.minix titled “LINUX is obsolete”. The basic objection was one of design: a monolithic kernel, where every subsystem shares a single area of memory, was held to be a backward choice next to a microkernel, where drivers, filesystems and memory management run as separate processes in user space and talk to one another by passing messages. Tanenbaum added that Linux was too tightly bound to the 386 family to have a future.
Torvalds replied by granting that the microkernel was preferable “from a theoretical and aesthetical” standpoint, but defended the monolith on two concrete fronts: MINIX had design flaws of its own, and Linux existed and was usable now. Theodore Ts’o and David Miller, who would later be among the kernel maintainers, also took part in the exchange.
The point I keep coming back to is that the argument did not close with a winner on architecture. It closed, in practice, with a third position, and that is the one the 2.2 kernel embodies in its current form.
Everything in kernel space, but separable
In Linux 2.2 the kernel is a single binary running in the processor’s privileged mode. A network driver, a filesystem’s code and the scheduler share one address space and call into one another with ordinary function calls, not by passing messages. That is the monolithic part, and it carries a definite cost: a fault in any piece of code in kernel space can corrupt the data structures of any other piece. There is no isolation.
The modular part is the loadable kernel modules (LKM). A module is a separately compiled object that gets linked into the running kernel with insmod and removed with rmmod. Once loaded, the module’s code runs exactly as if it had been compiled into the kernel: same address space, same privileges. Here modularity is not isolation; it is deferred dynamic linking.
Automatic loading sits on top of this. When the kernel needs a feature it does not have to hand — mounting a filesystem never used before, bringing up a network card — it can launch modprobe, which in turn calls insmod to insert the missing module. In 2.2 this is handled by kmod, a thread internal to the kernel, which took the place of the kerneld daemon of the 2.0 series: there the request travelled out to a user-space process by way of inter-process communication (IPC). The practical difference is that with kmod the request stays inside the kernel and does not depend on an external user process being permanently alive. The mechanism is documented in Documentation/kmod.txt in the sources.
What modules solve and what they do not
Better to say plainly how far modules go and where they stop, because it is easy to mistake them for the benefits of a microkernel.
What they give is practical management. A distribution can ship a minimal kernel plus a few hundred modules and load only the ones the present hardware needs. The driver for an exotic SCSI controller takes no memory on a machine that does not have one. Someone developing a driver can recompile and reload only that module instead of rebuilding and rebooting the whole kernel, and the work cycle shortens considerably. And a hardware vendor can distribute a driver as a module without having to land it in the official kernel sources.
What they do not give is fault containment. A faulty module panics the machine exactly as statically compiled code could. There is no protection boundary of the kind that, in a microkernel, lets a user-space driver crash while the rest of the system stays up. This was the property Tanenbaum placed at the centre, and Linux gave it up in exchange for the simplicity and speed of direct calls.
The rigorous alternative existed and exists. The GNU Hurd project builds an operating system as a set of user-space servers on top of the Mach microkernel; in November 1999 Roland McGrath presented the oskit-mach variant of GNU Mach precisely to improve its hardware support. I cite it as a yardstick: the microkernel road was being actively travelled in the same period, and what held it back was the driver problem, not the elegance of the design.
What changes for people running servers
Anyone running Linux in production carries a discipline about code in kernel space. Every loaded module is trusted code with full privileges: an insmod of a binary of doubtful provenance means handing it control of the machine. The modprobe/insmod chain, and the paths modules are read from, need checking with the same care as any other privileged part of the system.
The same reasoning explains a recurring choice: for subsystems that are always active on a dedicated machine — the server’s network driver, the root filesystem — many administrators prefer to compile statically into the kernel and keep modules for variable or rarely used hardware. The memory saving from modules is marginal when a function is always needed, and a more closed kernel presents a smaller dynamic-loading surface.
Limits of this reading
I am describing the 2.2 series, which is the one in production today. The 2.4 series, by now well into development, redraws important parts — the netfilter network subsystem, journaling, symmetric multi-processing (SMP) scalability — and with them details of how modules fit in with the rest will change. The comparison with the microkernel stays at the level of architecture: I have not measured the IPC overhead of a microkernel against Linux’s direct calls, and the figures published in the literature change a great deal with the workload and with how the message passing is built. Anyone wanting a quantitative view has to look at specific benchmarks, not at theory.
What is verifiable, and what I wanted to pin down, is the shape of the choice: Linux did not settle the 1992 argument by siding with one of the two camps. It moved the question from the plane of isolation to the plane of dynamic linking.
- https://groups.google.com/g/comp.os.minix/c/wlhw16QWltI
- https://en.wikipedia.org/wiki/Tanenbaum%E2%80%93Torvalds_debate
- https://www.kernel.org/
- https://kernel.googlesource.com/pub/scm/linux/kernel/git/ralf/linux/+/linux-2.2.9/Documentation/kmod.txt
- https://www.gnu.org/software/hurd/microkernel/mach/history.html
- https://www.noze.it/en/insights/linux-kernel/
Cover image: Tux, the Linux penguin mascot: a stylized sitting penguin with a black body and white belly, with a yellow beak and feet — illustration by Larry Ewing, Simon Budig, Garrett LeSage, CC0 — https://commons.wikimedia.org/wiki/File:Tux.svg