Die Thread-Behandlung ist mitentscheidend für die Performance und Parallelisierbarkeit von Linux-Anwendungen. Im folgenden Beitrag geht es darum, wie Threads und Prozesse arbeiten und wie die aktuellen Entwicklungen auf diesem Gebiet aussehen.
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.
Threads im Rahmen von Bibliotheken
Posix-Threads sind nur für den Userspace definiert. Wie der Kernel damit umgeht, ist kein Bestandteil der Spezifikation. Deshalb liegt es nahe, Thread-Bibliotheken zu schreiben, die vom zugrunde liegenden System abstrahieren und so etwa Posix-Kompatibilität herstellen.
Diese Bibliotheks-Funktionen stellen das allgemeine Handling zur Verfügung, das spezielle für das benutzte Unix-System verschwindet in den Interna. Die Funktionen haben dann die Aufgabe, Userspace-Threads auf Kernel-Threads oder Prozesse abzubilden. Die Art dieser Abbildung ist eins der wichtigsten Kriterien, um eine Thread-Bibliothek zu beurteilen. Regelrechte Glaubenskriege um den richtigen Ansatz sind hierbei keine Seltenheit und auch nicht neu. Folgende Verhältnisse sind möglich: 1:n, m:n sowie 1:1.
Der Ansatz 1:n meint eine Strategie, die im Allgemeinen für Unix-Systeme interessant ist, die über keine eigene geeignete Strategie zur Thread- Behandlung verfügen: Die Threads verbleiben im User-Level eines Prozesses, der das gesamte Handling übernimmt. Der Kernel sieht nur diesen Prozess. Das Verhältnis m:n entspricht einem doppelten Lottchen, denn auf der einen Seite benutzt man die Implementation von Threads auf der Kernelebene, die zusätzlich ein weiteres Mal im Rahmen der Bibliotheksfunktionen verwaltet werden. Das Scheduling erfolgt dann sowohl auf der Kernel- als auch auf der Funktionsebene. Im User-Level werden m Threads auf n Threads im Kernel-Level abgearbeitet.
Der Vorteil dieses komplexen Ansatzes: Vor allem bei Multiprozessormaschinen gelingt es so, das System optimal auszulasten. Unter anderem verwenden ältere Thread-Bibliotheken von Solaris und Tru64 diesen Ansatz. Wie die Abbildung auf Kernel-Threads erfolgt, kann der Entwickler selbst bestimmen, in der Praxis wird dabei die Anzahl der Kernel-Threads ein ganzzahliges Vielfaches der CPU-Anzahl sein.
Eine 1:1-Relation lässt sich einfacher implementieren: Das gesamte Scheduling wird vom Kernel realisiert, der Threads unterstützt. Die Bibliothek dient eher dazu, die Kompatibilität zu wahren. Die meisten Thread-Bibliotheken nutzen eine Form der 1:1-Umsetzung.
Ab Mitte der 90er Jahre gab es zahlreiche Ansätze für Thread-Bibliotheken, durchgesetzt haben sich aber letztlich nur die Linuxthreads von Xavier Leroy, implementiert in der Pthread-Lib. Ulrich Drepper hat bei der Entwicklung der Glibc2 die Threading-Funktionen eng an die Standardbibliothek angebunden, seit geraumer Zeit sind also Threads auf jedem Linux-System verfügbar.
Aktuelle Thread-Bibliotheken
Die Pthread-Lib verwendet in ihrer Implementation für Linux eine modifizierte 1:1-Relation, wobei sie immer zumindest ein Leit-Thread produziert; aber alle Threads werden via »clone()« erzeugt. Je interessanter Linux für Serverfarmen und Mehrprozessorsysteme wurde, umso lauter ertönte allerdings auch die Kritik an dieser Thread-Bibliothek. Unzulängliches Handling der Signale, hierarchische statt Peer-Beziehungen zwischen den Threads, nicht ganz komplette Posix-Konformität sowie die Tatsache, dass mit der Pthread-Bibliothek Threads – entgegen aller reinen Theorie – doch in »/proc« auftauchten, waren nur einige der Kritikpunkte.
In letzter Zeit haben sich aus den zahlreich vorhandenen Ansätzen drei Projekte herausgeschält, die besonders aktiv sind. Ralf S. Engelschall startete Gnu Pth[3] bereits 1999, inzwischen steht auch der Versionssprung von 1.4x auf 2.0 unmittelbar bevor. Obwohl es sich um ein offizielles GNU-Projekt handelt, sind die neuesten Betas und Informationen nur auf Ralf Engelschalls eigener Website [www.ossp.org] sowie auf Freshmeat zu finden. Gnu Pth ist als portable Bibliothek für nicht präemptives Multitasking ausgelegt. Sie besitzt einen Posix-Kompatibilitätsmodus und bildet Userspace-Threads 1:n ab. Die zugrunde liegende Theorie hat der Autor ausführlich dargelegt[4].
Fünf Programmierer von Intel und IBM haben Gnu Pth als Ausgangspunkt genommen, um die Next Generation Posix Threads (NGPT) zu entwickeln. Da es für die Auftraggeber vor allem wichtig war, die Skalierung auf Mehrprozessorsysteme zu erhöhen, findet hier ein m:n-Abbildungsmodell Anwendung.
Die aktuelle Version 2.2.0 erschien im Januar 2003 und wird von IBM als stabil bezeichnet. Eine Dokumentation war zum Redaktionsschluss noch nicht verfügbar, im Quellpaket fanden sich nur die Dokus des Ausgangsprojekts Gnu Pth. Die Website kündigt jedoch Whitepaper und Manpages an. Um NGPT zu testen, genügt ein 2.4.19-Kernel, der allerdings zu patchen ist. Eine Installationsanleitung ist verfügbar[5].
Red Hat prescht vor
Die NPTL (Native Posix Thread Library) von Ulrich Drepper und Ingo Molnar – beide bei Red Hat beschäftigt – basiert weiterhin auf dem 1:1-Ansatz, verzichtet aber auf einen Main- oder Manager-Thread und nimmt dafür Modifikationen des Kernels in Kauf (neue Systemcalls zur Unterstützung von Threads, Erweiterung des Clone-Calls, Modifikationen der PIDs und des Signalhandlings, Thread Local Storage). In Red Hat 8.1 wird all das schon enthalten sein.
Drepper und Molnar arbeiten derzeit fieberhaft an der Bibliothek, neue Minor Releases der 0.x-Serie erscheinen wöchentlich oder öfter[6],[7]. NPTL setzt derzeit einen Entwicklerkernel der 2.5er Serie voraus, dazu eine aktuelle Glibc2.3, deren Maintainer ebenfalls Ulrich Drepper ist. Im Netz findet sich eine (inzwischen jedoch möglicherweise veraltete) Installationsanleitung[8]. Binärpakete sind, rückportiert auf den 2.4er Kernel, Teil der Red Hat 8.1 Beta (Phoebe). Die beiden Red-Hat-Spitzenentwickler haben durchaus das Ziel, ihre Thread-Bibliothek zum künftigen Standard in Linux zu machen.
Benchmarks zu NPTL, NGPT und den alten Linuxthreads liegen zwar vor, stammen aber nicht von einer unabhängigen Instanz. Im Herbst 2002 veröffentlichten Molnar und Drepper Benchmark-Ergebnisse, die zeigten, dass ihre Implementation um den Faktor vier besser ist als Linux Threads und um den Faktor zwei besser als NGPT. Inzwischen sind diese aber in der stabilen Version erschienen und die Entwickler reklamieren für sich eine bessere Performance als NPTL. Auch bleibt abzuwarten, was die neue Version von Gnu Pth leistet. Das Rennen ist derzeit also noch offen.
|
Infos |
|
[1] Bovet u. Cesati, “Understanding the Linux-Kernel”: O’Reilly, ISBN 0569-00002-2 [2] J. Cooperstein, “Linux Multithreading Advances”: [http://www.oreillynet.com/pub/a/onlamp/2002/11/07/linux_threads.html] [3] Gnu Pth: [http://www.ossp.org/pkg/lib/pth/] [4] Portable Multithreading, Ralf S. Engelschall: [www.engelschall.com/pw/usenix/2000/pmt.pdf] [5] Next Generation Posix Threads (NGPT): [http://www.ibm.com/developerworks/ oss/pthreads] [6] Download der aktuellen NPTL-Version: [http://people.redhat.com/drepper/nptl/] [7] Drepper u. Molnar, “The Native Posix Thread Library for Linux”: [people.redhat.com/drepper/nptl-design.pdf] [8] NPTL installieren: [https://listman.redhat.com/pipermail/phil-list/2002-November/000275.html] [9] NPTL/NGPT-Benchmarks von Red Hat: [http://people.redhat.com/drepper/perf-s-100000-pro.pdf ] |
|
Der Autor |
|
Wolfgang Hetzler arbeitet als Dozent, Autor und Berater im Unix/Linux-Bereich. |






