Open Source im professionellen Einsatz
Linux-Magazin 01/2005

Native Posix Thread Library - ein Blick hinter die Kulissen

Sauber eingefädelt

Der Standard für Threads unter Linux ist heute die Native Posix Threads Library (NPTL). Die Bibliothek überzeugt durch große Kompatibilität zum Standard und hohe Performance. Dieser Artikel untersucht die neue Threading-Engine und zeigt, wie Benutzeranwendungen davon profitieren.

1428

Vor Kernel 2.6 herrschte bei Threads unter Linux große Verwirrung, denn gleich mehrere Implementationen konkurrierten um den ersten Platz: Linuxthreads, Gnu Pth, NGPT (Next Generation Posix Threads) und NPTL (Native Posix Threads Library). Mittlerweile hat sich die NPTL von Ulrich Drepper und Ingo Molnar durchgesetzt[1].

Verwirrende IDs

Die neuen Threads setzen allerdings eine bessere Thread-Abstraktion im Kernel voraus. Ein Prozess wird nun als Thread-Gruppe abgebildet. Ein Thread entspricht einer Kernel-Task, ein Prozess einer Thread-Gruppe mit einem Thread-Gruppenführer. Einem Kernel-Thread entspricht ein Userspace-Thread, denn NPTL benutzt das 1:1-Scheduling-Modell, siehe Abbildung 1.

Die Struktur »task_t« enthält zwei Werte vom Typ »pid_t«: »pid« ist die Task-ID, »tgid« die Thread-Gruppen-ID. Bei Single-Thread-Prozessen besitzen die beiden IDs den gleichen Wert. Userspace-NPTL-Code bildet die TID auf die Posix-Thread-ID (Thread-Handle) ab. Tabelle 1 stellt alle Identifikatoren der NPTL- Threads gegenüber.

Tabelle 1:
Thread-Identifikatoren

 

User-Mode-IDs

Typ

Funktion (Bibliothek)

Kernel-Mode-Äquivalente

Typ

Posix-Thread-ID

»pthread_t«

»pthread_create()« (NPTL)

-

-

PID (Prozess-ID)

»pid_t«

»getpid()« (Glibc/Systemaufruf)

»task_t:tgid« (Thread-Gruppen-ID)

»pid_t«

TID (Kernel-Thread-ID)

»pid_t«

»clone()«, »gettid()«,
»tkill()« (Systemaufrufe)

»task_t:pid« (Thread-ID)

»pid_t«

Im Kernel erzeugt der Systemaufruf »clone()« einen neuen Thread mit Thread-ID (TID) und »tgid« (PID). Ist beim Aufruf von »clone()« das »CLONE_ THREAD«-Flag gesetzt, wird der neue Thread einer Gruppe zugeordnet. Anwendungsprogrammierer benutzen statt des Systemaufrufs besser die Posix-Schnittstelle. Als ID stehen ihnen der Thread-Handle und die PID zur Verfügung, die TID ist ein privates Detail der Kernel-Implementierung. Für Neugierige gibt es trotzdem zwei TID-bezogene Systemaufrufe, die keine entsprechenden Glibc-Wrapper-Funktionen besitzen: »gettid()« gibt die TID zurück, »tkill()«, sendet dem Thread ein Signal.

Das C++-Programm in Listing 1 zeigt die verschiedenen IDs an. Der Haupt-Thread erzeugt zunächst mit »pthread_create()« einen Arbeits-Thread und wartet dann auf ihn. Optional versteht das Programm eine Kommandozeilenoption für eine Verzögerung in Sekunden. Damit hat man beim Testen Gelegenheit, »ps« auszuführen und die Ergebnisse zu vergleichen. Zur Übersetzung des Code siehe den Kasten "Kompilieren".

Kompilieren

Alle hier aufgeführten Programme lassen sich mit dem Befehl »make Programm LDFLAGS=-lpthread« übersetzen, wobei Programm durch den Namen der jeweiligen C++-Datei zu ersetzen ist. In der gedruckten Version fehlen der Übersichtlichkeit halber die Includes. Die vollständigen Listings sind auf dem Server des Linux-Magazins [3] zu finden.

Die Ausgabe des kompilierten Programms sieht so aus:

# ./ids
Main thread: PID=3775, pthread_t=
  4143878816, TID=3775
Worker thread: PID=3775, pthread_t=
  4143877040, TID=3776


Die beiden Threads besitzen die gleiche PID, die der »getpid()«-Systemaufruf zurückgibt. Der Haupt-Thread hat die gleichen PID- und TID-Werte, weil er Thread-Gruppenführer ist. Das Kommando »ps« zeigt den Prozess an und die einzelnen Threads mit der Option »-m«. TIDs werden vom generischen PID-Allokator erzeugt, der durch einen cleveren Algorithmus von Molnar deutlich schneller und mit konstanter Komplexität O(1) arbeitet.

Das folgende Beispiel zeigt das Verhalten desselben Programms mit Linuxthreads. Der Kasten "Rückwärtskompatibilität" erklärt, wie man Linuxthreads aktiviert:

# export LD_ASSUME_KERNEL=2.4.1
# getconf GNU_LIBPTHREAD_VERSION
linuxthreads-0.10
# ./ids
Main thread: PID=3824, pthread_t=16384, U TID=3824
Worker thread: PID=3826, pthread_t=16386, U TID=3826


Im Gegensatz zu den alten Linuxthreads benutzt NPTL keinen Manager-Thread, dessen Aufgaben hat der Kernel übernommen.

Abbildung 1: Drei Modelle ordnen Threads von User- und Kernelspace unterschiedlich zu. NPTL arbeitet nach dem 1:1-Modell.

Vaterschaftstest

Mit LWP-Threads gab es noch ein grundsätzliches Problem mit der Exec-Familie von Systemaufrufen: Wenn ein ande- rer als der Haupt-Thread eines Multi-Thread-Prozesses (MT-Prozesses) einen Prozess durch »fork()« und »exec...()« startet, erhält der Kindprozess als PPID (Parent Process ID) die Vater-Thread-PID. NPTL korrigiert dieses Fehlverhalten. In Listing 2 kreiert der Haupt-Thread einen Arbeits-Thread, der Mozilla startet. Die Ausgabe sieht unter Verwendung von NPTL so aus:

# ./spawn
I'm main thread in the parent process: 19322
I'm forked process: 19324 from parent: 19322


Der Befehl »ps« zeigt zusätzlich die Mozilla-PID, die auch in der zweiten Meldung zu sehen ist.

Mit Linuxthreads ergibt sich ein anderes Resultat:

# export LD_ASSUME_KERNEL=2.4.1
# ./spawn
I'm main thread in the parent process: 19337
I'm forked process: 19340 from parent: 19339

Rückwärtskompatibilität

NPTL ist binärkompatibel zu Linuxthreads. Alle Anwendungen, die sich an den Posix-Standard halten, laufen weiterhin problemlos mit NPTL. Anwendungen, die weiterhin Linuxthreads benutzen möchten, haben folgenden Mechanismus zur Verfügung: Die bekannten Distributionen (Red Hat Linux 9.0, Suse Linux 9.1, Fedora Core 1, 2) liefern drei Versionen der »libpthread.so«:

  • Im Verzeichnis »/lib« befindet sich Linuxthreads
    ohne Floating Stacks (OS ABI 2.2.5).
  • »/lib/i686« enthält Linuxthreads mit
    Floating-Stacks (OS ABI 2.4.1).
  • Die NPTL-Version findet sich in »/lib/tls« (OS ABI
    2.4.20).

In jedem der Verzeichnisse gibt es passende Runtime-Bibliotheken. Die OS-ABI-Versionen beziehen sich auf Red-Hat-Kernel. NPTL ist ab Kernel 2.5.36 verfügbar, Red Hat hat aber die nötigen Änderungen auf Version 2.4.20 rückportiert. Suse kommt ab Version 9.1 mit Kernel 2.6 und mit Glibc2.3/NPTL.

Der dynamische Linker erfragt den Wert der Umgebungsvariablen »LD_ASSUME_KERNEL« und lädt die passende Bibliothek. Die Konfigurationsvariable »GNU_LIBPTHREAD_VERSION« zeigt die aktuelle Bibliotheksversion:

# getconf GNU_LIBPTHREAD_VERSION
NPTL 0.61
# export LD_ASSUME_KERNEL=2.4.1
# getconf GNU_LIBPTHREAD_VERSION
linuxthreads-0.10


Dieselbe Information zeigt auch der folgende Befehl:

`ldd Programm | grep libc.so.6
 | cut -d' ' -f 3` | grep -i 
 'linuxthreads|nptl'


Die NPTL-Bibliothek lässt sich sogar direkt ausführen:

# /lib/tls/libpthread.so.0
NPTL 0.61 by Ulrich Drepper
Copyright (C) 2003 Free Software 
Foundation, Inc ...

Listing 1:
»ids.cpp«

01 void printThreadInfo(const char* pstrThread)
02 {
03   cout << pstrThread << ": PID=" << getpid() << ", pthread_t=" 
04    << pthread_self() << ", TID=" << syscall(__NR_gettid) << endl;
05 }
06 
07 void* doWork(void* pArg)
08 {
09   printThreadInfo("Worker thread");
10   int nSeconds = *(int*)pArg;
11   sleep(nSeconds);
12 }
13 
14 int main(int argc, char* const argv[])
15 {
16   printThreadInfo("Main thread");
17   int nSeconds = (argc > 1) ? atoi(argv[1]) : 0;
18   pthread_t hThread;
19   pthread_create(&hThread, 0, &doWork, (void*)&nSeconds);
20   pthread_join(hThread, 0);
21   return 0;
22 };

Hier ist die Haupt-Thread-PID 19337, die Arbeits-Thread-PID 19339 und die PID des Kindprozesses 19340. Die PPID des Kindprozesses ist 19339, weil ihn der Arbeits-Thread gestartet hat.

Die Routine »pthread_atfork()« registriert Funktionen, die das System unmittelbar vor und nach dem Systemaufruf »fork()« im Vater- und Kindprozess ausführt. Im Gegensatz zu Linuxthreads werden mit der NPTL diese Funktionen bei »vfork()« nicht ausgeführt.

Alle NPTL-Threads eines Prozesses dürfen gleichberechtigt auf den Kindprozess warten, wenn der Programmierer die »wait...()«-Systemaufrufe verwendet. Das geht mit Linuxthreads nicht. In Listing 3 forkt der Arbeits-Thread einen Prozess, auf den der Haupt-Thread wartet. Mit NPTL ist das Warten erfolgreich, bei Linuxthreads liefert die »waitpid()«-Funktion den Fehlercode »ECHILD«.

# ./wait
waitpid successfull!
# export LD_ASSUME_KERNEL=2.4.1
# ./wait
waitpid failed: : No child processes


Eines der wichtigsten Designziele für NPTL war gute Skalierbarkeit. Die ersten Benchmarks zeigten sehr gute Ergebnisse. Entwickler Ingo Molnar erzeugte 100000 Threads in etwa 2 Sekunden - der gleiche Test mit Linuxthreads dauert 15 Minuten! Obwohl die meisten Anwendungen nur wenige Threads benutzen, gibt es auch solche, die Hunderte von Threads erzeugen: Serveranwendungen, die für jeden Request oder jede Verbindung einen Thread starten, oder virtuelle Maschinen mit Thread-Pools und asynchronen Frameworks. Damit das Betriebssystem mit der steigenden Anzahl von Threads gut skaliert, sollen Kernelroutinen möglichst konstante Komplexität besitzen.

Listing 2:
»spawn.cpp«

01 void* doWork(void* pArg)
02 {
03   pid_t nChildPID = fork();
04   if (0 < nChildPID)
05     waitpid(nChildPID, 0, 0);
06   else if (0 == nChildPID)
07   {
08     cout << "I'm forked process: " << getpid() << " from parent: " << getppid() << endl;
09     execvp ("mozilla", 0);
10     exit(1);
11   }
12   return 0;
13 }
14 
15 int main (int argc, char* const argv[])
16 {
17   cout << "I'm main thread in the parent process: " << getpid() << endl;
18   pthread_t hThread;
19   pthread_create(&hThread, 0, &doWork, 0);
20   pthread_join(hThread, 0);
21   return 0;
22 };

Listing 3:
»wait.cpp«

01 static pid_t nChildPID = 0;
02 
03 void* doWork(void* pArg)
04 {
05   nChildPID = fork ();
06   if (0 == nChildPID)
07   {
08     exit(0);
09   }
10   return 0;
11 }
12 
13 int main (int argc, char* const argv[])
14 {
15   pthread_t hThread;
16   pthread_create(&hThread, 0, &doWork, 0);
17   pthread_join(hThread, 0);
18   pid_t pid = -1;
19   if (0 >= (pid = waitpid(nChildPID, 0, 0)))
20     perror("waitpid failed: ");
21   else
22   {
23     assert(pid == nChildPID);
24     cout << "waitpid successfull!" << endl;
25   }
26 
27   return 0;
28 };

Listing 4:
»sigstop.cpp«

01 void* doWork(void* pArg)
02 {
03   for (int i = 0; i < 4; ++i)
04   {
05     cout << "Worker thread doing the job!" << endl;
06     sleep(1);
07   }
08   return 0;
09 }
10 
11 int main (int argc, char* const argv[])
12 {
13   pthread_t hThread;
14   pthread_create(&hThread, 0, &doWork, 0);
15   sleep(2);
16   cout << "Main thread sending SIGSTOP to worker thread!" << endl;
17   pthread_kill(hThread, SIGSTOP);
18 
19   for (int i = 0; i < 2; ++i)
20   {
21     cout << "Main thread doing the job!" << endl;
22     sleep(1);
23   }
24 
25   cout << "Main thread sending SIGCONT to worker thread!" << endl;
26   pthread_kill(hThread, SIGCONT);
27   pthread_join(hThread, 0);
28   return 0;
29 };

Linux-Magazin kaufen

Einzelne Ausgabe
 
Abonnements
 
TABLET & SMARTPHONE APPS
Bald erhältlich
Get it on Google Play

Deutschland

Ähnliche Artikel

  • Klon-Debatte

     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.

  • Baukasten

    Mit den Threading Buildings Blocks 1.0 bietet Intel für Parallelprogrammierung ein C++-Template-basiertes kommerzielles Framework an. Zwei Intel-Entwickler führen in das komplexe Konzept ein, das sich für alle Multicore- und SMP-Systeme eignet, und stellen wichtige Features vor.

  • C++11

    Die neue Zeitbibliothek von C++11 erweist sich als elementarer Bestandteil der Threading-Schnittstelle: Sowohl Threads, Locks und Bedingungsvariablen als auch Futures haben ein Verständnis von Zeit. Dank ihrer Unterstützung kann ein Entwickler unterschiedliche Wartestrategien verfolgen.

  • C++11

    Die C++11-Reihe beschäftigt sich weiter mit dem Synchronisieren von Threads. Diesmal setzt der Chef-Thread Bedingungsvariablen ein, um die Tätigkeit seiner Mitarbeiter-Threads zu koordinieren.

  • C++11

    2014 ist ein besonderes Jahr für C++. Drei Jahre nach C++11 erfährt der Sprachstandard mit C++14 den letzten Feinschliff. Neben generischen Lambda-Funktionen und der vereinfachten Ermittlung des Rückgabetyps kann C++14 vor allem mit einem Feature punkten: Reader-Writer-Locks.

comments powered by Disqus

Stellenmarkt

Artikelserien und interessante Workshops aus dem Magazin können Sie hier als Bundle erwerben.