Czy wątki są implementowane jako procesy w systemie Linux?


65

Przeglądam tę książkę , Advanced Linux Programming, Mark Mitchell, Jeffrey Oldham i Alex Samuel. Jest z 2001 roku, więc trochę stary. Ale i tak uważam to za całkiem dobre.

Doszedłem jednak do momentu, gdy odbiega on od tego, co mój Linux produkuje w wynikach powłoki. Na stronie 92 (116 w przeglądarce) rozdział 4.5 Implementacja wątku GNU / Linux zaczyna się od akapitu zawierającego to oświadczenie:

Implementacja wątków POSIX w GNU / Linux różni się od implementacji wątków w wielu innych systemach podobnych do UNIX w istotny sposób: w GNU / Linux wątki są implementowane jako procesy.

To wydaje się kluczowe, a później zilustrowane kodem C. Dane wyjściowe w książce to:

main thread pid is 14608
child thread pid is 14610

A w moim Ubuntu 16.04 jest to:

main thread pid is 3615
child thread pid is 3615

ps wyjście obsługuje to.

Wydaje mi się, że coś musiało się zmienić od 2001 roku do teraz.

Następny podrozdział na następnej stronie, 4.5.1 Obsługa sygnałów, stanowi rozwinięcie poprzedniej instrukcji:

Zachowanie interakcji między sygnałami i wątkami różni się w zależności od systemu uniksowego. W GNU / Linux zachowanie jest podyktowane faktem, że wątki są implementowane jako procesy.

Wygląda na to, że będzie to jeszcze ważniejsze w dalszej części książki. Czy ktoś mógłby wyjaśnić, co się tutaj dzieje?

Widziałem to. Czy wątki jądra Linuksa naprawdę są procesami jądra? , ale to niewiele pomaga. Jestem zmieszany.

To jest kod 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
Nie rozumiem, jakie jest źródło twojego zamieszania. Wątki są implementowane jako procesy współużytkujące przestrzeń adresową z rodzicem.
Johan Myréen

2
@ JohanMyréen Dlaczego więc stawki za wątki są równe?
Tomasz

Ach, teraz rozumiem. Tak, coś się naprawdę zmieniło. Zobacz odpowiedź @ ilkkachu.
Johan Myréen

5
Wątki są nadal implementowane jako procesy - jednak teraz getpidzwraca to, co byłoby nazywane identyfikatorem grupy wątków i aby uzyskać unikalny identyfikator dla procesu, którego musisz użyć gettid. Jednak inne niż jądro, większość ludzi i narzędzi nazywa grupę wątków procesem, a proces nazywa wątkiem, aby zachować spójność z innymi systemami.
user253751

Nie całkiem. Proces ma własną pamięć i deskryptory plików, to nigdy nie jest wywoływana wątku, byłoby to w zgodzie z innymi systemami.
reinierpost

Odpowiedzi:


50

Myślę, że ta część clone(2)strony człowieka może wyjaśnić różnicę re. PID:

CLONE_THREAD (od Linuksa 2.4.0-test8)
Jeśli ustawiony jest CLONE_THREAD, dziecko jest umieszczane w tej samej grupie wątków co proces wywołujący.
Grupy wątków były funkcją dodaną w Linuksie 2.4 do obsługi pojęcia wątków POSIX dla zestawu wątków, które mają jeden PID. Ten wewnętrzny PID jest tak zwanym identyfikatorem grupy wątków (TGID) dla grupy wątków. Od Linuksa 2.4 wywołania getpid (2) zwracają TGID dzwoniącego.

Wyrażenie „wątki są implementowane jako procesy” odnosi się do problemu wątków, które miały osobne PID w przeszłości. Zasadniczo Linux pierwotnie nie miał wątków w procesie, tylko oddzielne procesy (z oddzielnymi PID), które mogły mieć pewne wspólne zasoby, takie jak pamięć wirtualna lub deskryptory plików. CLONE_THREADa oddzielenie identyfikatora procesu (*) i identyfikatora wątku sprawia, że ​​zachowanie Linuksa bardziej przypomina inne systemy i bardziej przypomina wymagania POSIX w tym sensie. Chociaż technicznie system operacyjny wciąż nie ma osobnych implementacji wątków i procesów.

Obsługa sygnałów była kolejnym problematycznym obszarem ze starą implementacją, co opisano bardziej szczegółowo w dokumencie @FooF, do którego odnosi się w ich odpowiedzi .

Jak zauważono w komentarzach, Linux 2.4 został wydany również w 2001 roku, w tym samym roku, co książka, więc nic dziwnego, że wiadomości nie dotarły do ​​tego druku.


2
oddzielne procesy, które mogły mieć wspólne zasoby, takie jak pamięć wirtualna lub deskryptory plików. W zasadzie tak działają wątki Linuksa, a problemy, o których wspominasz, zostały usunięte. Powiedziałbym, że nazywanie jednostek planowania używanych w jądrze „wątkami” lub „procesami” jest naprawdę nieistotne. Fakt, że zaczęli na Linuksie nazywać się tylko „procesami”, nie oznacza, że ​​to wszystko, czym są teraz.
Andrew Henle,

@AndrewHenle, tak, trochę zmodyfikowałem. Mam nadzieję, że to uchwyci twoją myśl, choć wydaje mi się, że trudno jest sformułować. (idź naprzód i edytuj tę część, jeśli chcesz.) Zrozumiałem, że niektóre inne uniksowe systemy operacyjne mają wyraźniejsze oddzielenie wątków od procesów, przy czym Linux jest swego rodzaju wyjątkiem, ponieważ tylko jeden typ służy obie funkcje. Ale nie wiem wystarczająco dużo o innych systemach i nie mam pod ręką źródeł, więc trudno powiedzieć coś konkretnego.
ilkkachu

@tomas Pamiętaj, że ta odpowiedź wyjaśnia, jak działa teraz Linux. Jak sugeruje ilkkachu, działało to inaczej, gdy książka została napisana. Odpowiedź FooF wyjaśnia, jak wtedy działał Linux.
Gilles

38

Masz rację, rzeczywiście „coś musiało się zmienić od 2001 roku do teraz”. Książka, którą czytasz, opisuje świat zgodnie z pierwszą historyczną implementacją wątków POSIX w Linuksie, zwaną LinuxThreads (zobacz także artykuł na Wikipedii ).

LinuxThreads miał pewne problemy ze zgodnością ze standardem POSIX - na przykład wątki nieposiadające PID-ów - i inne poważne problemy. Aby naprawić te wady, Red Hat kierował kolejną implementacją o nazwie NPTL (natywna biblioteka wątków POSIX) w celu dodania niezbędnej obsługi jądra i biblioteki przestrzeni użytkownika w celu osiągnięcia lepszej zgodności z POSIX (biorąc dobre części z innego konkurencyjnego projektu reimplementacji przez IBM o nazwie NGPT („ Posix Next Generation Threads ”), zobacz artykuł w Wikipedii na temat NPTL ). Dodatkowe flagi dodane do clone(2)wywołania systemowego (zwłaszcza CLONE_THREADto @ikkkachuwskazuje w jego odpowiedzi ) jest prawdopodobnie najbardziej widoczną częścią modyfikacji jądra. Część pracy w przestrzeni użytkownika została ostatecznie włączona do biblioteki GNU C.

Nadal obecnie niektóre wbudowane zestawy SDK systemu Linux używają starej implementacji LinuxThreads, ponieważ używają mniejszej wersji LibC zajmującej pamięć, zwanej uClibc (zwanej także µClibc) , i zajęło sporo czasu, zanim implementacja przestrzeni użytkownika NPTL z GNU LibC została przeniesiona i przyjęta jako domyślna implementacja wątków POSIX, ponieważ ogólnie rzecz biorąc, te specjalne platformy nie starają się podążać za najnowszymi modami z prędkością błyskawicy. Można to zaobserwować, zauważając, że PID dla różnych wątków na tych platformach są również różne w przeciwieństwie do specyfikacji standardu POSIX - tak jak opisuje czytana książka. Właściwie kiedy zadzwoniszpthread_create(), nagle zwiększyłeś liczbę procesów z jednego do trzech, ponieważ potrzebny był dodatkowy proces, aby utrzymać bałagan razem.

Strona podręcznika Linux pthreads (7) zawiera wyczerpujący i interesujący przegląd różnic między nimi. Innym pouczającym, choć nieaktualnym, opisem różnic jest ten artykuł autorstwa Ulricha Deppera i Ingo Molnara na temat projektu NPTL.

Radzę, abyś nie traktował tej części książki zbyt poważnie. Zamiast tego polecam wątki programistyczne POSIX firmy Butenhof oraz strony podręczników POSIX i Linux na ten temat. Wiele samouczków na ten temat jest niedokładnych.


22

Wątki (Przestrzeń użytkownika) nie są implementowane jako takie procesy w systemie Linux, ponieważ nie mają własnej prywatnej przestrzeni adresowej, nadal współużytkują przestrzeń adresową procesu nadrzędnego.

Wątki te są jednak implementowane w celu korzystania z systemu rozliczania procesów jądra, dlatego przydzielane są im własne identyfikatory wątków (TID), ale otrzymują takie same PID i „ID grup wątków” (TGID) jak proces nadrzędny - w przeciwieństwie do rozwidlenie, w którym tworzone są nowe TGID i PID, a TID jest taki sam jak PID.

Wygląda więc na to, że najnowsze jądra miały osobny TID, który można zapytać, to jest inny dla wątków, odpowiedni fragment kodu pokazujący to w każdej z głównych () funkcji thread_funkcji powyżej:

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

Cały kod z tym to:

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

Podając przykładowy wynik:

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

3
@tomas einonm ma rację. Zignoruj ​​to, co mówi książka, jest to bardzo mylące. Nie wiem, jaki pomysł autor chciał przekazać, ale bardzo mu się nie udało. Tak więc w Linuksie masz wątki jądra i wątki przestrzeni użytkownika. Wątki jądra są w zasadzie procesami bez przestrzeni użytkownika. Wątki przestrzeni użytkownika są normalnymi wątkami POSIX. Procesy przestrzeni użytkownika współużytkują deskryptory plików, mogą współużytkować segmenty kodu, ale żyją w całkowicie oddzielnych wirtualnych przestrzeniach adresowych. Wątki przestrzeni użytkownika w procesie współużytkują segment kodu, pamięć statyczną i stertę (pamięć dynamiczną), ale mają osobne zestawy rejestrów procesorów i stosy.
Boris Burkov

8

Zasadniczo informacje w książce są historycznie dokładne, z powodu haniebnie złej historii implementacji wątków w systemie Linux. Ta odpowiedź przeze mnie na powiązane pytanie dotyczące SO służy również jako odpowiedź na Twoje pytanie:

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

Wszystkie te nieporozumienia wynikają z faktu, że twórcy jądra początkowo mieli irracjonalny i błędny pogląd, że wątki mogą być implementowane prawie całkowicie w przestrzeni użytkownika, wykorzystując procesy jądra jako prymitywne, pod warunkiem, że jądro oferuje sposób na dzielenie się pamięcią i deskryptorami plików . Doprowadziło to do notorycznie złej implementacji wątków POSIX w LinuxThreads, co było raczej błędne, ponieważ nie dawało niczego, co przypominałoby semantykę wątków POSIX. Ostatecznie LinuxThreads został zastąpiony (przez NPTL), ale nadal istnieje wiele mylących terminów i nieporozumień.

Pierwszą i najważniejszą rzeczą do zrozumienia jest to, że „PID” oznacza różne rzeczy w przestrzeni jądra i przestrzeni użytkownika. To, co jądro nazywa PID, to w rzeczywistości identyfikatory wątków na poziomie jądra (często nazywane TID), których nie należy mylić z pthread_tosobnym identyfikatorem. Każdy wątek w systemie, czy to w tym samym procesie, czy w innym, ma unikalny TID (lub „PID” w terminologii jądra).

Z drugiej strony to, co jest uważane za PID w znaczeniu POSIX „proces”, w jądrze nazywa się „ID grupy wątków” lub „TGID”. Każdy proces składa się z jednego lub więcej wątków (procesów jądra), każdy z własnym TID (jądro PID), ale wszystkie współużytkują ten sam TGID, który jest równy TID (jądro PID) pierwotnego wątku, w którym maindziała.

Kiedy toppokazuje ci wątki, pokazuje TID (jądro PID), a nie PID (jądro TGID), i dlatego każdy wątek ma osobny.

Wraz z pojawieniem się NPTL większość wywołań systemowych, które pobierają argument PID lub działają na proces wywoływania , zostały zmienione, aby traktować PID jako TGID i działać na całej „grupie wątków” (proces POSIX).


8

Wewnętrznie w jądrze Linuksa nie ma procesów ani wątków. Procesy i wątki są głównie koncepcją przestrzeni użytkownika, samo jądro widzi tylko „zadania”, które są planowalnym obiektem, który może nie dzielić żadnych, niektórych lub wszystkich swoich zasobów z innymi zadaniami. Wątki to zadania skonfigurowane do współdzielenia większości zasobów (przestrzeń adresowa, mmapy, potoki, otwarte programy obsługi plików, gniazda itp.) Z zadaniem nadrzędnym, a procesy to zadania skonfigurowane do współdzielenia minimalnych zasobów z zadaniem nadrzędnym .

Kiedy używasz Linux API bezpośrednio ( clone () , zamiast fork () i pthread_create () ), masz większą elastyczność w określaniu ilości zasobów do udostępnienia lub nie, i możesz tworzyć zadania, które nie są w pełni proces ani w pełni wątek. Jeśli używasz tych połączeń niskiego poziomu bezpośrednio, możliwe jest również utworzenie zadania z nowym TGID (traktowanym jako proces przez większość narzędzi użytkownika), które faktycznie współużytkują wszystkie swoje zasoby z zadaniem nadrzędnym lub odwrotnie, aby utworzyć zadanie ze współużytkowanym TGID (traktowane jako wątek przez większość narzędzi użytkownika), które nie współużytkuje zasobów z zadaniem nadrzędnym.

Podczas gdy Linux 2.4 implementuje TGID, jest to głównie tylko dla korzyści księgowania zasobów. Wielu użytkowników i narzędzie przestrzeni użytkownika uważają, że przydatne jest grupowanie powiązanych zadań i raportowanie zużycia zasobów.

Implementacja zadań w Linuksie jest znacznie płynniejsza niż światopogląd procesów i wątków prezentowany przez narzędzia przestrzeni użytkownika.


Papier @FooF związana opisuje liczbę punktów, w których jądro rozważyć procesy i wątki jako odrębne podmioty (np obsługę sygnału i exec ()), więc po przeczytaniu go, nie będę naprawdę powiedzieć, że „nie ma takiego coś jak procesy lub wątki w jądrze Linuksa. "
ilkkachu

5

Linus Torvalds stwierdził w liście z listami dyskusyjnymi jądra w 1996 r., Że „zarówno wątki, jak i procesy są traktowane jako„ kontekst wykonania ”, który jest„ tylko konglomeratem całego stanu tego CoE .... obejmuje rzeczy takie jak CPU stan, stan MMU, uprawnienia i różne stany komunikacji (otwarte pliki, procedury obsługi sygnałów itp.) ”.

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

Jak widać, ten program odrodzi 25 wątków jednocześnie, z których każdy będzie spał przez 100 sekund, a następnie ponownie dołączy do programu głównego. Po ponownym dołączeniu wszystkich 25 wątków do programu, program jest gotowy i zakończy działanie.

Za pomocą topbędziesz mógł zobaczyć 25 instancji programu „Thread2”. Ale nudząca nerka. Produkcja ps auwxjest jeszcze mniej interesująca ... ALE ps -eLfstaje się trochę ekscytująca.

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

Możesz zobaczyć tutaj wszystkie 26 regionów Europy, które thread2program utworzył. Wszystkie mają ten sam identyfikator procesu (PID) i identyfikator procesu nadrzędnego (PPID), ale każdy z nich ma inny identyfikator LWP (proces lekki), a liczba LWP (NLWP) wskazuje, że jest 26 CoE - główny program i Odrodziło się 25 wątków.


Zgadza się, wątek jest po prostu lekkim procesem (LWP)
fpmurphy

3

Jeśli chodzi o Linux, procesy i wątki są w pewnym sensie tym samym. To znaczy są one tworzone z tego samego wywołania systemowego: clone.

Jeśli się nad tym zastanowić, różnica między wątkami i procesami polega na tym, w których obiektach jądra będą współużytkowane przez dziecko i rodzic. W przypadku procesów nie jest to wiele: otwarte deskryptory plików, segmenty pamięci, do których nie zapisano, prawdopodobnie kilka innych, o których nie mogę myśleć bez końca. W przypadku wątków współdzielonych jest znacznie więcej obiektów, ale nie wszystkie.

Tym, co zbliża wątki i obiekty w Linuksie, jest unsharewywołanie systemowe. Obiekty jądra, które zaczynają się jako współdzielone, mogą zostać udostępnione po utworzeniu wątku. Możesz na przykład mieć dwa wątki tego samego procesu, które mają inną przestrzeń deskryptorów plików (przez odwołanie udostępniania deskryptorów plików po utworzeniu wątków). Możesz to przetestować samodzielnie, tworząc wątek, wywołując unshareoba wątki, a następnie zamykając wszystkie pliki i otwierając nowe pliki, potoki lub obiekty w obu wątkach. Następnie spójrz, /proc/your_proc_fd/task/*/fda zobaczysz, że każdy task(który utworzyłeś jako wątek) będzie miał inne fd.

W rzeczywistości zarówno tworzenie nowych wątków, jak i nowych procesów są procedurami bibliotecznymi, które wywołują cloneponiżej i określają, który z obiektów jądra nowo utworzony proces-wątek-rzeczamajig (tj. task) Będzie współdzielił z wywołującym procesem / wątkiem.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.