I thread sono implementati come processi su Linux?


65

Sto sfogliando questo libro , Advanced Linux Programming di Mark Mitchell, Jeffrey Oldham e Alex Samuel. È del 2001, quindi un po 'vecchio. Ma lo trovo abbastanza buono comunque.

Tuttavia, sono arrivato al punto in cui si differenzia da ciò che il mio Linux produce nell'output della shell. A pagina 92 ​​(116 nel visualizzatore), il capitolo 4.5 L'implementazione del thread GNU / Linux inizia con il paragrafo che contiene questa affermazione:

L'implementazione dei thread POSIX su GNU / Linux differisce dall'implementazione dei thread su molti altri sistemi simili a UNIX in un modo importante: su GNU / Linux, i thread sono implementati come processi.

Questo sembra un punto chiave e in seguito viene illustrato con un codice C. L'output nel libro è:

main thread pid is 14608
child thread pid is 14610

E nel mio Ubuntu 16.04 è:

main thread pid is 3615
child thread pid is 3615

ps l'output supporta questo.

Immagino che qualcosa sia cambiato tra il 2001 e ora.

Il prossimo sottocapitolo nella pagina successiva, 4.5.1 Gestione dei segnali, si basa sull'affermazione precedente:

Il comportamento dell'interazione tra segnali e thread varia da un sistema simile a UNIX a un altro. In GNU / Linux, il comportamento è dettato dal fatto che i thread sono implementati come processi.

E sembra che questo sarà ancora più importante più avanti nel libro. Qualcuno potrebbe spiegare cosa sta succedendo qui?

Ho visto questo I thread del kernel Linux sono davvero processi del kernel? , ma non aiuta molto. Non ho capito bene.

Questo è il codice 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;
}

1
Non capisco quale sia la fonte della tua confusione. I thread sono implementati come processi che condividono lo spazio degli indirizzi con i loro genitori.
Johan Myréen,

2
@ JohanMyréen Allora perché i pid del thread sono uguali?
Tomasz,

Ah, ora vedo. Sì, qualcosa è davvero cambiato. Vedi la risposta di @ ilkkachu.
Johan Myréen,

5
I thread sono ancora implementati come processi - tuttavia ora getpidrestituisce quello che sarebbe chiamato un ID gruppo di thread e per ottenere un ID univoco per un processo che è necessario utilizzare gettid. Tuttavia, oltre al kernel, la maggior parte delle persone e degli strumenti chiamerà un gruppo di thread un processo e chiamerà un processo un thread, per coerenza con altri sistemi.
user253751

Non proprio. Un processo ha la propria memoria e descrittori di file, non viene mai chiamato un filo, così facendo sarebbe in linea con gli altri sistemi.
reinierpost,

Risposte:


50

Penso che questa parte della clone(2)pagina man possa chiarire la differenza. il PID:

CLONE_THREAD (da Linux 2.4.0-test8)
Se è impostato CLONE_THREAD, il figlio viene inserito nello stesso gruppo di thread del processo chiamante.
I gruppi di thread erano una funzionalità aggiunta in Linux 2.4 per supportare l'idea dei thread POSIX di un set di thread che condividono un singolo PID. Internamente, questo PID condiviso è il cosiddetto identificatore del gruppo di thread (TGID) per il gruppo di thread. Da Linux 2.4, le chiamate a getpid (2) restituiscono il TGID del chiamante.

La frase "thread sono implementati come processi" si riferisce al problema dei thread che hanno avuto PID separati in passato. Fondamentalmente, Linux inizialmente non aveva thread all'interno di un processo, ma solo processi separati (con PID separati) che avrebbero potuto avere alcune risorse condivise, come memoria virtuale o descrittori di file. CLONE_THREADe la separazione di ID processo (*) e ID thread rendono il comportamento di Linux più simile ad altri sistemi e più simile ai requisiti POSIX in questo senso. Sebbene tecnicamente il sistema operativo non abbia ancora implementazioni separate per thread e processi.

La gestione del segnale era un'altra area problematica con la vecchia implementazione, questo è descritto più dettagliatamente nel documento a cui @FooF fa riferimento nella loro risposta .

Come notato nei commenti, Linux 2.4 è stato rilasciato anche nel 2001, lo stesso anno del libro, quindi non sorprende che la notizia non sia arrivata a quella stampa.


2
processi separati che potrebbero aver avuto alcune risorse condivise, come memoria virtuale o descrittori di file. Questo è ancora il modo in cui funzionano i thread di Linux, con i problemi menzionati che sono stati ripuliti. Direi che chiamare le unità di pianificazione utilizzate nei "thread" o "processi" del kernel è davvero irrilevante. Il fatto che abbiano iniziato su Linux vengano chiamati solo "processi" non significa che sia tutto ciò che sono ora.
Andrew Henle,

@AndrewHenle, sì, modificato un po '. Spero che catturi il tuo pensiero, anche se mi sembra un momento difficile con le parole. (vai avanti e modifica quella parte se vuoi.) Ho capito che alcuni altri sistemi operativi simili a Unix hanno una separazione più distinta tra thread e processi, con Linux che è una sorta di eccezione nel fatto di avere solo un tipo di servizio entrambe le funzioni. Ma non ne so abbastanza di altri sistemi e non ho fonti a portata di mano, quindi è difficile dire qualcosa di concreto.
ilkkachu,

@tomas Nota che questa risposta spiega come funziona Linux. Come suggerisce ilkkachu, ha funzionato diversamente quando il libro è stato scritto. La risposta di FooF spiega come funzionava Linux in quel momento.
Gilles 'SO- smetti di essere malvagio'

38

Hai ragione, infatti "qualcosa deve essere cambiato tra il 2001 e ora". Il libro che stai leggendo descrive il mondo secondo la prima implementazione storica dei thread POSIX su Linux, chiamato LinuxThreads (vedi anche l' articolo di Wikipedia per alcuni).

LinuxThreads presentava alcuni problemi di compatibilità con lo standard POSIX - ad esempio thread che non condividevano i PID - e altri seri problemi. Per correggere questi difetti, un'altra implementazione chiamata NPTL (Native POSIX Thread Library) è stata guidata da Red Hat per aggiungere il necessario supporto della libreria dello spazio utente e kernel per raggiungere una migliore conformità POSIX (prendendo buone parti da un altro progetto di reimplementazione concorrente di IBM chiamato NGPT (" Discussioni Posix di prossima generazione "), vedi l' articolo di Wikipedia su NPTL ). I flag aggiuntivi aggiunti alla clone(2)chiamata di sistema (in particolare CLONE_THREADciò che @ikkkachusottolinea nella sua risposta ) è probabilmente la parte più evidente delle modifiche del kernel. La parte dello spazio utente del lavoro alla fine è stata incorporata nella libreria GNU C.

Ancora oggi alcuni SDK Linux incorporati usano la vecchia implementazione LinuxThreads perché usano una versione di footprint di memoria più piccola di LibC chiamata uClibc (anche chiamata µClibc) e ci sono voluti molti anni prima che l'implementazione dello spazio utente NPTL da GNU LibC fosse portata e assunta come implementazione di threading POSIX predefinita, come in generale queste piattaforme speciali non si sforzano di seguire le ultime mode alla velocità della luce. Ciò può essere osservato notando che i PID per i diversi thread su quelle piattaforme sono anche diversi a differenza dello standard POSIX, proprio come descrive il libro che stai leggendo. In realtà una volta che hai chiamatopthread_create(), hai improvvisamente aumentato il conteggio dei processi da uno a tre poiché era necessario un processo aggiuntivo per tenere insieme il pasticcio.

La pagina di manuale di Linux pthreads (7) fornisce una panoramica completa e interessante delle differenze tra i due. Un'altra descrizione illuminante, anche se obsoleta, delle differenze è questo articolo di Ulrich Depper e Ingo Molnar sul design di NPTL.

Ti consiglio di non prendere troppo sul serio quella parte del libro. Consiglio invece i thread POSIX di programmazione di Butenhof e le pagine di manuale POSIX e Linux sull'argomento. Molti tutorial sull'argomento sono inaccurati.


22

I thread (Userspace) non sono implementati come processi in quanto tali su Linux, in quanto non dispongono di un proprio spazio di indirizzi privato, ma condividono comunque lo spazio di indirizzi del processo principale.

Tuttavia, questi thread sono implementati per utilizzare il sistema di contabilità dei processi del kernel, quindi sono allocati il ​​proprio ID thread (TID), ma ricevono lo stesso PID e 'ID gruppo thread' (TGID) del processo genitore - questo è in contrasto con un fork, in cui vengono creati un nuovo TGID e PID e il TID è uguale al PID.

Quindi sembra che i kernel recenti abbiano un TID separato che può essere interrogato, è questo che è diverso per i thread, uno snippet di codice adatto per mostrarlo in ciascuno dei main () thread_function () sopra è:

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

Quindi l'intero codice con questo sarebbe:

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

Dare un esempio di output di:

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

3
@tomas einonm ha ragione. Ignorare ciò che dice il libro, è terribilmente confuso. Non so quale idea l'autore volesse trasmettere, ma ha fallito gravemente. Quindi, in Linux hai thread del kernel e thread dello spazio utente. I thread del kernel sono essenzialmente processi senza spazio utente. I thread dello spazio utente sono normali thread POSIX. I processi dello spazio utente condividono descrittori di file, possono condividere segmenti di codice, ma vivono in spazi di indirizzi virtuali completamente separati. I thread dello spazio utente all'interno di un processo condividono il segmento di codice, la memoria statica e l'heap (memoria dinamica), ma hanno set e stack di registri del processore separati.
Boris Burkov,

8

Fondamentalmente, le informazioni nel tuo libro sono storicamente accurate, a causa di una storia di implementazione vergognosamente negativa di thread su Linux. Questa mia risposta a una domanda correlata su SO serve anche come risposta alla tua domanda:

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

Queste confusioni derivano tutte dal fatto che gli sviluppatori del kernel inizialmente avevano una visione irrazionale e sbagliata secondo cui i thread potevano essere implementati quasi interamente nello spazio utente usando i processi del kernel come primitivi, purché il kernel offrisse un modo per farli condividere memoria e descrittori di file . Ciò portò all'implementazione notoriamente cattiva di LinuxThreads dei thread POSIX, che era piuttosto un termine improprio perché non forniva nulla che somigliasse in remoto alla semantica dei thread POSIX. Alla fine LinuxThreads è stato sostituito (da NPTL), ma persistono molte terminologie confuse e incomprensioni.

La prima e più importante cosa da capire è che "PID" significa cose diverse nello spazio del kernel e nello spazio utente. Quelli che il kernel chiama PID sono in realtà ID thread a livello di kernel (spesso chiamati TID), da non confondere con il pthread_tquale è un identificatore separato. Ogni thread sul sistema, sia nello stesso processo che in uno diverso, ha un TID univoco (o "PID" nella terminologia del kernel).

Ciò che è considerato un PID nel senso POSIX di "processo", d'altra parte, è chiamato un "ID gruppo thread" o "TGID" nel kernel. Ogni processo è costituito da uno o più thread (processi del kernel) ciascuno con il proprio TID (kernel PID), ma tutti condividono lo stesso TGID, che è uguale al TID (kernel PID) del thread iniziale in cui mainviene eseguito.

Quando topmostra thread, mostra TID (kernel PID), non PID (kernel TGID), ed è per questo che ogni thread ne ha uno separato.

Con l'avvento di NPTL, la maggior parte delle chiamate di sistema che accettano un argomento PID o agiscono sul processo di chiamata sono state modificate per trattare il PID come TGID e agire sull'intero "gruppo di thread" (processo POSIX).


8

Internamente, non ci sono processi o thread nel kernel di Linux. Processi e thread sono un concetto per lo più userland, il kernel stesso vede solo "task", che sono un oggetto programmabile che può condividere nessuna, alcune o tutte le sue risorse con altri task. I thread sono attività che sono state configurate per condividere la maggior parte delle sue risorse (spazio degli indirizzi, mmaps, pipe, gestori di file aperti, socket, ecc.) Con l'attività padre e i processi sono attività che sono state configurate per condividere risorse minime con l'attività padre .

Quando usi direttamente l'API Linux ( clone () , invece di fork () e pthread_create () ), hai molta più flessibilità nel definire quante risorse condividere o non condividere e puoi creare attività che non sono nemmeno processo né completamente un thread. Se si utilizzano direttamente queste chiamate di basso livello, è anche possibile creare un'attività con un nuovo TGID (quindi trattato come un processo dalla maggior parte degli strumenti di userland) che effettivamente condivide tutte le sue risorse con l'attività principale, o viceversa, per creare un'attività con TGID condiviso (quindi trattato come un thread dalla maggior parte degli strumenti userland) che non condividono alcuna risorsa con l'attività principale.

Mentre Linux 2.4 implementa TGID, questo è principalmente solo a vantaggio della contabilità delle risorse. Molti utenti e lo strumento userspace trovano utile essere in grado di raggruppare le attività correlate e riportare insieme il loro utilizzo delle risorse.

L'implementazione delle attività in Linux è molto più fluida rispetto alla visione del mondo dei processi e dei thread presentata dagli strumenti di userspace.


L' articolo @FooF collegato descrive un numero di punti in cui il kernel deve considerare processi e thread come entità separate (ad es. Gestione del segnale ed exec ()), quindi dopo averlo letto, non direi davvero che "non esiste cosa come processi o thread nel kernel di Linux. "
ilkkachu,

5

Nel 1996 Linus Torvalds dichiarò in una mailing list del kernel che "sia i thread che i processi sono trattati come un" contesto di esecuzione "", che è "solo un conglomerato di tutto lo stato di quel CoE .... include cose come la CPU stato, stato MMU, autorizzazioni e vari stati di comunicazione (file aperti, gestori di segnale, ecc.) ".

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

Come puoi vedere, questo programma genererà 25 thread contemporaneamente, ognuno dei quali dormirà per 100 secondi e poi si unirà nuovamente al programma principale. Dopo che tutti e 25 i thread sono rientrati nel programma, il programma è terminato e verrà chiuso.

Usando topsarai in grado di vedere 25 istanze del programma "thread2". Ma kidna noioso. L'output di ps auwxè ancora meno interessante ... MA ps -eLfdiventa piuttosto eccitante.

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

Puoi vedere qui tutti i 26 CoE che il thread2programma ha creato. Tutti condividono lo stesso ID processo (PID) e ID processo principale (PPID) ma ognuno ha un ID LWP diverso (processo leggero) e il numero di LWP (NLWP) indica che ci sono 26 CoE: il programma principale e il 25 fili generati da esso.


Corretto, un thread è solo un processo leggero (LWP)
fpmurphy

2

Quando si tratta di processi di Linux e le discussioni sono tipo della stessa cosa. Vale a dire che sono stati creati con la stessa chiamata di sistema: clone.

Se ci pensate, la differenza tra thread e processi è in cui gli oggetti kernel saranno condivisi da figlio e genitore. Per i processi, non è molto: descrittori di file aperti, segmenti di memoria su cui non sono stati scritti, probabilmente alcuni altri a cui non riesco a pensare dalla cima della mia testa. Per i thread, vengono condivisi molti più oggetti, ma non tutti.

Ciò che avvicina thread e oggetti in Linux è la unsharechiamata di sistema. Gli oggetti del kernel che iniziano come condivisi possono essere condivisi dopo la creazione del thread. Quindi, ad esempio, puoi avere due thread dello stesso processo che hanno uno spazio descrittore di file diverso (revocando la condivisione dei descrittori di file dopo la creazione dei thread). Puoi testarlo tu stesso creando un thread, chiamando unshareentrambi i thread e quindi chiudendo tutti i file e aprendo nuovi file, pipe o oggetti in entrambi i thread. Quindi guarda dentro /proc/your_proc_fd/task/*/fde vedrai che ognuno task(che hai creato come thread) avrà fd diversi.

In effetti, sia la creazione di nuovi thread sia di nuovi processi sono routine di libreria che chiamano clonesotto e specificano quale oggetto del kernel taskcondividerà il processo-thread-thingamajig (cioè, ) appena creato con il processo / thread chiamante.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.