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

65

Je suis en train de parcourir ce livre , Advanced Linux Programming de Mark Mitchell, Jeffrey Oldham et Alex Samuel. C'est à partir de 2001, donc un peu vieux. Mais je trouve ça assez bon quand même.

Cependant, je suis arrivé à un point où il s'écarte de ce que mon Linux produit dans la sortie du shell. Sur la page 92 (116 dans le visualiseur), le chapitre 4.5 Implémentation du thread GNU / Linux commence par le paragraphe contenant cette instruction:

L'implémentation de threads POSIX sur GNU / Linux diffère de l'implémentation de thread sur de nombreux autres systèmes de type UNIX de manière importante: sous 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 supporte cela.

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

Le sous-chapitre suivant à la page suivante, 4.5.1 Traitement du signal, reprend la déclaration précédente:

Le comportement de l'interaction entre les signaux et les unités d'exécution varie d'un système de type UNIX à un 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 déjà vu celui-ci. Les threads du noyau Linux sont-ils vraiment des processus du noyau? , mais ça n’aide pas beaucoup. Je suis confus.

C'est 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;
}
Tomasz
la source
1
Je ne comprends pas la source de votre confusion. Les threads sont implémentés en tant que processus partageant l'espace d'adressage avec leur parent.
Johan Myréen
2
@ JohanMyréen Alors pourquoi les pids de fils sont-ils égaux?
Tomasz
Ah, maintenant je vois. Oui, quelque chose a vraiment changé. Voir la réponse de @ ilkkachu.
Johan Myréen
5
Les threads sont toujours implémentés en tant que processus - mais getpidrenvoie à présent ce qui serait appelé un identifiant de groupe de threads et obtient un identifiant unique pour un processus que vous devez utiliser gettid. Cependant, mis à part le noyau, la plupart des utilisateurs et des outils appellent un processus un groupe de threads, et appellent un processus un thread, par souci de cohérence avec les autres systèmes.
user253751
Pas vraiment. Un processus a ses propres descripteurs de mémoire et de fichiers, il est jamais appelé un fil, le faire serait en cohérence avec les autres systèmes.
reinierpost

Réponses:

50

Je pense que cette partie de la clone(2)page de manuel peut éclaircir la différence re. 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 d'appel.
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 appelé identifiant de groupe de threads (TGID) pour le groupe de threads. Depuis Linux 2.4, les appels à getpid (2) renvoient le TGID de l'appelant.

L'expression "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é. À la base, à l'origine, Linux ne comportait pas de threads dans un processus, mais simplement des processus séparés (avec des PID distincts) pouvant avoir des ressources partagées, telles que la mémoire virtuelle ou les descripteurs de fichier. CLONE_THREADet la séparation de l'ID de processus (*) et de l'ID de thread fait que le comportement de Linux ressemble davantage à celui des autres systèmes et davantage à la configuration POSIX requise en ce sens. Bien que techniquement, le système d'exploitation ne dispose toujours pas d'implémentations distinctes pour les threads et les processus.

La gestion du signal était un autre domaine problématique de l’ancienne implémentation, elle est décrite plus en détail dans l’ article auquel @FooF fait référence dans sa réponse .

Comme indiqué dans les commentaires, Linux 2.4 est également sorti en 2001, la même année que le livre. Il n’est donc pas surprenant que la nouvelle n’ait pas eu cette impression.

ilkkachu
la source
2
des processus distincts pouvant avoir des ressources partagées, tels que la mémoire virtuelle ou des descripteurs de fichier. C’est à peu près toujours le fonctionnement des threads Linux, les problèmes que vous avez mentionnés ayant été résolus. Je dirais que l'appel des unités de planification utilisées dans le noyau "threads" ou "processus" n'est vraiment pas pertinent. Le fait qu’ils aient démarré sous Linux n’appelant que «processus» ne signifie pas que c’est tout ce qu’ils sont maintenant.
Andrew Henle
@ AndrewHenle, oui, édité un peu. J'espère que cela capture votre pensée, même si je trouve que le libellé est difficile. (Allez-y et éditez cette partie si vous voulez.) J'ai compris que certains autres systèmes d'exploitation de type Unix ont une séparation plus distincte des threads par rapport aux processus, Linux étant une sorte d'exception car il n'y a vraiment qu'un seul type servant les deux fonctions. Mais je ne connais pas assez les autres systèmes et je n'ai pas de sources à portée de main, il est donc difficile de dire quelque chose de concret.
ilkkachu
@tomas Notez que cette réponse explique le fonctionnement actuel de Linux. Comme ilkkachu le laisse entendre, cela a fonctionné différemment lors de la rédaction du livre. La réponse de FooF explique le fonctionnement de Linux à l'époque.
Gilles 'SO- arrête d'être méchant'
38

Vous avez raison, en effet "quelque chose a dû changer entre 2001 et maintenant". Le livre que vous lisez décrit le monde selon la première implémentation historique de threads POSIX sous Linux, appelée LinuxThreads (voir aussi l' article de Wikipedia dans certains).

LinuxThreads présentait des problèmes de compatibilité avec le standard POSIX - par exemple, des threads ne partageant pas les PID - et quelques autres problèmes graves. Pour résoudre ces problèmes, Red Hat a piloté une autre implémentation appelée NPTL (Native POSIX Thread Library) afin d’ajouter le support nécessaire à la bibliothèque du noyau et de l’espace utilisateur afin d’améliorer la conformité POSIX (en prenant de bonnes parties d’un autre projet de réimplémentation concurrent appelé IBM par NGPT (" Threads Posix de nouvelle génération "), voir article de Wikipedia sur NPTL ). Les indicateurs supplémentaires ajoutés à l' clone(2)appel système (notamment ce CLONE_THREADqui est @ikkkachuindiqué dans sa réponse ) constituent probablement la partie la plus évidente des modifications du noyau. La partie espace de travail du travail a finalement été intégrée à la bibliothèque GNU C.

Aujourd'hui encore certains Linux SDKs utilisent l'ancienne implémentation de LinuxThreads embarqués parce qu'ils utilisent version plus petite empreinte mémoire de libc appelé uClibc (aussi appelé μClibc) , et il a fallu beaucoup de années avant la mise en œuvre de l' espace utilisateur NPTL de GNU libc a été porté et assumé comme implémentation POSIX par défaut des threads, car en général ces plates-formes spéciales ne cherchent pas à suivre les dernières modes à la vitesse de l'éclair. Ceci peut être observé en remarquant qu'en réalité, les PID de différents threads sur ces plates-formes sont également différents, contrairement à la norme POSIX, comme le décrit le livre que vous lisez. 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 désordre ensemble.

La page de manuel Linux pthreads (7) fournit un aperçu complet et intéressant des différences entre les deux. Un autre instructif, bien que hors-date, la description des différences est ce 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 fils de programmation POSIX et les pages de manuel POSIX et Linux de Butenhof sur le sujet. De nombreux tutoriels sur le sujet sont inexacts.

FooF
la source
22

Les threads (Espace utilisateur) ne sont pas implémentés en tant que processus en tant que tels sous Linux, dans la mesure où ils ne disposent pas de leur propre espace d'adressage privé, ils partagent néanmoins l'espace d'adressage du processus parent.

Cependant, ces threads sont implémentés pour utiliser le système de comptabilisation des processus du noyau. Ils se voient donc attribuer leur propre ID de thread (TID), mais se voient attribuer les mêmes PID et 'ID de groupe de threads' (TGID) que le processus parent. une fourche, où un nouveau TGID et PID sont créés, et le TID est identique au PID.

Il semble donc que les noyaux récents contenaient un TID distinct qui peut être interrogé. C’est ce qui est différent pour les threads. Un extrait de code approprié pour l’afficher dans chacune des fonctions main () thread_function () ci-dessus est:

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

Donc tout le code 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
einonm
la source
3
@ Thomas Einonm a raison. Ne tenez pas compte de ce que dit le livre, c'est terriblement déroutant. Je ne sais pas quelle idée l'auteur a voulu transmettre, mais il a échoué. Donc, sous Linux, vous avez les threads du noyau et les threads de l'espace utilisateur. Les threads du noyau sont essentiellement des processus sans aucun espace utilisateur. Les threads d'espace utilisateur sont des threads POSIX normaux. Les processus d'espace utilisateur partagent des descripteurs de fichier, peuvent partager des segments de code, mais résident dans des espaces d'adressage virtuels complètement séparés. Les threads d'espace utilisateur dans un processus partagent un segment de code, une mémoire statique et une pile (mémoire dynamique), mais possèdent des ensembles de registres de processeurs et des piles séparés.
Boris Burkov
8

Fondamentalement, les informations contenues dans votre livre sont historiquement exactes, en raison d’un historique d’implémentation extrêmement regrettable des threads sous Linux. Cette réponse de ma part à une question connexe sur le SO sert également de réponse à votre question:

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

Ces confusions découlent toutes du fait que les développeurs du noyau avaient à l’origine une vision irrationnelle et erronée selon laquelle les threads pourraient être presque entièrement implémentés dans l’espace utilisateur en utilisant les processus du noyau comme primitive, à condition que le noyau leur permette de partager la mémoire et les descripteurs de fichiers. . Cela a conduit à la mise en œuvre notoirement mauvaise des threads POSIX sous LinuxThreads, ce qui était plutôt impropre dans la mesure où elle ne donnait rien qui ressemble de loin à la sémantique des threads POSIX. Finalement, LinuxThreads a été remplacé (par NPTL), mais une grande partie de la terminologie prêtant à confusion et des malentendus persistent.

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

Par contre, ce qui est considéré comme un PID au sens POSIX du terme "processus" est appelé "ID de groupe de threads" ou "TGID" dans le noyau. Chaque processus consiste en un ou plusieurs threads (processus du noyau), chacun avec son propre TID (PID du noyau), mais partageant tous le même TGID, qui est égal au TID (PID du noyau) du thread initial dans lequel mains'exécute.

Lorsque topvous affichez les threads, il affiche les TID (PID du noyau) et non les PID (TGID du noyau), raison pour laquelle chaque thread en a un distinct.

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

R ..
la source
8

En interne, il n’existe pas de processus ou de threads dans le noyau Linux. Les processus et les threads sont un concept principalement utilisateur, le noyau lui-même ne voit que des "tâches", qui sont un objet programmable 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, tubes, gestionnaires de fichiers ouverts, sockets, etc.) avec la tâche parente et les processus sont des tâches configurées pour partager des ressources minimales avec la tâche parente .

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

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

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

Lie Ryan
la source
Le document @FooF lié à décrit un certain nombre de points où le noyau doit considérer les processus et les threads comme des entités distinctes (par exemple, traitement du signal et exec ()). Ainsi, après l'avoir lu, je ne dirais pas vraiment qu '"il n'y en a pas chose que les processus ou les threads dans le noyau Linux ".
Ilkkachu
5

En 1996, Linus Torvalds avait déclaré dans un message de la liste de diffusion du noyau que "les threads et les processus sont traités comme un" contexte d'exécution "", ce qui "n'est qu'un conglomérat de tous les états de ce CoE. état, état MMU, autorisations 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 constater, ce programme va générer 25 threads à la fois, chacun dormant pendant 100 secondes avant de rejoindre à nouveau le programme principal. Une fois que les 25 threads ont rejoint le programme, le programme est terminé et se termine.

En utilisant, topvous pourrez voir 25 instances du programme "threads2". Mais kidna ennuyeux. La sortie de ps auwxest encore moins intéressante ... MAIS ps -eLfça 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 centres d'excellence créés par le thread2programme. Ils partagent tous le même ID de processus (PID) et le même ID de processus parent (PPID), mais chacun a un ID de LWP différent (processus léger), et le nombre de LWP (NLWP) indique qu'il y a 26 centres d'excellence - le programme principal et le 25 fils engendrés par elle.

Ivanivan
la source
Correct, un thread est juste un processus léger (LWP)
fpmurphy
2

En ce qui concerne Linux, les processus et les threads sont un peu la même chose. Ce qui est de 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: descripteurs de fichiers ouverts, segments de mémoire sur lesquels il n’a pas été écrit, probablement quelques autres auxquels je ne peux penser spontanément. Pour les threads, beaucoup plus d'objets sont partagés, mais pas tous.

Ce qui rapproche les threads et les objets sous Linux, c'est l' unshareappel système. Les objets du noyau qui commencent par être partagés peuvent ne pas être partagés après la création du fil. 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 les unsharedeux threads, puis en fermant tous les fichiers et en ouvrant de nouveaux fichiers, canaux ou objets dans les deux threads. Ensuite, regardez dans /proc/your_proc_fd/task/*/fdet vous verrez que chacun task(que vous avez créé en tant que fil) aura des fd différents.

En fait, la création de nouveaux threads et de nouveaux processus est une routine de bibliothèque qui appelle en clonedessous et spécifie les objets du noyau que le processus nouvellement créé taskva partager avec le processus / thread appelant.

Dmitry Rubanovich
la source