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 6

Der Scheduler

Wie der Kernel entscheidet, welcher Task wann und auf welcher CPU rechnet – Scheduling-Klassen, Prioritäten und nice-Werte, der EEVDF-Algorithmus, Verdrängung, Mehrprozessorsysteme und Echtzeit.

Kurz gesagt

Auf einem Rechner laufen meist viel mehr Programme, als es CPU-Kerne gibt. Der Scheduler teilt die Rechenzeit auf. Er wechselt so schnell zwischen den Programmen, dass sie scheinbar gleichzeitig laufen. Normale Programme bekommen dabei einen fairen Anteil, den man mit dem nice-Wert verschieben kann. Echtzeitprogramme haben immer Vorrang. Jeder CPU-Kern hat seine eigene Warteschlange, und der Kernel verteilt die Last zwischen den Kernen.

Nach diesem Kapitel …

  • die Ziele eines Schedulers und die Zielkonflikte zwischen ihnen benennen
  • die Scheduling-Klassen und ihre Rangfolge erklären
  • nice-Werte, Gewichte und die interne Prioritätsskala in Beziehung setzen
  • das Prinzip der virtuellen Laufzeit und die Auswahlregel von EEVDF verstehen
  • beschreiben, wann der Scheduler aufgerufen wird und was Verdrängung bedeutet
  • Echtzeit-Policies und ihre Risiken einordnen und Scheduling-Parameter selbst setzen

Die Aufgabe des Schedulers

Ein typischer Desktop hat einige hundert Tasks, aber nur eine Handvoll CPU-Kerne. Die meisten Tasks schlafen zwar und warten auf Ereignisse (Kapitel 5), doch sobald mehrere gleichzeitig rechnen wollen, muss jemand entscheiden: Wer darf jetzt auf welche CPU, und wie lange? Das ist die Aufgabe des Schedulers.

Dabei stehen mehrere Ziele im Widerspruch zueinander:

  • Durchsatz: möglichst viel Arbeit pro Zeit erledigen, also selten wechseln, weil jeder Kontextwechsel kostet.
  • Latenz: auf Ereignisse schnell reagieren, etwa auf einen Tastendruck oder ein ankommendes Paket. Das erfordert häufiges Wechseln.
  • Fairness: Gleichberechtigte Tasks bekommen gleich viel Rechenzeit, und niemand verhungert.
  • Vorhersagbarkeit: Echtzeit-Anwendungen müssen garantierte Fristen einhalten.
  • Energieeffizienz und Skalierbarkeit: CPUs sollen schlafen dürfen, wenn wenig zu tun ist, und auch auf Systemen mit hunderten Kernen soll der Scheduler effizient bleiben.

Der Linux-Scheduler löst das nicht mit einem einzigen Algorithmus, sondern mit mehreren Scheduling-Klassen, die jeweils einen Teil dieser Ziele verfolgen.

Scheduling-Klassen und Policies

Jeder Task gehört über seine Policy zu genau einer Klasse. Die Klassen sind streng nach Priorität geordnet:

Die Scheduling-Klassen in der Reihenfolge ihrer Priorität hoch niedrig Reihenfolge der Abfrage Stop stop_sched_class intern: CPU anhalten, Tasks migrieren Deadline dl_sched_class SCHED_DEADLINE Echtzeit rt_sched_class SCHED_FIFO, SCHED_RR Fair (EEVDF) fair_sched_class SCHED_NORMAL, SCHED_BATCH, SCHED_IDLE sched_ext (BPF) ext_sched_class SCHED_EXT Idle idle_sched_class intern: Leerlauf-Task je CPU
Bei jeder Auswahl fragt der Scheduler die Klassen von oben nach unten. Die erste Klasse, die einen lauffähigen Task hat, bestimmt, wer als Nächstes rechnet. Ein normaler Task kommt also nur zum Zug, wenn kein Echtzeit- oder Deadline-Task bereit ist.

Die Policy eines Tasks setzt man mit sched_setscheduler() bzw. sched_setattr() oder auf der Kommandozeile mit chrt:

PolicyKlasseEinsatz
SCHED_NORMAL (auch SCHED_OTHER)fairStandard für fast alle Programme
SCHED_BATCHfairrechenintensive Hintergrundjobs: wird beim Aufwachen nicht bevorzugt
SCHED_IDLEfairnur wenn sonst nichts zu tun ist (Gewicht 3, noch unter nice 19)
SCHED_FIFOEchtzeitläuft, bis ein höherer Echtzeit-Task kommt oder er selbst die CPU abgibt
SCHED_RREchtzeitwie FIFO, aber mit Zeitscheibe (Standard 100 ms) unter Tasks gleicher Priorität
SCHED_DEADLINEDeadlineperiodische Aufgaben mit garantierter Rechenzeit je Periode
SCHED_EXTsched_extScheduling-Strategie wird als BPF-Programm geladen (seit Linux 6.12)

Intern ist jede Klasse eine struct sched_class, im Grunde eine Tabelle von Funktionszeigern: enqueue_task, dequeue_task, pick_next_task, task_tick und weitere. Der Kern des Schedulers in kernel/sched/core.c ruft nur diese Funktionen auf und weiß nichts über die Algorithmen der einzelnen Klassen.

Prioritäten und nice-Werte

Ein Begriff verwirrt anfangs fast alle: Priorität. Linux kennt mehrere Skalen, die intern auf eine gemeinsame Skala von 0 bis 139 abgebildet werden:

Die interne Prioritätsskala des Kernels Echtzeit (SCHED_FIFO, SCHED_RR) prio = 99 − rt_priority (1…99) Normal (SCHED_NORMAL …) prio = 120 + nice (−20…19) 0 50 99 100 120 139 Standard: nice 0 ← höhere Priorität 0 … 139
Intern rechnet der Kernel mit einer Skala von 0 bis 139. Kleinere Zahlen bedeuten höhere Priorität. Echtzeit-Tasks belegen 0 bis 99, normale Tasks 100 bis 139 – der nice-Wert verschiebt sie innerhalb dieses Bereichs.

Für normale Tasks ist der nice-Wertnice-WertZahl von −20 bis 19, die die Priorität eines normalen Tasks beeinflusst. Höhere Werte bedeuten „netter zu anderen“, also weniger CPU-Anteil. Jede Stufe ändert den Anteil um etwa 10 %. maßgeblich (−20 bis 19, Standard 0). Er bestimmt nicht die Reihenfolge, sondern das Gewicht eines Tasks und damit seinen Anteil an der CPU-Zeit. Die Gewichte stehen in der Tabelle sched_prio_to_weight in kernel/sched/core.c. nice 0 entspricht dem Gewicht 1024, und jede Stufe ändert das Gewicht um den Faktor 1,25:

nice−20−10−5051019
Gewicht8876195483121102433511015

Die Faustregel: Eine nice-Stufe entspricht etwa 10 % CPU-Anteil. Konkurrieren zwei Tasks mit nice 0 und nice 1, bekommt der erste 1024 / (1024 + 820) ≈ 55 %, der zweite 45 %. Der Abstand ist relativ: nice 0 gegen nice 5 verhält sich genauso wie nice 10 gegen nice 15.

Normale Benutzer dürfen den nice-Wert nur erhöhen (sich selbst „netter“ machen). Zum Senken unter 0 braucht man Root-Rechte bzw. die Capability CAP_SYS_NICE.

Faires Scheduling: die virtuelle Laufzeit

Wie verteilt man CPU-Zeit fair nach Gewichten? Die Idee stammt vom Completely Fair Scheduler (CFS), der von 2007 (Linux 2.6.23) bis 2023 der Standard war. Jeder Task führt eine virtuelle Laufzeit vruntime. Läuft er real Δt lang, wächst sie um

Δvruntime = Δt × 1024 / Gewicht

Ein Task mit nice 0 (Gewicht 1024) altert also in Echtzeit. Ein Task mit doppeltem Gewicht altert halb so schnell, einer mit halbem Gewicht doppelt so schnell. Der Scheduler versucht, die virtuellen Laufzeiten aller Tasks gleich zu halten: Er wählt immer den Task, der „am weitesten zurückliegt“. Wer schwerer ist, darf dadurch real länger rechnen, bis er aufgeholt hat.

Probiere es aus. Mit zwei Tasks nice 0 und einem nice 5 verteilt sich die Zeit auf etwa 43 %, 43 % und 14 %:

TaskniceGewichtSoll-Anteilin 60 ms erhalten
A 1024 43,0 % 27 ms
B 1024 43,0 % 24 ms
C 335 14,1 % 9 ms
ABC 0 10 20 30 40 50 60
Vereinfachtes Modell: Der Scheduler wählt immer den Task mit der kleinsten virtuellen Laufzeit und lässt ihn 3 ms rechnen. Die virtuelle Laufzeit wächst umso langsamer, je größer das Gewicht ist. Ändere die nice-Werte und beobachte die Anteile.

EEVDF: der aktuelle Algorithmus

Seit Linux 6.6 verwendet die faire Klasse den Algorithmus EEVDFEEVDFEarliest Eligible Virtual Deadline First: seit Linux 6.6 der Algorithmus für normale Tasks. Unter den Tasks, die noch CPU-Zeit „gut haben“, läuft der mit der frühesten virtuellen Deadline. (Earliest Eligible Virtual Deadline First). Er beruht auf einer wissenschaftlichen Arbeit von 1995 und behält die virtuelle Laufzeit bei, trifft die Auswahl aber nach einer genaueren Regel:

  1. Anspruch (Lag): Für jeden Task wird berechnet, wie viel Rechenzeit er im Vergleich zu einer ideal fairen Verteilung zu wenig oder zu viel bekommen hat. Ein Task ist berechtigt (eligible), wenn er nicht im Plus ist, seine vruntime also nicht über dem gewichteten Durchschnitt liegt.
  2. Virtuelle Deadline: Jeder Task fordert eine Zeitscheibe an (Standard: base_slice, 0,7 ms, auf Rechnern mit vielen CPUs bis zu viermal so viel). Daraus ergibt sich eine virtuelle Deadline: deadline = vruntime + slice × 1024 / Gewicht.
  3. Auswahl: Unter allen berechtigten Tasks läuft der mit der frühesten virtuellen Deadline.

Der Vorteil gegenüber CFS: Tasks, die kurze Zeitscheiben anfordern, bekommen frühere Deadlines und kommen schneller dran. Langfristig erhalten sie trotzdem nicht mehr als ihren fairen Anteil. Latenz und Anteil lassen sich so getrennt steuern. CFS brauchte dafür zahlreiche Heuristiken, die EEVDF überflüssig macht.

Tiefer eintauchenImplementierung: ein erweiterter Rot-Schwarz-Baum

Die lauffähigen Tasks einer CPU liegen in einem Rot-Schwarz-Baum, einem balancierten binären Suchbaum mit Einfügen und Löschen in O(log n). Seit EEVDF ist er nach der virtuellen Deadline sortiert (entity_before). Zusätzlich speichert jeder Knoten die kleinste vruntime in seinem Teilbaum (ein augmented Baum). Damit findet pick_eevdf in logarithmischer Zeit den berechtigten Task mit der frühesten Deadline, ohne alle Tasks durchsehen zu müssen.

Der Scheduler plant übrigens nicht nur Tasks, sondern Scheduling-Entitäten (struct sched_entity). Eine Entität kann auch eine ganze Gruppe von Tasks sein, etwa eine cgroup. Dann bekommt zunächst die Gruppe ihren fairen Anteil, und innerhalb der Gruppe wird wieder fair verteilt. So erhält ein Container mit 100 Prozessen nicht automatisch hundertmal so viel CPU-Zeit wie einer mit einem Prozess.

Tiefer eintauchenWarum nice manchmal scheinbar nicht wirkt: Autogroups

Viele Desktop-Distributionen aktivieren Autogroups (CONFIG_SCHED_AUTOGROUP): Alle Prozesse einer Sitzung, etwa eines Terminalfensters, bilden automatisch eine Gruppe. Startet man in einem Terminal einen Compiler mit 16 Threads, bekommt dieses Terminal als Ganzes denselben Anteil wie der Browser in einer anderen Sitzung. Der Desktop bleibt dadurch flüssig.

Die Kehrseite: Ein nice-Wert wirkt dann nur innerhalb der eigenen Gruppe. nice -n 19 in einem Terminal bremst einen Prozess gegenüber Prozessen in anderen Terminals kaum. Für die Gruppe als Ganzes gibt es einen eigenen nice-Wert in /proc/<pid>/autogroup. Ob Autogroups aktiv sind, zeigt /proc/sys/kernel/sched_autogroup_enabled.

Wann der Scheduler entscheidet

Der Scheduler ist kein eigener Prozess. Er besteht aus der Funktion __schedule, die zu bestimmten Zeitpunkten aufgerufen wird:

  1. Freiwillig, wenn ein Task blockiert, etwa auf I/O, eine Sperre oder sleep(). Er ruft dann selbst schedule() auf.
  2. Beim Beenden eines Tasks.
  3. Bei Verdrängung (PreemptionVerdrängung (Preemption)Der Scheduler entzieht einem laufenden Task die CPU, ohne dass dieser sie freiwillig abgibt, etwa weil ein wichtigerer Task lauffähig geworden ist.): Der Kernel stellt fest, dass ein anderer Task jetzt Vorrang haben sollte, und setzt beim laufenden Task das Flag TIF_NEED_RESCHED. Das geschieht zum Beispiel
    • im Timer-Interrupt (sched_tick()), wenn die Zeitscheibe abgelaufen ist,
    • beim Aufwecken eines Tasks, der Vorrang hat, etwa eines Echtzeit-Tasks.

Das Flag allein wechselt noch nichts. Der eigentliche Wechsel passiert an der nächsten sicheren Stelle: spätestens bei der Rückkehr in den User Space, nach einem Interrupt oder an einer Stelle im Kernel, an der Verdrängung erlaubt ist.

Verdrängung im Kernel: Preemption-Modelle

Ob auch Kernel-Code mitten in der Ausführung verdrängt werden darf, ist eine Abwägung zwischen Durchsatz und Latenz. Linux bietet dafür mehrere Modelle, die sich bei vielen Distributionen beim Booten mit preempt= wählen lassen:

ModellVerhaltentypischer Einsatz
noneKernel-Code wird nie verdrängt, nur bei Rückkehr in den User SpaceServer mit Fokus auf Durchsatz
voluntaryzusätzlich an expliziten Stellen (cond_resched())klassischer Desktop-Standard
fullKernel-Code ist überall verdrängbar, außer in gesperrten AbschnittenDesktop, geringe Latenz
lazywie full, normale Tasks werden aber erst an der nächsten Tick-Grenze verdrängtKompromiss (seit 6.13)
Echtzeit (PREEMPT_RT)fast alles verdrängbar, auch Interrupt-Handler laufen als ThreadsIndustriesteuerung, Audio (seit 6.12 im Hauptkernel)

Abschnitte, die nicht unterbrochen werden dürfen, etwa solange ein Spinlock gehalten wird, schalten die Verdrängung vorübergehend ab (preempt_disable()). Mehr dazu in Kapitel 9.

Mehrere CPUs

Auf Mehrkernsystemen hat jede CPU ihre eigene Run-QueueRun-QueueWarteschlange der lauffähigen Tasks einer CPU (struct rq). Jede CPU hat ihre eigene. (struct rq). Eine einzige gemeinsame Warteschlange wäre ein Engpass, weil alle CPUs ständig um dieselbe Sperre konkurrieren würden:

Run-Queues: eine pro CPU CPU 0 A · läuft gerade dl_rq: nach Deadline sortiert leer rt_rq: Listen je Priorität 0…99 R cfs_rq: Rot-Schwarz-Baum F C H B D ↑ früheste Deadline CPU 1 K · läuft gerade dl_rq: nach Deadline sortiert leer rt_rq: Listen je Priorität 0…99 leer cfs_rq: Rot-Schwarz-Baum M L P ↑ früheste Deadline Lastausgleich
Jede CPU hat ihre eigene Run-Queue mit Unter-Warteschlangen für jede Scheduling-Klasse. Normale Tasks liegen in einem Rot-Schwarz-Baum, sortiert nach ihrer virtuellen Deadline. Der Lastausgleich verschiebt Tasks zwischen den CPUs.

Damit keine CPU überlastet ist, während eine andere sich langweilt, gleicht der Kernel die Last regelmäßig aus:

  • Periodischer Lastausgleich: In festen Abständen prüft jede CPU, ob andere CPUs deutlich mehr zu tun haben, und zieht gegebenenfalls Tasks zu sich.
  • Leerlauf-Ausgleich: Wird eine CPU untätig, sucht sie sofort nach Arbeit bei den Nachbarn.
  • Platzierung beim Aufwachen: Ein aufwachender Task wird möglichst auf eine CPU gelegt, die gerade frei ist und deren Caches seine Daten vielleicht noch enthalten.

Dabei berücksichtigt der Scheduler die Topologie über Scheduling Domains. Ein Task zwischen zwei Hyperthreads desselben Kerns zu verschieben ist billig, weil sie sich die Caches teilen. Ein Wechsel auf einen anderen Prozessorsockel (NUMA-Knoten) ist teuer, weil der Arbeitsspeicher dann „weiter weg“ liegt. Auf Prozessoren mit unterschiedlich starken Kernen (z. B. ARM big.LITTLE oder Intels P- und E-Kerne) bezieht der Scheduler außerdem Leistung und Energieverbrauch der Kerne ein.

Mit der CPU-Affinität lässt sich festlegen, auf welchen CPUs ein Task laufen darf (sched_setaffinity(), Kommandozeile: taskset).

Echtzeit-Scheduling

Echtzeit bedeutet nicht „schnell“, sondern vorhersagbar: Eine Reaktion muss innerhalb einer garantierten Frist erfolgen. Dafür gibt es zwei Klassen:

SCHED_FIFO und SCHED_RR arbeiten mit festen Prioritäten von 1 bis 99. Ein lauffähiger Echtzeit-Task verdrängt sofort jeden normalen Task und jeden Echtzeit-Task niedrigerer Priorität. Unter Tasks gleicher Priorität läuft bei FIFO der erste so lange, bis er blockiert. Bei RR wechseln sie sich nach ihrer Zeitscheibe ab.

SCHED_DEADLINE beschreibt einen Task durch drei Werte: Er braucht Laufzeit R innerhalb jeder Periode P und muss jeweils bis zur Frist D fertig sein. Der Kernel nimmt einen solchen Task nur an, wenn die Summe der angeforderten Anteile R/P die CPU-Kapazität nicht übersteigt (Admission Control). Ausgewählt wird nach Earliest Deadline First. Damit lassen sich Garantien geben, die mit festen Prioritäten nicht möglich sind.

sched_ext: Scheduler als BPF-Programm

Seit Linux 6.12 kann man mit sched_ext eine komplette Scheduling-Strategie als BPF-Programm zur Laufzeit laden (Kapitel 16). Tasks mit der Policy SCHED_EXT werden dann von diesem Programm verteilt. Das ermöglicht schnelle Experimente mit neuen Algorithmen und Strategien, die auf eine bestimmte Arbeitslast zugeschnitten sind, etwa für Spiele oder große Rechenzentren. Stürzt der BPF-Scheduler ab oder verhält er sich falsch, schaltet der Kernel automatisch auf den normalen Scheduler zurück.

Selbst ausprobieren

Selbst ausprobieren: Scheduling-Parameter ansehen und ändern

# Policy und Priorität der eigenen Shell
chrt -p $$

# Policy (CLS), nice (NI) und Echtzeit-Priorität aller Prozesse
ps -eo pid,cls,ni,rtprio,comm | head -n 20

# Erlaubte Prioritätsbereiche je Policy
chrt -m

# Auf welchen CPUs darf die Shell laufen?
taskset -cp $$

# Scheduler-Statistik eines Prozesses (Laufzeit, Wartezeit, Kontextwechsel …)
head -n 25 /proc/$$/sched

# Basis-Zeitscheibe und Echtzeit-Reservierung
sudo cat /sys/kernel/debug/sched/base_slice_ns
cat /proc/sys/kernel/sched_rt_runtime_us /proc/sys/kernel/sched_rt_period_us

Selbst ausprobieren: nice in Aktion

# Zwei Endlosschleifen auf dieselbe CPU 0 zwingen, eine davon mit nice 5
taskset -c 0 yes > /dev/null &
taskset -c 0 nice -n 5 yes > /dev/null &

# CPU-Anteile beobachten (Spalte %CPU), mit q beenden
top -p "$(pgrep -d, -x yes)"

# Aufräumen
pkill -x yes

Nach der Gewichtstabelle sollten sich die Anteile wie 1024 : 335 verhalten, also etwa 75 % zu 25 %. Die Spalte PR in top zeigt übrigens die interne Priorität minus 100, für nice 0 also 20.