web-dev-qa-db-fra.com

Les threads sont-ils implémentés en tant que processus sous Linux?

Je passe par ce livre , Advanced Linux Programming de Mark Mitchell, Jeffrey Oldham et Alex Samuel. Ça date de 2001, donc un peu vieux. Mais je trouve ça assez bon de toute façon.

Cependant, je suis arrivé à un point où il diffère de ce que mon Linux produit dans la sortie Shell. À la page 92 (116 dans la visionneuse), le chapitre 4.5 Implémentation de thread GNU/Linux commence par le paragraphe contenant cette déclaration:

L'implémentation des threads POSIX sur GNU/Linux diffère de l'implémentation des threads sur de nombreux autres systèmes de type UNIX: sur GNU/Linux, les threads sont implémentés en tant que processus.

Cela semble être un point clé et est illustré plus tard avec un code C. La sortie dans le livre est:

main thread pid is 14608
child thread pid is 14610

Et dans mon Ubuntu 16.04 c'est:

main thread pid is 3615
child thread pid is 3615

ps la sortie le supporte.

Je suppose que quelque chose a dû changer entre 2001 et maintenant.

Le sous-chapitre suivant de la page suivante, 4.5.1 Gestion du signal, s'appuie sur l'instruction précédente:

Le comportement de l'interaction entre les signaux et les threads varie d'un système de type UNIX à l'autre. Sous GNU/Linux, le comportement est dicté par le fait que les threads sont implémentés en tant que processus.

Et il semble que cela sera encore plus important plus tard dans le livre. Quelqu'un pourrait-il expliquer ce qui se passe ici?

J'ai vu celui-ci Les threads du noyau Linux sont-ils vraiment des processus du noyau? , mais cela n'aide pas beaucoup. Je suis confus.

Voici le code C:

#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

Je pense que cette partie de la page de manuel clone(2) peut clarifier la différence. le PID:

CLONE_THREAD (depuis Linux 2.4.0-test8)
Si CLONE_THREAD est défini, l'enfant est placé dans le même groupe de threads que le processus appelant.
Les groupes de threads étaient une fonctionnalité ajoutée dans Linux 2.4 pour prendre en charge la notion de threads POSIX d'un ensemble de threads partageant un seul PID. En interne, ce PID partagé est ce que l'on appelle l'identificateur de groupe de threads (TGID) pour le groupe de threads. Depuis Linux 2.4, les appels à getpid (2) renvoient le TGID de l'appelant.

La phrase "les threads sont implémentés en tant que processus" fait référence au problème des threads ayant eu des PID distincts dans le passé. Fondamentalement, Linux n'avait pas à l'origine de threads dans un processus, juste des processus séparés (avec des PID distincts) qui pouvaient avoir des ressources partagées, comme la mémoire virtuelle ou les descripteurs de fichiers. CLONE_THREAD et la séparation de l'ID de processus(*) et l'ID de thread font que le comportement de Linux ressemble plus à d'autres systèmes et plus aux exigences POSIX dans ce sens. Bien que techniquement, le système d'exploitation n'ait toujours pas d'implémentations distinctes pour les threads et les processus.

La gestion des signaux était un autre domaine problématique avec l'ancienne implémentation, cela est décrit plus en détail dans le papier @FooF fait référence à dans leur réponse .

Comme indiqué dans les commentaires, Linux 2.4 a également été publié en 2001, la même année que le livre, il n'est donc pas surprenant que les nouvelles n'aient pas été publiées.

51
ilkkachu

Vous avez raison, en effet "quelque chose doit avoir changé entre 2001 et maintenant". Le livre que vous lisez décrit le monde selon la première implémentation historique des threads POSIX sur Linux, appelée LinuxThreads (voir aussi Wikipedia article pour certains).

LinuxThreads avait quelques problèmes de compatibilité avec la norme POSIX - par exemple des threads ne partageant pas de PID - et quelques autres problèmes graves. Pour corriger ces failles, une autre implémentation appelée NPTL (Native POSIX Thread Library) a été dirigée par Red Hat pour ajouter la prise en charge nécessaire du noyau et de la bibliothèque d'espace utilisateur pour atteindre une meilleure conformité POSIX (en prenant bonne partie d'un autre projet de réimplémentation concurrent d'IBM appelé NGPT (" Next Posix Threads Threads "), voir article Wikipedia sur NPTL ). Les drapeaux supplémentaires ajoutés à l'appel système clone(2) (notamment CLONE_THREAD Que @ikkkachu Indique dans sa réponse ) est probablement la partie la plus évidente des modifications du noyau. La partie espace utilisateur du travail a finalement été incorporée dans la bibliothèque GNU C).

De nos jours, certains SDK Linux intégrés utilisent l'ancienne implémentation LinuxThreads car ils utilisent une version plus petite de l'empreinte mémoire de LibC appelée Clibc (également appelée µClibc) , et il a fallu des années considérables avant la mise en œuvre de l'espace utilisateur NPTL à partir de GNU LibC a été porté et supposé comme implémentation de threading POSIX par défaut, car en général ces plateformes spéciales ne s'efforcent pas de suivre les modes les plus récentes avec une vitesse fulgurante. Cela peut être observé en remarquant qu'en effet les PID pour différents threads sur ces plates-formes sont également différents contrairement à la norme POSIX spécifie - tout comme le livre que vous lisez décrit. En fait, une fois que vous avez appelé pthread_create(), vous avez soudainement augmenté le nombre de processus de un à trois, car un processus supplémentaire était nécessaire pour maintenir le bordel.

La page de manuel Linux pthreads (7) fournit un aperçu complet et intéressant des différences entre les deux. Une autre description éclairante, bien que dépassée, des différences est la suivante papier par Ulrich Depper et Ingo Molnar sur la conception de NPTL.

Je vous recommande de ne pas prendre cette partie du livre trop au sérieux. Je recommande plutôt les threads POSIX de programmation de Butenhof et les pages de manuel POSIX et Linux sur le sujet. De nombreux didacticiels sur le sujet sont inexacts.

38
FooF

Les threads (Espace utilisateur) ne sont pas implémentés en tant que processus en tant que tels sous Linux, en ce qu'ils n'ont pas leur propre espace d'adressage privé, ils partagent toujours l'espace d'adressage du processus parent.

Cependant, ces threads sont implémentés pour utiliser le système de comptabilité des processus du noyau, donc se voient attribuer leur propre ID de thread (TID), mais reçoivent le même PID et le `` ID de groupe de threads '' (TGID) que le processus parent - cela contraste avec un fork, où un nouveau TGID et PID sont créés, et le TID est le même que le PID.

Il semble donc que les noyaux récents avaient un TID distinct qui peut être interrogé, c'est cela qui est différent pour les threads, un extrait de code approprié pour le montrer dans chacun des principaux () thread_function () ci-dessus est:

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

Donc, le code entier avec ceci serait:

#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;                                                                                                                                                 
} 

Donner un exemple de sortie de:

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

En interne, il n'y a pas de processus ou de threads dans le noyau Linux. Les processus et les threads sont un concept essentiellement utilisateur, le noyau lui-même ne voit que les "tâches", qui sont un objet planifiable qui peut ne partager aucune, certaines ou toutes ses ressources avec d'autres tâches. Les threads sont des tâches qui ont été configurées pour partager la plupart de ses ressources (espace d'adressage, mmaps, canaux, gestionnaires de fichiers ouverts, sockets, etc.) avec la tâche parent, et les processus sont des tâches qui ont été configurées pour partager des ressources minimales avec la tâche parent .

Lorsque vous utilisez directement l'API Linux ( clone () , au lieu de fork () et pthread_create () ), vous avez alors beaucoup plus de flexibilité en définissant la quantité de ressources à partager ou à ne pas partager, et vous pouvez créer des tâches qui ne sont ni entièrement un processus ni entièrement un thread. Si vous utilisez directement ces appels de bas niveau, il est également possible de créer une tâche avec un nouveau TGID (ainsi traité comme un processus par la plupart des outils utilisateur) qui partage en fait toutes ses ressources avec la tâche parente, ou vice versa, pour créer une tâche avec un TGID partagé (donc traité comme un thread par la plupart des outils utilisateur) qui ne partage aucune ressource avec sa tâche parent.

Alors que Linux 2.4 implémente TGID, c'est principalement juste pour le bénéfice de la comptabilité des ressources. De nombreux utilisateurs et outils d'espace utilisateur trouvent utile de pouvoir regrouper les tâches connexes et de signaler leur utilisation des ressources ensemble.

L'implémentation des tâches sous Linux est beaucoup plus fluide que la vision du monde des processus et des threads présentée par les outils de l'espace utilisateur.

9
Lie Ryan

Fondamentalement, les informations de votre livre sont historiquement exactes, en raison d'un historique de mise en œuvre honteusement mauvais des threads sous Linux. Cette réponse de moi à une question connexe sur SO sert également de réponse à votre question:

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

Ces confusions proviennent toutes du fait que les développeurs du noyau avaient à l'origine une vue irrationnelle et erronée selon laquelle les threads pouvaient être implémentés presque entièrement dans l'espace utilisateur en utilisant les processus du noyau comme primitifs, tant que le noyau offrait un moyen de leur faire partager la mémoire et les descripteurs de fichiers . Cela a conduit à l'implémentation notoirement mauvaise LinuxThreads des threads POSIX, ce qui était plutôt un terme impropre car cela ne donnait rien qui ressemble à distance à la sémantique des threads POSIX. Finalement, LinuxThreads a été remplacé (par NPTL), mais une grande partie de la terminologie déroutante et des malentendus persistent.

La première chose et la plus importante à réaliser est que "PID" signifie différentes choses dans l'espace noyau et l'espace utilisateur. Ce que le noyau appelle les PID sont en fait des identifiants de threads au niveau du noyau (souvent appelés TID), à ne pas confondre avec pthread_t qui est un identifiant distinct. Chaque thread du système, qu'il s'agisse du même processus ou d'un processus différent, possède un TID unique (ou "PID" dans la terminologie du noyau).

Ce qui est considéré comme un PID au sens POSIX de "processus", d'autre part, est appelé "ID de groupe de threads" ou "TGID" dans le noyau. Chaque processus consiste en un ou plusieurs threads (processus du noyau) ayant chacun leur propre TID (kernel PID), mais partageant tous le même TGID, qui est égal au TID (kernel PID) du thread initial dans lequel main s'exécute.

Lorsque top vous montre des threads, cela montre des TID (kernel PID), pas des PID (kernel TGID), et c'est pourquoi chaque thread en a un distinct.

Avec l'avènement de NPTL, la plupart des appels système qui prennent un argument PID ou agissent sur le processus appelant ont été modifiés pour traiter le PID comme un TGID et agir sur l'ensemble du "groupe de threads" (processus POSIX).

Linus Torvalds a déclaré dans une publication de la liste de diffusion du noyau en 1996 que "les threads et les processus sont traités comme un" contexte d'exécution "", qui est "juste un conglomérat de tout l'état de ce CoE .... comprend des choses comme CPU state, MMU state, permissions et divers états de communication (fichiers ouverts, gestionnaires de signaux, etc.) ".

// 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;
}

Comme vous pouvez le voir, ce programme générera 25 threads à la fois, chacun dormant pendant 100 secondes puis rejoignant à nouveau le programme principal. Une fois que les 25 threads ont rejoint le programme, le programme est terminé et se termine.

En utilisant top, vous pourrez voir 25 instances du programme "threads2". Mais kidna ennuyeux. La sortie de ps auwx est encore moins intéressant ... MAIS ps -eLf devient un peu excitant.

UID        PID  PPID   LWP  C NLWP STIME TTY          TIME CMD
debian     689   687   689  0    1 14:52 ?        00:00:00 sshd: debian@pts/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

Vous pouvez voir ici les 26 CoE que le thread2 le programme a été créé. Ils partagent tous le même ID de processus (PID) et l'ID de processus parent (PPID) mais chacun a un ID LWP différent (processus léger), et le nombre de LWP (NLWP) indique qu'il y a 26 CoE - le programme principal et le 25 threads engendrés par elle.

5
ivanivan

En ce qui concerne les processus et les threads Linux sont genre de la même chose. C'est-à-dire qu'ils sont créés avec le même appel système: clone.

Si vous y réfléchissez, la différence entre les threads et les processus réside dans le fait que les objets du noyau seront partagés par l'enfant et le parent. Pour les processus, ce n'est pas beaucoup: des descripteurs de fichiers ouverts, des segments de mémoire qui n'ont pas été écrits, probablement quelques autres auxquels je ne peux pas penser du haut de ma tête. Pour les threads, beaucoup plus d'objets sont partagés, mais pas tous.

Ce qui rend les threads et les objets plus proches sous Linux, c'est l'appel système unshare. Les objets du noyau qui commencent comme partagés peuvent être non partagés après la création du thread. Ainsi, vous pouvez, par exemple, avoir deux threads du même processus qui ont un espace de descripteur de fichier différent (en révoquant le partage des descripteurs de fichier après la création des threads). Vous pouvez le tester vous-même en créant un thread, en appelant unshare dans les deux threads, puis en fermant tous les fichiers et en ouvrant de nouveaux fichiers, tuyaux ou objets dans les deux threads. Regardez ensuite dans /proc/your_proc_fd/task/*/fd et vous verrez que chaque task (que vous avez créé en tant que thread) aura des fd différents.

En fait, la création de nouveaux threads et de nouveaux processus sont des routines de bibliothèque qui appellent clone en dessous et spécifient quels objets du noyau le processus-thread-thingamajig nouvellement créé (c'est-à-dire task) va partager avec le processus/thread appelant.

3