it-swarm.com.de

Sind Threads unter Linux als Prozesse implementiert?

Ich gehe durch dieses Buch , Advanced Linux Programming von Mark Mitchell, Jeffrey Oldham und Alex Samuel. Es ist aus dem Jahr 2001, also ein bisschen alt. Aber ich finde es trotzdem ziemlich gut.

Ich kam jedoch zu einem Punkt, an dem es von dem abweicht, was mein Linux in der Shell-Ausgabe produziert. Auf Seite 92 (116 im Viewer) beginnt das Kapitel 4.5 GNU/Linux-Thread-Implementierung mit dem Absatz, der diese Anweisung enthält:

Die Implementierung von POSIX-Threads unter GNU/Linux unterscheidet sich von der Thread-Implementierung auf vielen anderen UNIX-ähnlichen Systemen in einem wichtigen Punkt: Unter GNU/Linux werden Threads als Prozesse implementiert.

Dies scheint ein wichtiger Punkt zu sein und wird später mit einem C-Code veranschaulicht. Die Ausgabe im Buch ist:

main thread pid is 14608
child thread pid is 14610

Und in meinem Ubuntu 16.04 ist es:

main thread pid is 3615
child thread pid is 3615

ps Ausgabe unterstützt dies.

Ich denke, zwischen 2001 und jetzt muss sich etwas geändert haben.

Das nächste Unterkapitel auf der nächsten Seite, 4.5.1 Signalbehandlung, baut auf der vorherigen Aussage auf:

Das Verhalten der Interaktion zwischen Signalen und Threads variiert von einem UNIX-ähnlichen System zum anderen. In GNU/Linux wird das Verhalten durch die Tatsache bestimmt, dass Threads als Prozesse implementiert werden.

Und es sieht so aus, als würde dies später in diesem Buch noch wichtiger werden. Könnte jemand erklären, was hier los ist?

Ich habe diesen gesehen Sind Linux-Kernel-Threads wirklich Kernel-Prozesse? , aber es hilft nicht viel. Ich bin verwirrt.

Dies ist der C-Code:

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void* thread_function (void* arg)
{
    fprintf (stderr, "child thread pid is %d\n", (int) getpid ());
    /* Spin forever. */
    while (1);
    return NULL;
}

int main ()
{
    pthread_t thread;
    fprintf (stderr, "main thread pid is %d\n", (int) getpid ());
    pthread_create (&thread, NULL, &thread_function, NULL);
    /* Spin forever. */
    while (1);
    return 0;
}
66
user147505

Ich denke, dieser Teil der clone(2) Manpage kann den Unterschied bezüglich aufklären. die PID:

CLONE_THREAD (seit Linux 2.4.0-test8)
Wenn CLONE_THREAD festgelegt ist, wird das untergeordnete Element in dieselbe Threadgruppe wie der aufrufende Prozess eingefügt.
Thread-Gruppen wurden in Linux 2.4 hinzugefügt, um den POSIX-Thread-Begriff einer Reihe von Threads zu unterstützen, die eine einzelne PID gemeinsam nutzen. Intern ist diese gemeinsam genutzte PID die sogenannte Thread Group Identifier (TGID) für die Thread Group. Seit Linux 2.4 geben Aufrufe von getpid (2) die TGID des Aufrufers zurück.

Der Ausdruck "Threads werden als Prozesse implementiert" bezieht sich auf das Problem von Threads, die in der Vergangenheit separate PIDs hatten. Grundsätzlich hatte Linux ursprünglich keine Threads innerhalb eines Prozesses, sondern nur separate Prozesse (mit separaten PIDs), die möglicherweise über gemeinsam genutzte Ressourcen wie virtuellen Speicher oder Dateideskriptoren verfügten. CLONE_THREAD und die Trennung der Prozess-ID(*) Durch die Thread-ID ähnelt das Linux-Verhalten eher anderen Systemen und in diesem Sinne eher den POSIX-Anforderungen. Obwohl das Betriebssystem technisch immer noch keine separaten Implementierungen für Threads und Prozesse hat.

Die Signalbehandlung war ein weiterer problematischer Bereich bei der alten Implementierung. Dies wird ausführlicher in Papier @FooF bezieht sich auf in ihrer Antwort .

Wie in den Kommentaren erwähnt, wurde Linux 2.4 auch im Jahr 2001 veröffentlicht, im selben Jahr wie das Buch. Es ist also nicht verwunderlich, dass die Nachrichten nicht zu diesem Druck kamen.

51
ilkkachu

Sie haben Recht, tatsächlich "muss sich zwischen 2001 und jetzt etwas geändert haben". Das Buch, das Sie lesen, beschreibt die Welt gemäß der ersten historischen Implementierung von POSIX-Threads unter Linux, genannt LinuxThreads (siehe auch Wikipedia Artikel für einige).

LinuxThreads hatte einige Kompatibilitätsprobleme mit dem POSIX-Standard - zum Beispiel Threads, die keine PIDs gemeinsam nutzen - und einige andere schwerwiegende Probleme. Um diese Fehler zu beheben, wurde eine weitere Implementierung namens NPTL (Native POSIX Thread Library) von Red Hat angeführt, um die erforderliche Unterstützung für Kernel- und User Space-Bibliotheken hinzuzufügen, um eine bessere POSIX-Konformität zu erreichen (wobei gute Teile aus einem weiteren konkurrierenden Neuimplementierungsprojekt von IBM namens NGPT übernommen wurden). Posix-Threads der nächsten Generation "), siehe Wikipedia-Artikel zu NPTL ). Die zusätzlichen Flags, die dem Systemaufruf clone(2) hinzugefügt wurden (insbesondere CLONE_THREAD Das @ikkkachu weist darauf hin, dass seine Antwort ) wahrscheinlich der offensichtlichste Teil der Kernel-Modifikationen ist. Der User Space-Teil der Arbeit wurde schließlich in die GNU C Library) aufgenommen.

Noch heute verwenden einige eingebettete Linux-SDKs die alte LinuxThreads-Implementierung, da sie eine Version von LibC mit kleinerem Speicherbedarf namens Clibc (auch als µClibc bezeichnet) verwenden. Es dauerte einige Jahre, bis die Implementierung des NPTL-Benutzerbereichs erfolgte GNU LibC wurde portiert und als Standard-POSIX-Threading-Implementierung angenommen, da diese speziellen Plattformen im Allgemeinen nicht danach streben, zu folgen Dies kann beobachtet werden, indem festgestellt wird, dass PIDs für verschiedene Threads auf diesen Plattformen auch anders sind als im POSIX-Standard angegeben - genau wie in dem Buch, das Sie lesen, beschrieben wird. Tatsächlich haben Sie einmal pthread_create() aufgerufen. Sie hatten plötzlich die Anzahl der Prozesse von eins auf drei erhöht, da zusätzliche Prozesse erforderlich waren, um das Durcheinander zusammenzuhalten.

Die Handbuchseite Linux pthreads (7) bietet einen umfassenden und interessanten Überblick über die Unterschiede zwischen den beiden. Eine andere aufschlussreiche, wenn auch veraltete Beschreibung der Unterschiede ist diese Papier von Ulrich Depper und Ingo Molnar über das Design von NPTL.

Ich empfehle Ihnen, diesen Teil des Buches nicht zu ernst zu nehmen. Ich empfehle stattdessen Butenhofs Programmier-POSIX-Threads sowie POSIX- und Linux-Handbuchseiten zu diesem Thema. Viele Tutorials zu diesem Thema sind ungenau.

38
FooF

(Userspace-) Threads werden unter Linux nicht als solche Prozesse implementiert, da sie keinen eigenen privaten Adressraum haben und dennoch den Adressraum des übergeordneten Prozesses gemeinsam nutzen.

Diese Threads sind jedoch für die Verwendung des Kernel-Prozessabrechnungssystems implementiert. Sie erhalten daher eine eigene Thread-ID (TID), erhalten jedoch dieselbe PID und 'Thread-Gruppen-ID' (TGID) wie der übergeordnete Prozess - im Gegensatz dazu Eine Gabelung, in der eine neue TGID und PID erstellt werden und die TID mit der PID identisch ist.

Es scheint also, dass die letzten Kernel eine separate TID hatten, die abgefragt werden kann. Dies ist für Threads unterschiedlich. Ein geeignetes Code-Snippet, um dies in jeder der oben genannten main () thread_function () anzuzeigen, ist:

    long tid = syscall(SYS_gettid);
    printf("%ld\n", tid);

Der gesamte Code mit diesem wäre also:

#include <pthread.h>                                                                                                                                          
#include <stdio.h>                                                                                                                                            
#include <unistd.h>                                                                                                                                           
#include <syscall.h>                                                                                                                                          

void* thread_function (void* arg)                                                                                                                             
{                                                                                                                                                             
    long tid = syscall(SYS_gettid);                                                                                                                           
    printf("child thread TID is %ld\n", tid);                                                                                                                 
    fprintf (stderr, "child thread pid is %d\n", (int) getpid ());                                                                                            
    /* Spin forever. */                                                                                                                                       
    while (1);                                                                                                                                                
    return NULL;                                                                                                                                              
}                                                                                                                                                             

int main ()                                                                                                                                                   
{                                                                                                                                               
    pthread_t thread;                                                                               
    long tid = syscall(SYS_gettid);     
    printf("main TID is %ld\n", tid);                                                                                             
    fprintf (stderr, "main thread pid is %d\n", (int) getpid ());                                                    
    pthread_create (&thread, NULL, &thread_function, NULL);                                           
    /* Spin forever. */                                                                                                                                       
    while (1);                                                                                                                                                
    return 0;                                                                                                                                                 
} 

Geben Sie eine Beispielausgabe von:

main TID is 17963
main thread pid is 17963
thread TID is 17964
child thread pid is 17963
21
einonm

Intern gibt es keine Prozesse oder Threads im Linux-Kernel. Prozesse und Threads sind meistens ein Userland-Konzept. Der Kernel selbst sieht nur "Aufgaben", bei denen es sich um ein planbares Objekt handelt, das möglicherweise keine, einige oder alle seiner Ressourcen mit anderen Aufgaben teilt. Threads sind Aufgaben, die so konfiguriert wurden, dass sie die meisten Ressourcen (Adressraum, mmaps, Pipes, Open File Handler, Sockets usw.) für die übergeordnete Aufgabe freigeben. Prozesse sind Aufgaben, die so konfiguriert wurden, dass sie nur minimale Ressourcen für die übergeordnete Aufgabe freigeben .

Wenn Sie die Linux-API direkt ( clone () anstelle von fork () und pthread_create () ) verwenden, haben Sie viel mehr Flexibilität Wenn Sie festlegen, wie viele Ressourcen freigegeben oder nicht freigegeben werden sollen, können Sie Aufgaben erstellen, die weder vollständig ein Prozess noch vollständig ein Thread sind. Wenn Sie diese Aufrufe auf niedriger Ebene direkt verwenden, können Sie auch eine Aufgabe mit einer neuen TGID erstellen (die von den meisten Userland-Tools als Prozess behandelt wird), die tatsächlich alle Ressourcen für die übergeordnete Aufgabe gemeinsam nutzt, oder umgekehrt Eine Aufgabe mit gemeinsam genutzter TGID (die von den meisten Userland-Tools als Thread behandelt wird), die keine Ressource mit ihrer übergeordneten Aufgabe gemeinsam nutzt.

Während Linux 2.4 TGID implementiert, dient dies meist nur der Ressourcenabrechnung. Viele Benutzer und Userspace-Tools finden es nützlich, verwandte Aufgaben zu gruppieren und ihre Ressourcennutzung zusammen zu melden.

Die Implementierung von Aufgaben unter Linux ist viel flüssiger als die Prozess- und Thread-Weltanschauung, die von Userspace-Tools dargestellt wird.

9
Lie Ryan

Grundsätzlich sind die Informationen in Ihrem Buch aufgrund einer schändlich schlechten Implementierungshistorie von Threads unter Linux historisch korrekt. Diese Antwort von mir auf eine verwandte Frage zu SO dient auch als Antwort auf Ihre Frage:

https://stackoverflow.com/questions/9154671/distinction-between-processes-and-threads-in-linux/9154725#9154725

Diese Verwirrungen beruhen alle auf der Tatsache, dass die Kernel-Entwickler ursprünglich eine irrationale und falsche Ansicht hatten, dass Threads fast vollständig im Benutzerraum implementiert werden könnten, indem Kernel-Prozesse als Grundelement verwendet werden, solange der Kernel eine Möglichkeit bietet, sie dazu zu bringen, Speicher- und Dateideskriptoren gemeinsam zu nutzen . Dies führte zu der notorisch schlechten LinuxThreads-Implementierung von POSIX-Threads, die eher eine Fehlbezeichnung war, da sie nichts gab, was der POSIX-Thread-Semantik im entferntesten ähnelte. Schließlich wurde LinuxThreads (durch NPTL) ersetzt, aber viele der verwirrenden Begriffe und Missverständnisse bleiben bestehen.

Das erste und wichtigste zu erkennende ist, dass "PID" verschiedene Dinge im Kernel- und Benutzerbereich bedeutet. Was der Kernel PIDs nennt, sind Thread-IDs auf Kernel-Ebene (oft als TIDs bezeichnet), nicht zu verwechseln mit pthread_t Dies ist eine separate Kennung. Jeder Thread im System, egal ob im selben oder in einem anderen Prozess, hat eine eindeutige TID (oder "PID" in der Terminologie des Kernels).

Was als PID im POSIX-Sinne von "Prozess" betrachtet wird, wird im Kernel als "Thread-Gruppen-ID" oder "TGID" bezeichnet. Jeder Prozess besteht aus einem oder mehreren Threads (Kernel-Prozessen) mit jeweils einer eigenen TID (Kernel-PID), die jedoch alle dieselbe TGID verwenden, die der TID (Kernel-PID) des ursprünglichen Threads entspricht, in dem main läuft.

Wenn top Ihnen Threads anzeigt, werden TIDs (Kernel-PIDs) und keine PIDs (Kernel-TGIDs) angezeigt. Aus diesem Grund hat jeder Thread einen eigenen.

Mit dem Aufkommen von NPTL wurden die meisten Systemaufrufe, die ein PID-Argument annehmen oder auf den aufrufenden Prozess reagieren, geändert, um die PID als TGID zu behandeln und darauf zu reagieren die gesamte "Thread-Gruppe" (POSIX-Prozess).

Linus Torvalds erklärte 1996 in einem Kernel-Mailinglistenbeitrag, dass "sowohl Threads als auch Prozesse als 'Kontext der Ausführung' behandelt werden", was "nur ein Konglomerat des gesamten Zustands dieses CoE ist ... Dinge wie CPU enthält state, MMU state, Berechtigungen und verschiedene Kommunikationszustände (geöffnete Dateien, Signalhandler usw.) ".

// simple program to create threads that simply sleep
// compile in debian jessie with apt-get install build-essential
// and then g++ -O4 -Wall -std=c++0x -pthread threads2.cpp -o threads2
#include <string>
#include <iostream>
#include <thread>
#include <chrono>

// how many seconds will the threads sleep for?
#define SLEEPTIME 100
// how many threads should I start?
#define NUM_THREADS 25

using namespace std;

// The function we want to execute on the new thread.
void threadSleeper(int threadid){
    // output what number thread we've created
    cout << "task: " << threadid << "\n";
    // take a nap and sleep for a while
    std::this_thread::sleep_for(std::chrono::seconds(SLEEPTIME));
}

void main(){
    // create an array of thread handles
    thread threadArr[NUM_THREADS];
    for(int i=0;i<NUM_THREADS;i++){
        // spawn the threads
        threadArr[i]=thread(threadSleeper, i);
    }
    for(int i=0;i<NUM_THREADS;i++){
        // wait for the threads to finish
        threadArr[i].join();
    }
    // program done
    cout << "Done\n";
    return;
}

Wie Sie sehen können, erzeugt dieses Programm 25 Threads gleichzeitig, von denen jeder 100 Sekunden lang in den Ruhezustand versetzt wird und dann erneut dem Hauptprogramm beitritt. Nachdem alle 25 Threads wieder dem Programm beigetreten sind, ist das Programm fertig und wird beendet.

Mit top können Sie 25 Instanzen des Programms "threads2" sehen. Aber Kidna langweilig. Die Ausgabe von ps auwx ist noch weniger interessant ... ABER ps -eLf wird irgendwie aufregend.

UID        PID  PPID   LWP  C NLWP STIME TTY          TIME CMD
debian     689   687   689  0    1 14:52 ?        00:00:00 sshd: [email protected]/0  
debian     690   689   690  0    1 14:52 pts/0    00:00:00 -bash
debian    6217   690  6217  0    1 15:04 pts/0    00:00:00 screen
debian    6218  6217  6218  0    1 15:04 ?        00:00:00 SCREEN
debian    6219  6218  6219  0    1 15:04 pts/1    00:00:00 /bin/bash
debian    6226  6218  6226  0    1 15:04 pts/2    00:00:00 /bin/bash
debian    6232  6219  6232  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6233  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6234  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6235  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6236  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6237  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6238  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6239  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6240  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6241  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6242  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6243  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6244  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6245  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6246  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6247  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6248  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6249  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6250  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6251  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6252  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6253  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6254  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6255  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6256  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6257  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6260  6226  6260  0    1 15:04 pts/2    00:00:00 ps -eLf

Sie können hier alle 26 CoEs sehen, die die thread2 Programm hat erstellt. Sie alle haben dieselbe Prozess-ID (PID) und übergeordnete Prozess-ID (PPID), aber jede hat eine andere LWP-ID (Lightweight-Prozess), und die Anzahl der LWPs (NLWP) gibt an, dass es 26 CoEs gibt - das Hauptprogramm und das 25 Threads davon erzeugt.

5
ivanivan

Wenn es um Linux geht, sind Prozesse und Threads irgendwie dasselbe. Das heißt, sie werden mit demselben Systemaufruf erstellt: clone.

Wenn Sie darüber nachdenken, besteht der Unterschied zwischen Threads und Prozessen darin, in welchen Kernelobjekten das Kind und das Elternteil gemeinsam genutzt werden. Für Prozesse ist es nicht viel: offene Dateideskriptoren, Speichersegmente, in die nicht geschrieben wurde, wahrscheinlich einige andere, an die ich auf Anhieb nicht denken kann. Bei Threads werden viel mehr Objekte gemeinsam genutzt, jedoch nicht alle.

Was Threads und Objekte unter Linux näher bringt, ist der Systemaufruf unshare. Kernel-Objekte, die als gemeinsam genutzt beginnen, können nach der Thread-Erstellung freigegeben werden. So können Sie beispielsweise zwei Threads desselben Prozesses mit unterschiedlichem Dateideskriptorbereich haben (indem Sie die Freigabe von Dateideskriptoren nach dem Erstellen der Threads widerrufen). Sie können es selbst testen, indem Sie einen Thread erstellen, unshare in beiden Threads aufrufen und dann alle Dateien schließen und neue Dateien, Pipes oder Objekte in beiden Threads öffnen. Dann schauen Sie in /proc/your_proc_fd/task/*/fd und du wirst sehen, dass jedes task (das du als Thread erstellt hast) unterschiedliche fd's hat.

Tatsächlich sind sowohl das Erstellen neuer Threads als auch neuer Prozesse Bibliotheksroutinen, die clone darunter aufrufen und angeben, welches der Kernelobjekte das neu erstellte Prozess-Thread-Dingamajig (dh task) wird mit dem aufrufenden Prozess/Thread teilen.

3