Kapitelübersicht
  1. 1 Einführung
  2. 2 Architektur-Überblick
  3. 3 Der Bootvorgang
  4. 4 Systemaufrufe
  5. 5 Prozesse & Threads
  6. 6 Der Scheduler
  7. 7 Speicherverwaltung
  8. 8 Interrupts & verzögerte Arbeit
  9. 9 Synchronisation
  10. 10 VFS & Dateisysteme
  11. 11 Block-I/O
  12. 12 Gerätetreiber
  13. 13 Netzwerk-Stack
  14. 14 Interprozesskommunikation
  15. 15 Sicherheit & Isolation
  16. 16 eBPF
  17. 17 Kernel-Entwicklung

Kapitel 2

Architektur-Überblick

Wie die CPU Kernel und Anwendungen voneinander trennt, wie der Adressraum aufgeteilt ist, auf welchen Wegen man in den Kernel gelangt – und aus welchen Subsystemen der Kernel besteht.

Kurz gesagt

Die CPU kennt zwei Betriebsarten. Anwendungen laufen eingeschränkt im Benutzermodus, der Kernel mit allen Rechten im Kernelmodus. In den Kernel gelangt man nur über drei Türen. Ein Programm kann ihn gezielt aufrufen (Systemaufruf), die CPU kann auf einen Fehler stoßen (Ausnahme), oder ein Gerät meldet sich (Interrupt). Im Kernel arbeiten spezialisierte Subsysteme zusammen, etwa für Prozesse, Speicher, Dateien, Netzwerk und Treiber.

Nach diesem Kapitel …

  • erklären, wie Privilegstufen und Seitentabellen Kernel und Anwendungen voneinander schützen
  • den virtuellen Adressraum eines Prozesses auf x86-64 skizzieren
  • Systemaufrufe, Ausnahmen und Interrupts als Einstiegspunkte in den Kernel unterscheiden
  • die wichtigsten Subsysteme und ihr Zusammenspiel an einem Beispiel nachvollziehen
  • Besonderheiten der Programmierung im Kernel benennen

Zwei Welten: Benutzermodus und Kernelmodus

Damit der Kernel Programme voneinander und vor sich selbst schützen kann, braucht er Hilfe von der Hardware. Jede moderne CPU kennt mindestens zwei Betriebsarten:

Privilegstufen auf x86 0 1 2 3 Ring 0 Kernel Ring 1 & 2 von Linux ungenutzt Ring 3 Anwendungen
x86-CPUs kennen vier Privilegstufen (Ringe). Linux nutzt nur zwei: Ring 0 für den Kernel und Ring 3 für alle Anwendungen.

Auf ARM64 heißen die Privilegstufen Exception Levels: Anwendungen laufen in EL0, der Kernel in EL1, ein Hypervisor in EL2 und Firmware in EL3. RISC-V unterscheidet entsprechend User, Supervisor und Machine Mode.

Privilegiert sind alle Befehle, mit denen sich die Schutzmechanismen aushebeln ließen, zum Beispiel:

  • cli/sti: Interrupts sperren und freigeben, sonst könnte ein Programm den Scheduler blockieren,
  • mov nach CR3: auf andere Seitentabellen umschalten, also in fremde Adressräume wechseln,
  • wrmsr: modellspezifische Register beschreiben, etwa die Einsprungadresse für Systemaufrufe,
  • hlt: den Prozessor anhalten.

Versucht ein Programm im Benutzermodus einen solchen Befehl, löst die CPU eine Ausnahme aus (auf x86 eine General Protection Fault). Der Kernel übernimmt und beendet das Programm in der Regel mit dem Signal SIGSEGV.

Speicherschutz über Seitentabellen

Der zweite Baustein ist die MMUMMU (Memory Management Unit)Hardware-Baustein der CPU, der virtuelle in physische Adressen übersetzt und dabei Zugriffsrechte prüft.. Jeder Eintrag in den Seitentabellen enthält neben der physischen Adresse einige Bits mit Zugriffsrechten, darunter das User/Supervisor-Bit. Seiten des Kernels sind als „nur Supervisor“ markiert. Greift Code im Benutzermodus darauf zu, meldet die MMU einen Seitenfehler, und der Zugriff scheitert, obwohl die Seite im Adressraum eingeblendet ist.

Der virtuelle Adressraum

Jeder Prozess sieht einen eigenen virtuellen Adressraum. Die MMU übersetzt jede Adresse anhand der Seitentabellen des Prozesses in eine physische Adresse. Wie das genau funktioniert, behandelt Kapitel 7. Hier interessiert zunächst die grobe Aufteilung:

Virtueller Adressraum eines Prozesses auf x86-64 0xffffffffffffffff0xffff8000000000000x00007fffffffffff0x0000000000000000 Kernel-Image & Module vmalloc-Bereich Direkte Abbildung des RAM Stack ↓ mmap, Bibliotheken Heap ↑ Daten, BSS Programmcode (.text) Kernel Space 128 TiB · in allen Prozessen identisch Nicht-kanonische Adressen Zugriff löst eine Ausnahme aus User Space 128 TiB · für jeden Prozess eigen
Mit 4-stufigen Seitentabellen sind 48 Bit der Adresse nutzbar: Die untere Hälfte gehört dem Prozess, die obere dem Kernel. Dazwischen liegt ein riesiger Bereich ungültiger („nicht-kanonischer“) Adressen.
  • Die untere Hälfte (User Space) ist für jeden Prozess anders. Dort liegen Programmcode, Daten, Heap, eingeblendete Bibliotheken und der Stack.
  • Die obere Hälfte (Kernel Space) ist in allen Prozessen gleich eingeblendet. Deshalb muss der Kernel bei einem Systemaufruf nicht den Adressraum wechseln und kann direkt auf den Puffer des aufrufenden Prozesses zugreifen, solange PTI nicht aktiv ist.
  • Ein Teil des Kernel-Adressraums bildet den gesamten physischen Arbeitsspeicher linear ab (direct map). So erreicht der Kernel jede physische Seite über eine einfache Adressrechnung.
Tiefer eintauchenKanonische Adressen und 5-stufige Seitentabellen

Obwohl Register auf x86-64 64 Bit breit sind, nutzen CPUs mit 4-stufigen Seitentabellen nur 48 Bit für virtuelle Adressen. Die Bits 63 bis 48 müssen eine Kopie von Bit 47 sein. Adressen, die diese Regel erfüllen, heißen kanonisch. Damit entstehen zwei gültige Bereiche, die untere und die obere Hälfte, und dazwischen eine riesige Lücke. Ein Zugriff auf eine nicht-kanonische Adresse löst sofort eine Ausnahme aus.

Neuere Prozessoren unterstützen 5-stufige Seitentabellen (LA57) mit 57 Bit virtuellen Adressen. Der User Space wächst dann von 128 TiB auf 64 PiB. Linux aktiviert diesen Modus nur, wenn die CPU ihn unterstützt und der Kernel entsprechend konfiguriert ist.

Drei Wege in den Kernel

Der Kernel ist kein Prozess, der ständig nebenher läuft. Er wird aktiv, wenn etwas passiert: Die CPU unterbricht den laufenden Code und springt an eine vom Kernel festgelegte Adresse. Dafür gibt es genau drei Anlässe:

SystemaufrufAusnahme (Exception)Interrupt
AuslöserProgramm bittet bewusst um einen Dienstder gerade ausgeführte Befehlein Gerät oder ein Timer
Zeitpunktsynchron, gewolltsynchron, meist ungewolltasynchron, jederzeit
Beispieleread(), fork(), mmap()Seitenfehler, Division durch null, ungültiger BefehlNetzwerkpaket angekommen, Timer abgelaufen, Taste gedrückt
Mehr dazuKapitel 4Kapitel 7 und Kapitel 8Kapitel 8

Nicht jede AusnahmeAusnahme (Exception)Von der CPU selbst ausgelöste Unterbrechung als Folge des gerade ausgeführten Befehls, etwa ein Seitenfehler oder eine Division durch null. ist ein Fehler. Ein Seitenfehler ist meist völlig normal: Er zeigt dem Kernel, dass eine Seite erst jetzt tatsächlich gebraucht wird, etwa weil sie noch von der Festplatte geladen oder erstmals mit Speicher hinterlegt werden muss. Dieses „verzögerte“ Bereitstellen von Speicher (demand paging) ist ein zentrales Prinzip der Speicherverwaltung.

Prozesskontext und Interruptkontext

Für die Kernel-Programmierung ist entscheidend, in wessen Auftrag Kernel-Code gerade läuft:

  • Im Prozesskontext arbeitet der Kernel für einen bestimmten Task, etwa während eines Systemaufrufs. Das Makro current zeigt auf dessen task_struct. Der Code darf schlafen, also blockieren und warten, zum Beispiel auf Daten von der Festplatte.
  • Im Interruptkontext reagiert der Kernel auf ein Hardware-Ereignis. Der unterbrochene Prozess hat damit nichts zu tun. Hier darf der Code niemals schlafen, denn es gibt keinen Task, den der Scheduler stattdessen schlafen legen könnte.

Die Subsysteme des Kernels

Intern ist der Kernel in Subsysteme gegliedert, die jeweils einen klar umrissenen Aufgabenbereich haben und meist einem eigenen Verzeichnis im Quellcode entsprechen. Jedes Subsystem hat eigene Maintainer, die Änderungen prüfen und an Linus Torvalds weiterreichen.

Die Subsysteme des Linux-Kernels User Space Anwendungen Shell & Dienste C-Bibliothek Kernel Space Systemaufruf-Schnittstelle Prozesse &Scheduler kernel/ Speicher-verwaltung mm/ VFS &Dateisysteme fs/ Netzwerk-Stack net/ IPC ipc/ Block-Schicht Sicherheit (LSM) Gerätetreiber Architekturcode: Einsprünge · Interrupts · Kontextwechsel · Seitentabellen · Boot Hardware CPU RAM Datenträger Netzwerkkarte Peripherie

Systemaufruf-Schnittstelle

Der einzige reguläre Weg aus dem User Space in den Kernel. Einige hundert Systemaufrufe bilden die stabile Schnittstelle zu allen Anwendungen – Linus Torvalds’ oberste Regel lautet: „We don’t break user space.“

Quellverzeichnis: arch/*/entry/, kernel/sys.c Zum Kapitel 4: Systemaufrufe →

Prozessverwaltung & Scheduler

Erzeugt und beendet Prozesse und Threads (beide sind für den Kernel „Tasks“) und entscheidet, welcher Task wann auf welcher CPU läuft. Zentrale Datenstruktur ist struct task_struct.

Quellverzeichnis: kernel/, kernel/sched/ Zum Kapitel 5: Prozesse & Threads →

Speicherverwaltung

Verwaltet den physischen Arbeitsspeicher in Seiten (meist 4 KiB), richtet für jeden Prozess einen virtuellen Adressraum ein, behandelt Seitenfehler und hält den Page Cache, der Dateiinhalte im RAM zwischenspeichert.

Quellverzeichnis: mm/ Zum Kapitel 7: Speicherverwaltung →

Virtuelles Dateisystem (VFS)

Eine einheitliche Abstraktionsschicht über allen Dateisystemen. open(), read() und write() funktionieren dadurch gleich – egal ob die Datei auf ext4, Btrfs, NFS oder in einem virtuellen Dateisystem wie /proc liegt.

Quellverzeichnis: fs/ Kapitel 10: VFS & Dateisysteme (in Vorbereitung)

Netzwerk-Stack

Implementiert Sockets und die Protokolle von Ethernet über IP bis TCP/UDP, dazu Routing, Paketfilterung (Netfilter) und schnelle Paketverarbeitung mit XDP.

Quellverzeichnis: net/ Kapitel 13: Netzwerk-Stack (in Vorbereitung)

Interprozesskommunikation

Mechanismen, mit denen Prozesse Daten austauschen oder sich synchronisieren: Pipes, Signale, System-V- und POSIX-IPC, Shared Memory und Futexe.

Quellverzeichnis: ipc/, kernel/signal.c, kernel/futex/ Kapitel 14: Interprozesskommunikation (in Vorbereitung)

Block-Schicht

Vermittelt zwischen Dateisystemen und Blockgeräten wie SSDs oder Festplatten. Sie sammelt, sortiert und verteilt I/O-Anfragen (blk-mq) auf die Hardware-Warteschlangen.

Quellverzeichnis: block/ Kapitel 11: Block-I/O (in Vorbereitung)

Sicherheit (LSM)

Das Linux Security Module Framework setzt an vielen Stellen im Kernel Prüfpunkte (Hooks). Module wie SELinux, AppArmor oder Landlock entscheiden dort zusätzlich zu den klassischen Dateirechten über Zugriffe.

Quellverzeichnis: security/ Kapitel 15: Sicherheit & Isolation (in Vorbereitung)

Gerätetreiber

Der mit Abstand größte Teil des Quellcodes. Treiber steuern konkrete Hardware an und melden sich über das Gerätemodell bei den passenden Subsystemen (Block, Netzwerk, Eingabe, Grafik …) an. Viele lassen sich als Module zur Laufzeit laden.

Quellverzeichnis: drivers/ Kapitel 12: Gerätetreiber (in Vorbereitung)

Architekturspezifischer Code

Alles, was von der CPU-Architektur abhängt: Einsprungpunkte für Systemaufrufe und Interrupts, Kontextwechsel, Aufbau der Seitentabellen, früher Bootcode. Linux unterstützt rund 20 Architekturen, darunter x86, ARM64 und RISC-V.

Quellverzeichnis: arch/ Kapitel 8: Interrupts & verzögerte Arbeit (in Vorbereitung)

Vereinfachtes Blockschaltbild. Die Farben werden auf der gesamten Seite einheitlich für die jeweiligen Subsysteme verwendet.

Zusammenspiel am Beispiel: cat notizen.txt

Ein einfacher Befehl in der Shell zeigt, wie eng die Subsysteme zusammenarbeiten:

  1. Prozessverwaltung: Die Shell erzeugt mit fork() (genauer clone()) einen neuen Prozess, der mit execve() das Programm /usr/bin/cat lädt (Kapitel 5).
  2. Speicherverwaltung: execve() baut einen neuen Adressraum auf. Programmcode und Bibliotheken werden nur eingeblendet. Erst beim ersten Zugriff lösen Seitenfehler das tatsächliche Laden aus.
  3. VFS: cat ruft openat() auf. Das VFS zerlegt den Pfad, findet die Datei im zuständigen Dateisystem (etwa ext4) und prüft die Zugriffsrechte, zusätzlich auch über die LSM-Hooks.
  4. Page Cache: Bei read() schaut der Kernel zuerst im Page CachePage CacheTeil des Arbeitsspeichers, in dem der Kernel Dateiinhalte zwischenspeichert, um Zugriffe auf Datenträger zu vermeiden. nach. Liegt die Datei schon im RAM, ist der Aufruf in Mikrosekunden erledigt.
  5. Block-Schicht und Treiber: Andernfalls erzeugt das Dateisystem eine I/O-Anfrage. Die Block-Schicht reicht sie an den NVMe-Treiber weiter, und der Prozess schläft.
  6. Interrupt und Scheduler: Sind die Daten da, meldet die SSD dies per Interrupt. Der Kernel markiert den Prozess als lauffähig, und der Scheduler lässt ihn wieder rechnen.
  7. Ausgabe: cat schreibt mit write() auf die Standardausgabe. Das geht über das Terminal-Subsystem (TTY) an das Terminalprogramm.

Dieser Ablauf berührt fast jedes Subsystem. Die folgenden Kapitel nehmen sie einzeln unter die Lupe.

Monolithisch, aber modular: Kernel-Module

Viele Teile des Kernels lassen sich wahlweise fest einbauen oder als ladbares ModulKernel-ModulZur Laufzeit ladbarer Teil des Kernels (Datei mit Endung .ko), meist ein Gerätetreiber oder Dateisystem. (.ko-Datei) übersetzen. Das legt die Kernel-Konfiguration fest: y für fest eingebaut, m für Modul, n für gar nicht. Distributionen bauen die meisten Treiber als Module, damit derselbe Kernel auf sehr unterschiedlicher Hardware läuft, ohne alle Treiber im Speicher zu halten.

Ein minimales Modul sieht so aus:

#include <linux/init.h>
#include <linux/module.h>

static int __init hello_init(void)
{
	pr_info("Hallo, Kernel!\n");
	return 0;            /* 0 = erfolgreich geladen */
}

static void __exit hello_exit(void)
{
	pr_info("Tschüss, Kernel!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Minimales Beispielmodul");

Geladen wird ein Modul mit modprobe, das auch Abhängigkeiten zu anderen Modulen auflöst. pr_info() schreibt in den Kernel-Log, den dmesg anzeigt. Wie man ein solches Modul übersetzt, zeigt Kapitel 17.

Tiefer eintauchenWarum es keine stabile interne Kernel-API gibt

Die Schnittstelle zum User Space ist bei Linux heilig: Einmal eingeführte Systemaufrufe bleiben stabil. Die internen Schnittstellen zwischen Kernel und Modulen ändern sich dagegen ständig. Funktionen werden umbenannt, Strukturen umgebaut, Parameter ergänzt.

Das ist Absicht und in der Kernel-Dokumentation (Documentation/process/stable-api-nonsense.rst) ausführlich begründet. Eine eingefrorene interne API würde Verbesserungen und Sicherheitskorrekturen bremsen. Wer einen Treiber in den offiziellen Kernel aufnehmen lässt, bekommt solche Anpassungen automatisch: Wer eine interne Schnittstelle ändert, muss alle Nutzer im Quellbaum mit anpassen. Treiber außerhalb des Kernels müssen dagegen ständig nachziehen.

Proprietäre Module ohne GPL-kompatible Lizenz sind technisch möglich, „verunreinigen“ aber den Kernel. Er wird als tainted markiert (/proc/sys/kernel/tainted), und viele interne Funktionen (EXPORT_SYMBOL_GPL) stehen ihnen nicht zur Verfügung.

Kernel-Threads

Nicht alle Arbeit im Kernel geschieht im Auftrag eines Benutzerprogramms. Für Hintergrundaufgaben startet der Kernel eigene Kernel-ThreadsKernel-ThreadTask, der ausschließlich im Kernelmodus läuft und keinen eigenen User-Space-Adressraum hat. In ps erscheint er in eckigen Klammern, z. B. [kworker/0:1].. Sie laufen ausschließlich im Kernelmodus und haben keinen eigenen User-Space-Adressraum. Einige Beispiele:

ThreadAufgabe
kthreadd (PID 2)erzeugt alle anderen Kernel-Threads
ksoftirqd/Nerledigt aufgestaute Softirq-Arbeit auf CPU N (Kapitel 8)
kworker/…arbeitet Aufträge aus Workqueues ab
migration/Nverschiebt Tasks zwischen CPUs
kswapd0gibt bei Speicherknappheit Seiten frei (Kapitel 7)
rcu_…Hilfsthreads für den RCU-Mechanismus (Kapitel 9)

Ein Blick in den Quellbaum

Die Gliederung in Subsysteme spiegelt sich direkt in der Verzeichnisstruktur des Quellcodes wider:

VerzeichnisInhalt
arch/architekturspezifischer Code (x86, arm64, riscv …)
kernel/Kernfunktionen: Prozesse, Scheduler (kernel/sched/), Signale, Timer
mm/Speicherverwaltung
fs/VFS und alle Dateisysteme
block/Block-Schicht
net/Netzwerk-Stack
drivers/Gerätetreiber, der mit Abstand größte Teil
ipc/System-V- und POSIX-IPC
security/LSM-Framework, SELinux, AppArmor, Landlock …
init/Start des Kernels (start_kernel() in init/main.c)
include/gemeinsame Header-Dateien
lib/Hilfsfunktionen (Strings, Listen, Kompression …)
rust/Rust-Unterstützung und -Abstraktionen
Documentation/die offizielle Dokumentation

Besonderheiten der Kernel-Programmierung

Kernel-Code ist C (seit Linux 5.18 im Standard GNU C11) und zunehmend auch Rust. Er unterscheidet sich aber deutlich von gewöhnlicher Anwendungsprogrammierung:

  • Keine C-Standardbibliothek. Es gibt kein printf() und kein malloc(). Der Kernel bringt eigene Gegenstücke mit, etwa printk()/pr_info() und kmalloc().
  • Kleiner, fester Stack. Jeder Task hat im Kernel nur einen kleinen Stack (auf x86-64 16 KiB). Große lokale Arrays und tiefe Rekursion verbieten sich.
  • Keine Gleitkommazahlen. Der Kernel sichert die FPU-Register bei Systemaufrufen nicht automatisch. Gleitkomma-Arithmetik ist deshalb nur in speziell geschützten Abschnitten erlaubt.
  • Kein Speicherschutz vor sich selbst. Ein ungültiger Zeigerzugriff führt nicht zu einem sauberen Programmabbruch, sondern zu einem Oops oder einer Kernel Panic.
  • Nebenläufigkeit überall. Auf mehreren CPUs gleichzeitig, unterbrochen von Interrupts und verdrängt vom Scheduler: Fast jede gemeinsam genutzte Datenstruktur braucht Synchronisation (Kapitel 9).

Selbst ausprobieren: Den Kernel bei der Arbeit beobachten

# Adressraum der eigenen Shell: Programmcode, Heap, Bibliotheken, Stack, vDSO
cat /proc/$$/maps

# Kernel-Threads: alle Kinder von kthreadd (PID 2)
ps -o pid,ppid,comm --ppid 2 | head -n 20

# Wie oft wurde welcher Interrupt auf welcher CPU ausgelöst?
head -n 15 /proc/interrupts

# Informationen zu einem Modul
modinfo ext4 | head -n 10

In der Ausgabe von /proc/$$/maps liegen alle Bereiche in der unteren Hälfte des Adressraums (Adressen wie 55… oder 7f…). Die einzige Ausnahme ist die historische Seite [vsyscall] ganz oben; den eigentlichen Kernel-Teil zeigt die Datei nicht an.