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 3

Der Bootvorgang

Was zwischen dem Druck auf den Einschaltknopf und dem Login-Bildschirm passiert – von der Firmware über den Bootloader und die Initialisierung des Kernels bis zum ersten Prozess.

Kurz gesagt

Beim Einschalten startet zuerst die Firmware des Mainboards. Sie lädt einen Bootloader, und der lädt den Kernel in den Arbeitsspeicher. Der Kernel entpackt sich, richtet Speicher, Scheduler und Treiber ein und startet dann sein erstes Programm, den Init-Prozess mit der Nummer 1. Bei den meisten Systemen kommt dabei vorher noch ein kleines Hilfsdateisystem im RAM zum Einsatz, die initramfs. Der Init-Prozess (meist systemd) startet schließlich alle weiteren Dienste.

Nach diesem Kapitel …

  • die Phasen des Bootvorgangs in der richtigen Reihenfolge benennen und ihre Aufgaben erklären
  • UEFI- und BIOS-Boot unterscheiden und die Rolle des Bootloaders beschreiben
  • nachvollziehen, wie start_kernel() die Subsysteme initialisiert und wie PID 1 und PID 2 entstehen
  • erklären, wozu die initramfs dient und wie der Wechsel auf das echte Root-Dateisystem funktioniert
  • Kernel-Parameter gezielt einsetzen und den Bootvorgang auf dem eigenen System analysieren

Überblick

Ein Computer, der gerade eingeschaltet wurde, „weiß“ nichts: Der Arbeitsspeicher ist leer, kein Treiber ist geladen, und die CPU führt einfach den Code an einer fest verdrahteten Adresse aus. Bis zum Login-Bildschirm übernimmt eine Kette von Programmen nacheinander die Kontrolle. Jedes davon lädt das nächste, etwas mächtigere Glied und tritt dann ab:

Die Phasen des Bootvorgangs Zeit → Firmware(UEFI) Bootloader(GRUB) Entpacken &früher Start start_kernel() kernel_init(PID 1) initramfs/init systemd Vor dem Kernel Kernel User Space

1. Firmware: UEFI (oder BIOS)

Nach dem Einschalten startet die Firmware aus einem Flash-Chip auf dem Mainboard. Sie testet und initialisiert die Hardware, sucht anhand der im NVRAM gespeicherten Booteinträge die EFI-Systempartition (ESP) und startet von dort den Bootloader – bei aktivem Secure Boot nur, wenn dessen Signatur gültig ist.

Beobachten: efibootmgr -v

2. Bootloader: GRUB oder systemd-boot

Der Bootloader zeigt bei Bedarf ein Auswahlmenü, lädt das Kernel-Image (vmlinuz) und die initramfs in den Arbeitsspeicher und übergibt dem Kernel seine Kommandozeile. Dann springt er in den Kernel – ab hier hat der Bootloader keine Rolle mehr.

Im Quellcode: Documentation/arch/x86/boot.rst Beobachten: cat /proc/cmdline

3. Der Kernel entpackt sich und richtet die CPU ein

Das Kernel-Image ist komprimiert. Ein kleiner Vorspann entpackt den eigentlichen Kernel (bei KASLR an eine zufällige Adresse), richtet vorläufige Seitentabellen ein und springt in den architekturspezifischen Startcode, der schließlich start_kernel() aufruft.

Im Quellcode: arch/x86/boot/compressed/, arch/x86/kernel/head_64.S Beobachten: dmesg | head

4. start_kernel(): der Kernel initialisiert sich

Die erste architekturunabhängige C-Funktion. Sie initialisiert nacheinander die Subsysteme: Speicherverwaltung, Scheduler, Interrupts, Timer, Konsole, VFS-Caches und vieles mehr. Am Ende erzeugt rest_init() die Tasks mit PID 1 (kernel_init) und PID 2 (kthreadd).

Im Quellcode: init/main.c Beobachten: dmesg | less

5. kernel_init: Treiber laden, Init starten

PID 1 ist zunächst noch ein Kernel-Thread. Er startet die übrigen CPUs, ruft alle fest eingebauten Initialisierungsfunktionen (Initcalls, darunter die Treiber) auf, entpackt die initramfs, gibt den nur zum Booten benötigten Speicher frei und führt schließlich /init als ersten User-Space-Prozess aus.

Im Quellcode: init/main.c (kernel_init) Beobachten: dmesg | grep -i 'freeing unused'

6. initramfs: das frühe User Space

Ein kleines Dateisystem im RAM mit genau den Werkzeugen und Modulen, die nötig sind, um das echte Root-Dateisystem einzuhängen: Speichertreiber laden, verschlüsselte Partitionen entsperren, RAID oder LVM zusammensetzen. Danach wechselt es per switch_root auf das echte Root-Dateisystem.

Im Quellcode: init/initramfs.c Beobachten: lsinitramfs /boot/initrd.img-$(uname -r) | head

7. Das Init-System: systemd

Das echte /sbin/init – heute fast immer systemd – übernimmt PID 1. Es hängt die übrigen Dateisysteme ein, startet Dienste parallel anhand ihrer Abhängigkeiten und bringt das System schließlich in den Zielzustand, etwa mit grafischer Anmeldung.

Beobachten: systemd-analyze critical-chain

Vom Einschalten bis zum Login durchläuft ein Linux-System sieben Phasen. Klicke auf eine Phase für Details.

Die folgenden Abschnitte gehen die Phasen der Reihe nach durch. Der Schwerpunkt liegt auf dem Teil, für den der Kernel selbst verantwortlich ist (Phasen 3 bis 5).

Firmware: UEFI und BIOS

Der erste Code nach dem Einschalten steckt in einem Flash-Speicher auf dem Mainboard, der Firmware. Sie prüft und initialisiert die Hardware (Power-On Self-Test) und richtet unter anderem den Arbeitsspeicher-Controller ein. Dann sucht sie ein Betriebssystem.

UEFIUEFIUnified Extensible Firmware Interface: moderne Firmware-Schnittstelle, die das klassische BIOS abgelöst hat. Startet Bootloader als EFI-Programme von einer FAT-formatierten EFI-Systempartition., der heutige Standard, kennt Dateisysteme. Die Firmware liest von einer kleinen, FAT-formatierten EFI-Systempartition (ESP, meist unter /boot/efi eingehängt) eine Programmdatei, etwa \EFI\ubuntu\shimx64.efi, und startet sie. Welche Datei das ist, steht in Booteinträgen im nichtflüchtigen Speicher der Firmware (NVRAM). Mit Secure Boot startet die Firmware nur Programme mit gültiger Signatur. Distributionen nutzen dafür meist das kleine, von Microsoft signierte Zwischenprogramm shim, das dann den eigentlichen, von der Distribution signierten Bootloader prüft.

Das klassische BIOS kannte dagegen keine Dateisysteme. Es lud einfach den ersten Sektor der Festplatte (den Master Boot Record, 512 Byte, davon nur 446 Byte für Code) und sprang hinein. Dieser winzige Code musste weitere Stufen des Bootloaders nachladen. Viele Firmwares können BIOS-Systeme noch nachbilden (Legacy-Modus, CSM), neue Rechner verzichten aber zunehmend darauf.

Der Bootloader

Ein Bootloader wie GRUB oder systemd-boot hat im Kern drei Aufgaben:

  1. das Kernel-Image laden, meist /boot/vmlinuz-<version>,
  2. die initramfs laden, meist /boot/initrd.img-<version>,
  3. dem Kernel seine Kommandozeile übergeben, etwa root=UUID=… ro quiet splash.

Danach springt er an den Einsprungpunkt des Kernels. Die Schnittstelle dafür, also wo die Daten im Speicher liegen und welche Register was enthalten, ist für jede Architektur festgelegt (für x86 in Documentation/arch/x86/boot.rst).

Tiefer eintauchenBooten ganz ohne Bootloader: EFI-Stub und Unified Kernel Images

Auf UEFI-Systemen ist ein separater Bootloader eigentlich überflüssig. Mit der Option CONFIG_EFI_STUB ist das Kernel-Image selbst ein gültiges EFI-Programm, das die Firmware direkt starten kann. Die Kommandozeile und die initramfs übergibt dann die Firmware bzw. ein Booteintrag.

Einen Schritt weiter gehen Unified Kernel Images (UKI): Kernel, initramfs, Kommandozeile und weitere Daten werden zu einer einzigen EFI-Datei zusammengefasst und gemeinsam signiert. So kann bei Secure Boot niemand unbemerkt die initramfs oder die Kernel-Parameter verändern. systemd-boot und mehrere Distributionen unterstützen diesen Ansatz.

Der Kernel entpackt sich selbst

Die Datei vmlinuz ist nicht der Kernel selbst, sondern ein komprimierter Kernel mit einem kleinen Vorspann (auf x86 das Format bzImage; das „z“ steht für zipped). Dieser Vorspann (arch/x86/boot/compressed/) läuft als Erstes:

  1. Er richtet einen vorläufigen Stack und einfache Seitentabellen ein.
  2. Er wählt bei aktivem KASLRKASLRKernel Address Space Layout Randomization: Der Kernel wird bei jedem Start an eine zufällige Adresse geladen, damit Angreifer die Lage von Kernel-Code nicht vorhersagen können. eine zufällige Zieladresse für den Kernel.
  3. Er entpackt den eigentlichen Kernel (vmlinux) dorthin. Je nach Konfiguration ist dieser mit gzip, xz, lz4 oder zstd komprimiert.
  4. Er springt in den architekturspezifischen Startcode, auf x86-64 startup_64 in arch/x86/kernel/head_64.S.

Dieser Startcode baut die endgültigen frühen Seitentabellen auf, lädt die Deskriptortabellen der CPU (GDT, IDT) und ruft über x86_64_start_kernel schließlich die erste architekturunabhängige C-Funktion auf: start_kernel().

start_kernel(): der Kernel initialisiert sich

start_kernel in init/main.c ist so etwas wie die main()-Funktion des Kernels. Sie ruft in einer genau festgelegten Reihenfolge die Initialisierungsfunktionen der Subsysteme auf. Die Reihenfolge ist entscheidend, denn jedes Subsystem darf nur auf bereits initialisierte Teile zurückgreifen:

/* init/main.c – stark gekürzt, Reihenfolge wie im Original */
asmlinkage __visible void __init start_kernel(void)
{
	char *command_line;

	setup_arch(&command_line);     /* Speicherkarte der Firmware, frühe Hardware */
	setup_per_cpu_areas();         /* Speicherbereiche je CPU */
	parse_early_param();           /* frühe Kernel-Parameter auswerten */
	trap_init();                   /* Ausnahmebehandlung (Seitenfehler …) */
	mm_core_init();                /* Seiten-Allokator, Slab-Allokator */
	sched_init();                  /* Scheduler, Run-Queues */
	rcu_init();                    /* RCU-Synchronisation */
	init_IRQ();                    /* Interrupt-Controller */
	init_timers();                 /* Timer */
	time_init();                   /* Zeitquellen */
	console_init();                /* Kernel-Meldungen auf die Konsole */
	vfs_caches_init();             /* Dentry- und Inode-Caches des VFS */
	/* … viele weitere … */

	rest_init();                   /* PID 1 und PID 2 erzeugen (je nach Version über einen kleinen Wrapper) */
}

Bis hierhin läuft auf dem System genau ein Ablauf auf genau einer CPU. Der Code, der das ausführt, wird später zum Idle-Task dieser CPU mit der PID 0, der immer dann läuft, wenn sonst nichts zu tun ist.

rest_init(): die ersten Tasks

Ganz am Ende erzeugt rest_init zwei neue Tasks:

  • PID 1 führt die Funktion kernel_init() aus. Aus diesem Kernel-Thread wird später der erste Prozess im User Space.
  • PID 2 ist kthreadd, der „Vater“ aller weiteren Kernel-Threads (siehe Kapitel 2).

Danach legt sich der ursprüngliche Ablauf als Idle-Task schlafen, und der Scheduler übernimmt. Ab jetzt gelten die normalen Regeln der Prozessverwaltung (Kapitel 5).

kernel_init(): von Treibern bis /init

PID 1 erledigt zunächst noch im Kernel einige Aufgaben:

  1. Weitere CPUs starten. Bisher lief nur die Boot-CPU; jetzt werden alle anderen Kerne hochgefahren.
  2. Initcalls ausführen. Alle fest eingebauten Treiber und Subsysteme registrieren sich über sogenannte Initcalls (siehe unten).
  3. initramfs entpacken. Das vom Bootloader geladene Archiv wird in ein Dateisystem im RAM (rootfs) entpackt.
  4. Speicher freigeben. Code und Daten, die mit __init markiert sind, werden nur zum Booten gebraucht. Ihr Speicher wird jetzt freigegeben, in dmesg erkennbar an der Meldung „Freeing unused kernel image memory“.
  5. Das erste Programm starten. Mit execve() wird /init aus der initramfs ausgeführt. Damit wechselt PID 1 zum ersten Mal in den Benutzermodus.

Gibt es keine initramfs, hängt der Kernel das mit root= angegebene Dateisystem selbst ein und versucht der Reihe nach /sbin/init, /etc/init, /bin/init und /bin/sh. Findet er nichts davon, endet der Bootvorgang mit der Meldung „Kernel panic – not syncing: No working init found.“

Tiefer eintauchenInitcalls: wie sich eingebaute Treiber anmelden

Fest eingebaute Treiber haben keine Funktion, die jemand ausdrücklich aufruft. Stattdessen legt das Makro module_init() bzw. eines der *_initcall()-Makros einen Zeiger auf die Initialisierungsfunktion in einem speziellen Abschnitt des Kernel-Images ab. Der Linker sammelt diese Zeiger, sortiert nach Ebenen, und do_initcalls() ruft sie der Reihe nach auf:

EbeneMakroBeispiele
0pure_initcallreine Variablen-Initialisierung
1core_initcallgrundlegende Infrastruktur
2postcore_initcallBus-Systeme
3arch_initcallarchitekturspezifische Geräte
4subsys_initcallSubsysteme wie PCI, USB-Kern, Netzwerk-Grundgerüst
5fs_initcallDateisysteme, Netzwerkprotokolle
–rootfs_initcallEntpacken der initramfs
6device_initcallGerätetreiber (module_init bei fest eingebautem Code)
7late_initcallalles, was zuletzt kommen muss

Mit dem Kernel-Parameter initcall_debug protokolliert der Kernel jeden Initcall mit seiner Laufzeit. Damit lassen sich langsame Treiber beim Booten aufspüren.

Wird derselbe Code als Modul gebaut, macht module_init() aus der Funktion stattdessen die Einsprungfunktion, die beim Laden des Moduls aufgerufen wird.

Die initramfs

Warum braucht es überhaupt ein Zwischenstadium im RAM? Weil der Kernel das echte Root-Dateisystem oft nicht ohne Hilfe erreichen kann:

  • Der Treiber für den Datenträger-Controller ist als Modul gebaut und liegt ausgerechnet auf dem Datenträger, der erst eingebunden werden soll.
  • Die Root-Partition ist mit LUKS verschlüsselt, und jemand muss eine Passphrase abfragen.
  • Das Dateisystem liegt auf LVM, einem Software-RAID oder im Netzwerk (NFS, iSCSI).

All das ist Logik, die man nicht in den Kernel einbauen möchte. Sie gehört in den User Space. Die initramfsinitramfsKleines Dateisystem-Archiv, das der Bootloader zusammen mit dem Kernel lädt. Der Kernel entpackt es in den RAM und startet daraus das erste Programm (/init), das das echte Root-Dateisystem vorbereitet. ist deshalb ein kleines Dateisystem-Archiv (im cpio-Format) mit genau den Programmen und Modulen, die für diese Aufgaben nötig sind. Werkzeuge wie dracut, mkinitcpio oder initramfs-tools erzeugen es passend zum installierten Kernel.

Von der initramfs zum echten Root-Dateisystem Arbeitsspeicher (rootfs) initramfs /init, Module, Werkzeuge Datenträger Echtes Root-Dateisystem ext4, Btrfs … ggf. in LUKS/LVM ① Treiber laden, entsperren ② einhängen unter /sysroot ③ switch_root → /sbin/init PID 1 bleibt erhalten
Die initramfs liegt als Archiv im RAM. Ihr /init bereitet das echte Root-Dateisystem vor, hängt es ein und ersetzt sich dann per switch_root durch das echte Init-System.

Der Wechsel am Ende ist bemerkenswert: switch_root löscht den Inhalt der initramfs, um RAM freizugeben, macht das echte Root-Dateisystem zum neuen / und ersetzt sich dann per execve() durch /sbin/init. Weil dabei kein neuer Prozess entsteht, bleibt die PID 1 erhalten. Für den Kernel ist es durchgehend derselbe Task.

PID 1: der besondere Prozess

Der Init-ProzessPID 1 (Init-Prozess)Der erste Prozess im User Space, heute meist systemd. Er ist Vorfahr aller anderen Prozesse und übernimmt verwaiste Prozesse. Endet er, löst der Kernel eine Panic aus. hat im System eine Sonderstellung, die der Kernel ausdrücklich durchsetzt:

  • Er darf nicht enden. Beendet sich PID 1 oder stürzt er ab, löst der Kernel eine Panic aus („Attempted to kill init!“), denn ohne ihn kann das System nicht sinnvoll weiterlaufen.
  • Er ist geschützt vor Signalen. Der Kernel stellt PID 1 nur Signale zu, für die der Prozess selbst einen Handler eingerichtet hat. Ein versehentliches kill -9 1 bleibt daher wirkungslos.
  • Er adoptiert Waisen. Endet ein Prozess, werden seine Kinder an PID 1 (oder einen anderen dafür vorgesehenen Prozess) weitergereicht. Init muss deren Exit-Status abholen, sonst bleiben Zombies zurück (Kapitel 5).

Heute ist das Init-System fast überall systemd. Es startet Dienste parallel anhand ihrer Abhängigkeiten, überwacht sie, startet sie bei Bedarf neu und erreicht schließlich ein Ziel (Target) wie multi-user.target oder graphical.target.

Kernel-Parameter

Über die Kommandozeile, die der Bootloader übergibt, lässt sich das Verhalten des Kernels beeinflussen, ohne ihn neu zu bauen. Einige wichtige Parameter:

ParameterWirkung
root=UUID=…Root-Dateisystem (bei initramfs wertet es meist deren /init aus)
ro / rwRoot-Dateisystem zunächst nur lesend bzw. schreibbar einhängen
quiet, loglevel=Nweniger bzw. gezielt eingestellte Kernel-Meldungen auf der Konsole
init=/bin/bashanderes Programm als Init starten (Notfall-Zugang)
rdinit=/pfadanderes Programm in der initramfs statt /init starten
console=ttyS0,115200Kernel-Meldungen auf die serielle Schnittstelle ausgeben
nokaslrKASLR abschalten, etwa für das Debugging
mitigations=offSchutzmaßnahmen gegen Spectre & Co. abschalten (schneller, aber unsicher)
systemd.unit=rescue.targetwird an systemd durchgereicht: im Rettungsmodus starten

Parameter, die der Kernel selbst nicht kennt, reicht er an den Init-Prozess weiter: solche mit = als Umgebungsvariablen, solche ohne als Kommandozeilenargumente. Parameter der Form modul.option=wert setzen Optionen fest eingebauter Module oder werden beim späteren Laden des Moduls angewendet.

Selbst ausprobieren

Selbst ausprobieren: Den eigenen Bootvorgang untersuchen

# Die ersten Meldungen des Kernels: Version, Kommandozeile, Speicherkarte
sudo dmesg | head -n 30

# Kernel-Meldungen des aktuellen Boots mit Zeitstempeln
journalctl -k -b | head -n 40

# Wie lange haben Firmware, Bootloader, Kernel und User Space gebraucht?
systemd-analyze

# Welche Dienste haben am längsten gebraucht, und was lag auf dem kritischen Pfad?
systemd-analyze blame | head
systemd-analyze critical-chain

# Kernel-Images und initramfs im Boot-Verzeichnis
ls -lh /boot

# Inhalt der initramfs (Debian/Ubuntu; Fedora: lsinitrd)
lsinitramfs /boot/initrd.img-$(uname -r) | head -n 30

# UEFI-Booteinträge (nur auf UEFI-Systemen)
efibootmgr -v

Die Ausgabe von systemd-analyze teilt die Startzeit in Abschnitte auf, etwa „firmware“, „loader“, „kernel“, „initrd“ und „userspace“. Das entspricht ziemlich genau den Phasen aus dem Diagramm oben.