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 8

Interrupts & verzögerte Arbeit

Wie Geräte den Prozessor unterbrechen, wie der Kernel darauf reagiert und warum er die eigentliche Arbeit auf Softirqs, Workqueues und Kernel-Threads verteilt – dazu Timer, Ticks und tickloser Betrieb.

Kurz gesagt

Geräte melden sich beim Prozessor, wenn etwas passiert ist, etwa wenn ein Netzwerkpaket ankommt oder eine Festplatte mit dem Lesen fertig ist. Der Prozessor unterbricht dann sofort, was er gerade tut, und springt in den Kernel. Weil diese Unterbrechung alles andere aufhält, erledigt der Kernel darin nur das Nötigste und verschiebt den Rest auf später. Auch die Zeitmessung und das regelmäßige Umschalten zwischen Programmen beruhen auf Interrupts eines Timers.

Nach diesem Kapitel …

  • den Weg eines Hardware-Interrupts vom Gerät bis zum Treiber-Handler beschreiben
  • Interrupt-Controller, Vektoren und MSI-X einordnen
  • das Prinzip von Top Half und Bottom Half erklären
  • Softirqs, Tasklets, Workqueues und Threaded IRQs vergleichen und passend auswählen
  • jiffies, hochauflösende Timer und tickloses Arbeiten verstehen
  • Interrupt-Statistiken und -Verteilung auf dem eigenen System untersuchen

Warum Interrupts?

Ein Gerät ist meist um Größenordnungen langsamer als die CPU. Der Kernel könnte natürlich immer wieder nachfragen, ob schon Daten da sind (Polling). Damit würde er aber Rechenzeit verschwenden und trotzdem verspätet reagieren. Stattdessen meldet sich das Gerät selbst, mit einem InterruptInterruptSignal eines Geräts (oder der CPU selbst), das den laufenden Code unterbricht, damit der Kernel ein Ereignis behandeln kann.. Die CPU kann in der Zwischenzeit andere Tasks ausführen oder Strom sparen.

In Kapitel 2 haben wir die drei Wege in den Kernel kennengelernt. Interrupts sind der einzige davon, der asynchron ist: Er kann zwischen zwei beliebigen Befehlen eintreffen, mitten in einem Benutzerprogramm und, sofern nicht gesperrt, auch mitten im Kernel. Neben Geräte-Interrupts gibt es:

  • Timer-Interrupts, die den Takt für Zeitmessung und Scheduler liefern,
  • Inter-Processor Interrupts (IPIs), mit denen CPUs sich gegenseitig benachrichtigen, etwa für TLB-Shootdowns (Kapitel 7) oder um einen Task auf einer anderen CPU aufzuwecken,
  • Nicht maskierbare Interrupts (NMI), die sich nicht sperren lassen. Linux nutzt sie für Hänger-Erkennung (Watchdog) und Leistungsmessung.

Interrupt-Controller

Zwischen den Geräten und den CPU-Kernen sitzt auf x86 ein System von APICs (Advanced Programmable Interrupt Controller):

Interrupt-Controller auf einem x86-Mehrkernsystem CPU 0 Local APIC + Timer CPU 1 Local APIC + Timer CPU 2 Local APIC + Timer CPU 3 Local APIC + Timer IPI PCIe / Systembus I/O-APIC Ältere Geräte Leitungen (INTx) NVMe-SSD MSI-X, eine je Queue Netzwerkkarte MSI-X, eine je Queue
Jede CPU hat einen eigenen Local APIC mit Timer. Ältere Geräte melden sich über den I/O-APIC; moderne PCIe-Geräte schicken MSI-X-Nachrichten direkt an einzelne CPUs – oft eine eigene pro Warteschlange. CPUs senden sich gegenseitig Inter-Processor Interrupts (IPIs).

Bei MSI und MSI-X (Message Signaled Interrupts) löst ein PCIe-Gerät einen Interrupt aus, indem es eine kleine Nachricht an eine bestimmte Speicheradresse schreibt. Das bringt drei Vorteile:

  • Es sind keine knappen Interrupt-Leitungen nötig.
  • Ein Gerät kann sehr viele getrennte Interrupts haben (MSI-X: bis zu 2048).
  • Jeder dieser Interrupts kann gezielt an eine bestimmte CPU gehen.

Eine moderne NVMe-SSD oder Netzwerkkarte hat deshalb oft eine Warteschlange pro CPU-Kern mit jeweils eigenem Interrupt. Jeder Kern bearbeitet dann „seine“ Anfragen, ohne mit anderen Kernen um Sperren konkurrieren zu müssen.

ARM-Systeme verwenden statt APICs den GIC (Generic Interrupt Controller), das Prinzip ist dasselbe.

Der Weg eines Interrupts

Der Weg eines Hardware-Interrupts auf x86-64 Gerät Paket empfangen Interrupt-Controller APIC · MSI-X CPU Vektor → IDT common_interrupt Register sichern IRQ-Kern irq_desc · handle_irq Treiber-Handler Top Half irq_exit() Softirqs ausführen Rückkehr unterbrochener Code
  1. Das Gerät meldet ein Ereignis

    Die Netzwerkkarte hat ein Paket per DMA in den Arbeitsspeicher geschrieben und will das melden. Moderne PCIe-Geräte tun das per MSI-X: Sie schreiben eine kleine Nachricht an eine spezielle Adresse, statt eine eigene Leitung zu benutzen. Jede Empfangs-Warteschlange kann dabei ihren eigenen Interrupt haben.

  2. Der Interrupt-Controller wählt Ziel-CPU und Vektor

    Die Nachricht landet beim Local APIC der Ziel-CPU. Ältere Geräte mit Interrupt-Leitung gehen über den I/O-APIC, der ebenfalls eine CPU auswählt. Am Ende steht eine Vektornummer (0–255), die festlegt, welcher Handler zuständig ist.

  3. Die CPU unterbricht den laufenden Code

    Nach dem aktuellen Befehl sichert die CPU Befehlszähler, Flags und Stackpointer auf dem Kernel-Stack, sperrt weitere Interrupts und springt an die Adresse, die in der Interrupt Descriptor Table (IDT) für diesen Vektor eingetragen ist. Kam der Interrupt aus dem User Space, wechselt sie dabei in den Kernelmodus.

  4. Einsprung im Kernel

    common_interrupt sichert die übrigen Register als struct pt_regs, wechselt auf einen eigenen Interrupt-Stack der CPU und meldet dem Kernel mit irq_enter(), dass er sich jetzt im Interruptkontext befindet.

  5. Der generische IRQ-Kern findet die Handler

    Über den Vektor findet der Kernel die zugehörige struct irq_desc. Ein Flow-Handler bestätigt den Interrupt beim Controller und ruft alle registrierten Handler auf – bei gemeinsam genutzten Leitungen nacheinander, bis sich einer zuständig fühlt.

  6. Der Handler des Treibers (Top Half)

    Der Handler muss schnell sein, denn auf dieser CPU sind Interrupts gesperrt. Er liest den Gerätestatus, bestätigt den Interrupt am Gerät und plant die eigentliche Arbeit für später ein – bei Netzwerkkarten, indem er den Softirq NET_RX auslöst. Dann gibt er IRQ_HANDLED zurück.

  7. Aufgeschobene Arbeit: Softirqs

    Beim Verlassen des Interrupts prüft irq_exit(), ob Softirqs anstehen, und führt sie aus – jetzt mit wieder freigegebenen Interrupts. Hier verarbeitet der Netzwerk-Stack die empfangenen Pakete. Dauert das zu lange (mehr als 2 ms oder 10 Durchläufe), übernimmt der Kernel-Thread ksoftirqd den Rest.

  8. Zurück zum unterbrochenen Code

    Schließlich stellt der Kernel die Register wieder her und kehrt mit iret zurück. Ist inzwischen ein wichtigerer Task lauffähig geworden – etwa der Prozess, der auf das Paket gewartet hat –, ruft der Kernel vorher den Scheduler auf.

Am Beispiel einer Netzwerkkarte, die ein Paket empfangen hat: vom Signal des Geräts bis zur Rückkehr in den unterbrochenen Code.
Tiefer eintauchenDie Interrupt Descriptor Table

Die IDT ist eine Tabelle mit 256 Einträgen, deren Adresse im CPU-Register IDTR steht. Jeder Eintrag beschreibt, wohin die CPU bei einem bestimmten Vektor springt. Die Vektoren 0 bis 31 sind für Ausnahmen der CPU reserviert, etwa 0 für Division durch null, 13 für die General Protection Fault und 14 für den Seitenfehler. Die übrigen Vektoren verteilt der Kernel an Geräte-Interrupts, IPIs und den lokalen Timer.

Bei der Rückkehr aus dem Interrupt stellt iret den gesicherten Zustand wieder her, einschließlich des Interrupt-Flags. Der unterbrochene Code merkt im Normalfall nichts von der Unterbrechung, außer dass etwas Zeit vergangen ist.

Handler registrieren

Ein Treiber meldet seinen Handler mit request_irq() an:

static irqreturn_t meinegeraet_irq(int irq, void *dev_id)
{
	struct meinegeraet *dev = dev_id;
	u32 status = readl(dev->regs + STATUS);

	if (!(status & IRQ_PENDING))
		return IRQ_NONE;              /* nicht von uns – geteilte Leitung */

	writel(status, dev->regs + STATUS);   /* Interrupt am Gerät bestätigen */
	schedule_work(&dev->work);            /* eigentliche Arbeit später erledigen */
	return IRQ_HANDLED;
}

/* bei der Initialisierung des Treibers */
err = request_irq(dev->irq, meinegeraet_irq, IRQF_SHARED, "meinegeraet", dev);

Der Rückgabewert sagt dem Kernel, ob der Handler sich zuständig fühlte. Meldet über längere Zeit kein Handler IRQ_HANDLED, schaltet der Kernel den Interrupt als „verwaist“ ab („nobody cared“), damit ein defektes Gerät das System nicht lahmlegt.

Top Half und Bottom Half

Während ein Interrupt-Handler läuft, sind auf dieser CPU weitere Interrupts gesperrt. Jede Mikrosekunde im Handler verzögert also andere Geräte und den Timer. Linux teilt die Arbeit deshalb auf:

  • Die Top Half ist der eigentliche Interrupt-Handler. Sie erledigt nur das zeitkritische Minimum: das Gerät bestätigen, Daten sichern, die nicht verloren gehen dürfen, und die restliche Arbeit einplanen.
  • Die Bottom Half erledigt alles Weitere später, mit freigegebenen Interrupts.
Top Half und Bottom Half auf der Zeitachse Unterbrochener Task Prozesskontext Top Half Interrupts gesperrt Softirq / BH darf nicht schlafen Threaded IRQ / Workqueue Kernel-Thread, darf schlafen IRQ vom Scheduler eingeplant Zeit →
Der eigentliche Interrupt-Handler läuft so kurz wie möglich. Arbeit, die warten kann, wird in Softirqs (sofort danach, nicht unterbrechbar durch Tasks) oder in Kernel-Threads verlagert, die der Scheduler wie normale Tasks einplant und die schlafen dürfen.

Für die Bottom Half gibt es mehrere Mechanismen:

MechanismusKontextdarf schlafentypischer Einsatz
Softirqnach dem Interrupt bzw. in ksoftirqdneinNetzwerk, Block-I/O, Timer – fest im Kernel definiert
TaskletSoftirqneinältere Treiber; gilt als veraltet
BH-Workqueue (WQ_BH)Softirqneinmoderner Ersatz für Tasklets (seit 6.9)
Threaded IRQeigener Kernel-Thread irq/N-namejaTreiber, die beim Abarbeiten warten müssen, z. B. über I²C/SPI angebundene Geräte
WorkqueueKernel-Thread kworkerjaalles, was Zeit braucht oder blockieren kann

Softirqs

SoftirqsSoftirqFest definierte Art aufgeschobener Kernel-Arbeit, die direkt nach einem Interrupt oder im Thread ksoftirqd läuft, etwa für Netzwerkpakete oder Timer. Darf nicht schlafen. sind die schnellste Form der Bottom Half. Es gibt genau zehn, fest im Quellcode definiert (include/linux/interrupt.h):

enum {
	HI_SOFTIRQ = 0,   /* hochpriore Tasklets */
	TIMER_SOFTIRQ,    /* abgelaufene Timer */
	NET_TX_SOFTIRQ,   /* Netzwerk: Senden */
	NET_RX_SOFTIRQ,   /* Netzwerk: Empfangen */
	BLOCK_SOFTIRQ,    /* Abschluss von Block-I/O */
	IRQ_POLL_SOFTIRQ,
	TASKLET_SOFTIRQ,  /* normale Tasklets */
	SCHED_SOFTIRQ,    /* Lastausgleich des Schedulers */
	HRTIMER_SOFTIRQ,  /* hochauflösende Timer */
	RCU_SOFTIRQ,      /* RCU-Verarbeitung (Kapitel 9) */
	NR_SOFTIRQS
};

Ein Interrupt-Handler markiert einen Softirq mit raise_softirq() als „anstehend“. Ausgeführt wird er spätestens beim Verlassen des Interrupts in handle_softirqs. Weil derselbe Softirq gleichzeitig auf mehreren CPUs laufen kann, müssen seine Handler sorgfältig synchronisiert sein. Dafür skalieren sie sehr gut.

Kommen ständig neue Softirqs nach, etwa unter hoher Netzwerklast, würden normale Tasks nie mehr an die Reihe kommen. Der Kernel begrenzt die Bearbeitung deshalb auf 2 ms bzw. 10 Durchläufe und übergibt den Rest an den Kernel-Thread ksoftirqd/N der jeweiligen CPU. Dieser wird vom Scheduler wie ein normaler Task behandelt.

Workqueues

Eine WorkqueueWorkqueueMechanismus, um Arbeit an Kernel-Threads (kworker) zu übergeben. Die Arbeit läuft im Prozesskontext und darf schlafen. nimmt Arbeitsaufträge (struct work_struct) entgegen und lässt sie von Kernel-Threads (kworker/…) im Prozesskontext ausführen. Dort darf der Code schlafen, Mutexe nehmen und I/O abwarten:

static void meinegeraet_work(struct work_struct *work)
{
	struct meinegeraet *dev = container_of(work, struct meinegeraet, work);

	mutex_lock(&dev->lock);       /* hier erlaubt – wir sind im Prozesskontext */
	/* … Daten verarbeiten, ggf. auf das Gerät warten … */
	mutex_unlock(&dev->lock);
}

INIT_WORK(&dev->work, meinegeraet_work);   /* einmalig */
schedule_work(&dev->work);                  /* z. B. aus dem Interrupt-Handler */

Die Workqueue-Infrastruktur („concurrency managed workqueues“) verwaltet einen Pool von Worker-Threads pro CPU. Sie startet nur so viele Threads, wie nötig sind, um die CPU ausgelastet zu halten. Blockiert ein Auftrag, übernimmt ein weiterer Worker die nächsten.

Threaded IRQs

Bei einem Threaded IRQ registriert der Treiber zwei Funktionen mit request_threaded_irq(): einen kurzen Handler, der im Interruptkontext läuft und IRQ_WAKE_THREAD zurückgibt, und eine Thread-Funktion, die anschließend in einem eigenen Kernel-Thread irq/<nr>-<name> ausgeführt wird. Dieser Thread hat eine Echtzeit-Priorität, reagiert also schnell, darf aber schlafen.

Mit PREEMPT_RT (Kapitel 6) werden fast alle Interrupt-Handler automatisch als Threads ausgeführt. Dadurch lassen sie sich verdrängen und priorisieren, und genau das macht vorhersagbare Reaktionszeiten möglich.

Zeit und Timer

Der Tick und jiffies

Klassischerweise lässt der Kernel einen Timer in festen Abständen einen Interrupt auslösen, den Tick. Die Frequenz HZ wird beim Bauen des Kernels festgelegt. Typisch sind 250 oder 1000 pro Sekunde. Bei jedem Tick

Timer im Kernel

Für Zeitüberschreitungen gibt es zwei Arten von Timern:

  • Klassische Timer (struct timer_list) haben eine Auflösung von einem Jiffy. Sie liegen in einem Timer-Rad (timer wheel) aus 8 bis 9 Ebenen mit je 64 Fächern. Nahe Zeitpunkte werden fein, ferne grob einsortiert. Einfügen und Entfernen geschieht so in konstanter Zeit. Die meisten dieser Timer (Netzwerk-Zeitüberschreitungen, Watchdogs) werden übrigens gelöscht, bevor sie ablaufen.
  • Hochauflösende Timer (hrtimer) arbeiten in Nanosekunden und werden in einem Rot-Schwarz-Baum nach Ablaufzeit sortiert. Sie stecken hinter nanosleep(), Timeouts von poll() und epoll, POSIX-Timern und dem Scheduler-Tick selbst.

Darunter liegen zwei Abstraktionen für die Hardware: clocksource (eine fortlaufende Zeitquelle zum Ablesen, auf x86 meist der TSC-Zähler der CPU) und clockevent (ein Gerät, das zu einem bestimmten Zeitpunkt einen Interrupt auslöst, etwa der Timer des Local APIC).

Tiefer eintauchenTickloser Betrieb (NO_HZ)

Ein Tick tausendmal pro Sekunde auf einer CPU, die gar nichts zu tun hat, verschwendet Energie, denn jeder Tick weckt sie aus dem Tiefschlaf. Mit NO_HZ_IDLE (heute Standard) schaltet der Kernel den Tick auf untätigen CPUs ab und programmiert den Timer stattdessen auf den nächsten tatsächlich benötigten Zeitpunkt.

Mit NO_HZ_FULL geht das noch weiter: Läuft auf einer CPU nur ein einziger Task, ist der Tick auch dort überflüssig. Für Anwendungen wie Hochfrequenzhandel oder HPC lassen sich so CPUs reservieren (nohz_full=, isolcpus=), auf denen ein Programm praktisch ungestört vom Kernel rechnet.

Interrupts verteilen und abschalten

Verteilung: Über /proc/irq/<nr>/smp_affinity_list lässt sich festlegen, welche CPUs einen Interrupt bearbeiten dürfen. Der Dienst irqbalance verteilt Interrupts automatisch nach Last. Für Geräte mit einer Warteschlange pro CPU legt der Treiber die Zuordnung meist selbst fest (managed interrupts).

Abschalten: Manchmal muss Kernel-Code verhindern, dass ein Interrupt dazwischenkommt, etwa wenn er Daten bearbeitet, die auch ein Interrupt-Handler anfasst. Dafür gibt es:

FunktionWirkung
local_irq_save(flags) / local_irq_restore(flags)alle Interrupts auf dieser CPU sperren bzw. den vorigen Zustand wiederherstellen
local_bh_disable() / local_bh_enable()nur Softirqs auf dieser CPU sperren
disable_irq(nr) / enable_irq(nr)einen bestimmten Interrupt auf allen CPUs sperren

Das Sperren von Interrupts auf einer CPU schützt nicht vor anderen CPUs. Dafür braucht man zusätzlich Sperren. Wie beides zusammenspielt, zeigt Kapitel 9.

Selbst ausprobieren

Selbst ausprobieren: Interrupts beobachten

# Zähler je Interrupt und CPU – zweimal ausführen und vergleichen
cat /proc/interrupts

# Live: welche Zähler steigen gerade? (Strg+C beendet)
watch -n1 -d "grep -E 'CPU|nvme|eth|enp|wlp|LOC|RES|TLB' /proc/interrupts"

# Softirq-Zähler je CPU
cat /proc/softirqs

# Kernel-Threads für Softirqs, Threaded IRQs und Workqueues
ps -eo pid,cls,rtprio,comm | grep -E 'ksoftirqd|irq/|kworker' | head -n 20

# Welche CPUs bearbeiten Interrupt Nr. 0 (Timer)? Beliebige Nummer aus /proc/interrupts einsetzen
cat /proc/irq/0/smp_affinity_list

# Tickrate und Zeitquelle
grep -E '^CONFIG_HZ=|^CONFIG_NO_HZ' /boot/config-$(uname -r)
cat /sys/devices/system/clocksource/clocksource0/current_clocksource

In /proc/interrupts stehen neben Geräten auch Sammelzeilen der CPU selbst: LOC (lokaler Timer), RES (rescheduling: IPI zum Aufwecken eines Tasks), CAL (function call: IPI zum Ausführen einer Funktion auf einer anderen CPU), TLB (TLB-Shootdowns) und NMI.