Virtualisierung auf der einen und Echtzeitverhalten auf der anderen Seite. Das sind die beiden großen Entwicklungsfelder am Linux-Kernel, die zurzeit spürbare Verbesserungen für den Nutzer mit sich bringen. Mit Kernel 2.6.21 hat Linus Torvalds nicht nur das in der Kern-Technik-Folge 31 (siehe [1]) beschriebene Dyntick-Patch übernommen, sondern gleich das dort in Aussicht gestellte Tickless-Patch mit dazu.
Doch damit nicht genug. In der Queue der Entwickler Thomas Gleixner und Ingo Molnar stehen weitere Änderungen, die Stück für Stück den Weg in den Mainstream-Kernel finden sollen. Entwickler von Systemen mit hohen Anforderungen an das Zeitverhalten nutzen das entwickelte Patchset »PREEMPT_RT« ([2], [3], [4], [5]) bereits seit einiger Zeit als Latenzzeit-Killer und um das Timing berechenbarer zu gestalten. Der Trick: Nicht unterbrechbare Bereiche werden unterbrechbar. Die nicht unterbrechbaren Bereiche existieren in mehreren Formen: als Interrupt-Service-Routinen, als Soft-IRQs und als durch Spinlocks geschützte Codesequenzen.
Alles ein Thread
Die Preempt-Modifikationen stellen damit das bisherige Unterbrechungsverhalten auf den Kopf, mit weitreichenden Konsequenzen für die Kernelprogrammierer. Denn mit einem Mal scheint der Kernel Interrupt-Service-Routinen zu schedulen und Soft-IRQs schlafen zu legen.
Doch das ist in Wahrheit nur babylonische Sprachverwirrung. Denn technisch gesehen stellt der Linux-Kernel weiterhin die vier Unterbrechungsebenen zur Verfügung, wie sie im Kasten "Unterbrechungsmodell" dargestellt sind. Nur dass die Entwickler den Code aus den Interrupt-Service-Routinen in hoch priorisierte Threads, die ISR-Threads, verschoben haben. Der zugehörige Code wird damit nicht mehr auf der Interrupt-, sondern auf der Kernel-Ebene abgearbeitet und der dort ablaufende Code ist bekanntlich unterbrechbar. Die richtige ISR hat nur noch die Aufgabe, den zugehörigen ISR-Thread zu aktivieren.
|
Codesequenzen im Linux-Kernel können auf vier unterschiedlichen Ebenen ablaufen (siehe Abbildung 1). Die Abarbeitungsebene entscheidet darüber, welche Funktionen des Kernels zum Einsatz kommen und welche (anderen) Codesequenzen sie unterbrechen dürfen. Die sich durch die Unterbrechung ergebenden kritischen Abschnitte sind durch entsprechende Methoden zu schützen.
Abbildung 1: Das Unterbrechungsmodell des Kernels: Die Unterbrechbarkeit setzt sich aus horizontaler und vertikaler Preemption zusammen.
Grundsätzlich gilt, dass Code, der auf einer niedrigeren Ebene lauffähig wird, höhere Priorität hat (vertikale Unterbrechbarkeit). Die Abarbeitung des Code auf einer darüberliegenden Ebene wird also zugunsten des lauffähig gewordenen Code unterbrochen. Eine Interrupt Service Routine (ISR) unterbricht einen Systemcall, der auf Kernel-Ebene arbeitet.
Die horizontale Preemption spezifiziert, ob Codesequenzen, die auf der gleichen Ebene ablaufen, sich gegenseitig unterbrechen können. Auf der ISR- und der Soft-IRQ-Ebene gibt es bei Linux typischerweise keine horizontale Unterbrechung. Werden mehrere konkurrierende Codesequenzen auf der Interrupt-Ebene oder der Soft-IRQ-Ebene lauffähig, findet konsequenterweise eine Sequenzialisierung im Sinne eines First Come First Serve (FCFS) statt: Erst wenn die gerade aktive Codesequenz beendet ist, arbeitet der Kernel die konkurrierende Codesequenz ab.
Auf der Kernel-Ebene hat Kernel 2.6 die Kernel-Preemption eingeführt. Dort abzuarbeitende Codesequenzen - Kernel-Threads und Systemcalls - sind priorisiert. Höher priore Jobs unterbrechen demnach niedrig priore Prozesse. Auf der Applikations-Ebene (Ebene 4) schließlich unterbrechen nicht nur die höher prioren Jobs die niedrig prioren, die gleich prioren Rechenprozesse geben nach dem Round-Robin-Prinzip (Zeitscheiben) die CPU nach einer endlichen Verarbeitungszeit ab. Diese Priorisierung arbeitet übrigens Ebenen-übergreifend, sodass ein hoch priorer Job auf Applikationsebene einen weniger wichtigen Job auf der Kernel-Ebene unterbrechen darf. Lock-Typen
Entwickler müssen das Unterbrechungsmodell kennen, damit sie kritische Abschnitte korrekt schützen können. Der Kernel bietet hierzu diverse Methoden an, die den konkurrierenden Zugriff auf ein Betriebsmittel innerhalb einer Ebene, aber auch von unterschiedlichen Unterbrechungsebenen heraus absichern. Die bekanntesten Methoden sind das Semaphor, das im Linux-Kernel im Wesentlichen in der Variante Mutex eingesetzt wird, und schließlich das Spinlock. Auf Einprozessormaschinen lassen sich Spinlocks allerdings nicht einsetzen. Innerhalb des Kernels setzen Programmierer hier alternativ die Interruptsperre ein. Sie mussten sich um dieses Detail bisher nicht kümmern, das Kernel-Buildsystem hat für sie jeweils den richtigen Code eingesetzt.
Jedoch mussten sie sehr wohl bedenken, auf welcher Ebene die an einem zu schützenden kritischen Abschnitt beteiligten Codesequenzen ablaufen. Schützen beispielsweise eine Interrupt-Service-Routine und Code auf Kernel-Ebene einen kritischen Abschnitt, mussten die Programmierer das Spinlock mit einer Interrupt-Sperre kombinieren (»spin_lock_irq()«). Das einfache Spinlock, repräsentiert durch die Funktion »spin_lock()«, kann den gegenseitigen Ausschluss, also die sequenzielle Abarbeitung, nur dann garantieren, wenn die konkurrierenden Codesequenzen auf unterschiedlichen Prozessoren laufen. Die zusätzliche Interruptsperre schließt damit die Unterbrechung des Code auf Kernel-Ebene durch eine ISR aus, die auf dem gleichen Prozessor abläuft.
Übrigens: Man sollte sich bei der Wahl des Schutzmechanismus eines kritischen Abschnitts nicht allein vom Unterbrechungsmodell und damit von einer scheinbaren Parallelverarbeitung leiten lassen. Mehrprozessorsysteme, die eine echte Parallelverarbeitung bieten, sind heute Standard. Besser ist es also, grundsätzlich davon auszugehen, dass alle Routinen mehrfach parallel ablaufen.
|
Genauso sieht es mit den Soft-IRQs aus. Den hierzu gehörenden Code haben die Kernelentwickler ebenfalls in eigene Threads verlagert (Soft-IRQ-Threads), die der Scheduler aufgrund ihrer hohen Priorität allerdings bevorzugt zur Abarbeitung auswählt. Im Fall des Falles unterbrechen aber wichtigere Rechenprozesse einen Soft-IRQ-Thread. In Anlehnung an das alte Unix-Motto "Alles ist eine Datei" heißt es jetzt also zusätzlich "Alles ist ein Thread".
Auch den Spinlocks rücken die Entwickler zu Leibe. Diese führen unter Umständen zu einer Interrupt-Sperre, sodass hoch priore Rechenprozesse mit engen zeitlichen Anforderungen bisher chancenlos waren. Wenn aber alles ein Thread ist und die zugehörigen Codesequenzen, beispielsweise die ehemaligen Interrupt-Service-Routinen, schlafen können, dann bleibt für Spinlocks nichts weiter als ein kleiner Performance-Vorteil (kein aufwändiger Prozesswechsel bei einem kurzen, gesperrten kritischen Abschnitt) übrig. Den tauschen die Entwickler gerne gegen verkürzte Latenzzeiten ein und verwandeln konsequenterweise Spinlocks im RT_PREEMPT-Patch in Mutexe.
Diese tiefgreifende Modifikation in der Basistechnologie (Spinlock zu Mutex - aktives zu passivem Warten) ist für den Programmierer unsichtbar. Der Compiler tauscht dank ausgeklügelter Makros die Namen der Datentypen und der verwendeten Funktionen gegen Mutex-Typen und -Funktionen aus. So wird aus dem Datentyp »spinlock_t« ein »struct rt_mutex«, und die Funktionen »spin_lock()« und »spin_unlock()« firmieren danach unter »rt_mutex_lock()« und »rt_mutex_unlock()«.
Namensverwirrung
Dass das Kind nicht mehr den richtigen Namen trägt, hat wohl damit zu tun, dass Linus Torvalds Veränderungen am Kernel bekanntlich nur in kleinen Häppchen akzeptiert. Der Austausch der Datentypen und Funktionsnamen wäre zweifelsohne ein dicker Brocken. Dennoch: Wo »rt-mutex« drin ist, sollte auch »rt-mutex« dranstehen! Alles andere führt über kurz oder lang nur zu Missverständnissen und zu unvorhersehbaren Fehlern.
Die Umwandlung von Mutexen zu Spinlocks führt auf Mehrprozessorsystemen zu einem Nebeneffekt, auf den der Programmierer achtgeben sollte: Während ein Spinlock sicherstellt, dass eine zusammenhängende Codesequenz komplett auf einer CPU abläuft, können beim Einsatz eines Mutex mehrere Prozessoren an der Abarbeitung des Programmabschnitts beteiligt sein. Wer Per-CPU-Variablen in seinem Code einsetzt, sollte also gut aufpassen. Nur wer die Preemption über die in [6] erläuterten Funktionen ausschaltet, schließt zusätzliche Race Conditions aus.
Auch wenn normale Kernelentwickler und Treiberprogrammierer wohl kaum einen Einsatzbereich dafür haben, lässt das PREEMPT_RT-Patch auch weiterhin die klassischen Interrupt-Service-Routinen zu. Um das alte Verhalten zu aktivieren, setzen Programmierer beim Aufruf von »request_irq()« das Flag »IRQF_NODELAY«. Damit läuft die ISR direkt beim Auftreten des Interrupts im Interrupt-Kontext ab.
Sind in einem solchen Fall kritische Abschnitte zu schützen, muss das gute alte Spinlock wieder herhalten, das jetzt unter dem Datentyp Raw-Spinlock (»raw_spinlock_t«) firmiert. In Verbindung mit den bekannten Spinlock-Funktionen werden beim Kompilieren daraus durch die erwähnten Makros echte Spinlocks.
Doch noch einmal zurück zum Realtime-Mutex, das nämlich noch mit einer weiteren Eigenschaft aufwarten kann: Es kennt und unterstützt die so genannte Prioritätsvererbung.