Alle modernen Betriebssysteme benutzen präemptives Multitasking, um eine Vielzahl der Aktivitäten gerecht auf die Ressource Prozessor zu verteilen. Dazu gibt es zwei Möglichkeiten: Prozesse und Threads. Prozesse sind der herkömmliche Weg, Threads die modernere Variante, Prozesse behandelt jedes Unix gleich, bei Threads gibt es eine große Vielfalt der Implementierungen.
Threads können das Entwickeln parallelisierter Anwendungen vereinfachen, viele wichtige Applikationen, die ihren Ursprung auf anderen Betriebssystemen haben, benutzen Threads sehr eifrig, Programmiersprachen wie Java bauen sogar darauf auf. Daher kam das Thema vor ein paar Jahren auch für Linux wieder verstärkt auf die Agenda.
Kein Wunder also, dass die Hardware-Industrie hier zum Teil selbst das Heft in die Hand nimmt und die Entwicklung aktiv vorantreibt. Schließlich soll der von interessierten Seiten gern hervorgebrachte Vorwurf "Linux skaliert nicht" bald für immer verstummen. Das Gerangel um die beste Lösung für Linux ist gegenwärtig in vollem Gange, wir stellen die drei aussichtsreichsten Kandidaten vor. Zunächst aber - notgedrungen - ein bisschen Theorie.
Schwergewichte und Leichtathleten
Der Prozessgedanke ist einer der elementaren Aspekte von Unix. Ältere Betriebssysteme kennen ähnliche Begriffe wie Task oder Jobs, also Arbeitseinheiten, denen Betriebsmittel über das Betriebssystem zugeordnet werden. In drei Worten bringt es beispielsweise der Betriebssystem-Professor Andrew Tanenbaum auf den Punkt: Ein Prozess ist ein "Programm in Aus-führung".
Die Implementierung von Prozessen unter Linux weist kaum Unterschiede zu klassischen Unix-Prozessen auf. Der Kernel legt durch das Scheduling fest, wie lange ein Prozess die Ressourcen des Systems in Anspruch nehmen darf. Aber die Scheduling-Algorithmen können je nach Linux- oder Unix-Version unterschiedlich sein. Die einzige Möglichkeit, einen neuen Prozess zu erzeugen, ist der Systemcall »fork()« (oder dessen ältere Variante »vfork()«), den ein schon bestehender Vaterprozess ausführen muss.
Der neue Prozess ist zunächst eine perfekte Kopie des alten, einschließlich Speicherabbild, aller Variablen und Register. Der Kindprozess besitzt also jetzt eine vollständige Kopie des Adressraums des Vaterprozesses für sich, gemeinsam genutzten Speicher gibt es nicht, von Besonderheiten wie Shared Memory natürlich abgesehen. Zur Unterscheidung von Vater- und Kindprozess dient die Prozess-ID. Der Systemcall fork»()« liefert einen positiven Integerwert an den Vaterprozess zurück, der Rückgabewert an den Kindprozess ist »0«.
In der Praxis ist das vollständige Kopieren das Speicherinhalts beim Fork zu aufwändig, da eine ganze Reihe aufeinander folgender Speicherzugriffe nötig ist. Deshalb kommt bei allen modernen Unix-Versionen und auch bei Linux eine Technik namens Copy-on-Write zum Zuge. Der Kindprozess erhält zwar eigene Page Tables, sie zeigen allerdings noch auf die Pages des Vaters und sind für den Kindprozess schreibgeschützt. Erst wenn einer der Prozesse versucht auf eine Page zu schreiben, wird diese tatsächlich kopiert.
Der Aufruf des Systemcalls »fork« geschieht mit der Funktion »do_fork()«, das zentrale Element zum Speichern von Informationen über Prozesse ist die Struktur »task_struct«, der so genannte Prozessdeskriptor. Die Deskriptoren aller Prozesse sind über eine doppelt verkettete Liste verbunden.
Eine besondere Art von Prozessen sind die Threads, in einer anderen Terminologie auch Light Weight Processes (LWP) genannt, die eine Erweiterung des Unix-Standards sind. Linux benutzt dazu den Systemcall »clone()« (»man 2 clone«) den es im Posix-Standard von Unix nicht gibt. Der Aufruf funktioniert ähnlich wie »fork«, nur dass es »clone()« dem Kind-"Prozess" ermöglicht, sich Ressourcen mit dem aufrufenden Prozess zu teilen. Über eine Bitmap, die »sharing_flags«, legt der Programmierer fest, welches Erbe das Kind mitbekommt. In früheren Linux-Versionen bestand diese Bitmap aus 5 Bits, in Version 2.5 ist sie auf 17 angewachsen.
Das wichtigste Bit ist »CLONE_VM«, ist es gesetzt, nutzt der neue Prozess den Adressraum des alten mit und es entsteht ein LWP, also ein Thread. Das Bit »CLONE_FS« regelt, ob Verzeichnisse und die »umask« geteilt werden. Ein gesetztes »CLONE_FILES«-Bit bewirkt, dass die File-Deskriptoren gemeinsam sind und »CLONE_SIGHAND« tut dasselbe für die Signalhandler.
Etwas tückisch ist »CLONE_PID«, hier könnte man vermuten, dass der Programmierer die Wahl hat, den Thread mit der PID des Vaters zu erzeugen. Dem ist aber nicht so. Nur Prozesse mit PID »0«, so genannte »idlestasks«, dürfen dieses Bit setzen. In Mehrprozessorsystemen gibt es eine »idlestask« pro CPU. Im Kernel 2.5 ist »CLONE_PID« in »CLONE_IDLETASK « umbenannt, um diese Verwirrung zu beseitigen.
Eine Besonderheit, die auch zur Verwirrung beiträgt, sind Threads, die gelegentlich Service-Threads, manchmal aber auch Kernel-Threads genannt werden. Das Problem ist, dass auch die bisher erwähnten Threads, da auf Kernelebene implementiert, oft Kernel-Thread heißen. Deren Sonderform, also die Service-Threads, verbringen im Gegensatz zu normalen Prozessen und Threads ihr ganzes Leben im Kernelspace und sind normalerweise dazu da, eine einzige spezifische Kernelfunktion auszuführen. Der erwähnte Prozess mit der PID »0« ist so ein Kernel-Thread. Kernel-Threads werden mit der Funktion »kernel _thread()« erzeugt, die (wie »clone()«) die Funktion »do_fork()« nutzt.
Posix-Threads als gemeinsamer Nenner
Threads haben eine ganze Reihe von Vorteile gegenüber den schwergewichtigen Prozessen. Durch die gemeinsam genutzten Ressourcen ist eine bessere Performance möglich, als wenn die gleiche Aufgabe durch mehrere Prozesse erledigt werden müsste; aufwändige Interprozess-Kommunikation, zum Beispiel mittels Pipes, entfällt. Auf Mehrprozessorsystemen können Threads für eine bessere Auslastung sorgen, indem sich Threads eines Prozesses auf verschiedene CPUs verteilen.
Nachteile gibt es natürlich auch, die Synchronisation von Threads ist sehr komplex. Und schließlich: Alles, was mittels »clone« passiert, ist Linux-spezifisch und nicht portierbar. Da dieses Problem nicht neu ist, sondern in der Unix-Geschichte schon früher auftrat, ist es Teil der großen Standardisierungsbemühung Posix, und zwar im Standard IEEE 1003.1c, er definiert die oft zitierten Posix-Threads.
Abbildung 1: Das »AUTHORS«-File von NGPT zeigt, dass den Autoren auch Nicht-Intel-Systeme wichtig waren, zumindest die von IBM.