Чи реалізовані потоки як процеси в Linux?


65

Я переглядаю цю книгу , розширене програмування Linux Марка Мітчелла, Джефрі Олдхема та Алекса Самюеля. Це з 2001 року, так трохи старий. Але я вважаю це досить непоганим.

Однак я дійшов до моменту, коли він розходиться з тим, що виробляє мій Linux у виході оболонки. На сторінці 92 (116 у переглядачі) розділ 4.5 Реалізація потоку GNU / Linux починається з абзацу, що містить це твердження:

Реалізація потоків POSIX у GNU / Linux важливим чином відрізняється від реалізації потоків у багатьох інших системах, схожих на UNIX: у GNU / Linux потоки реалізуються як процеси.

Це здається ключовим моментом і пізніше ілюструється кодом С. Вихід у книзі:

main thread pid is 14608
child thread pid is 14610

А в моєму Ubuntu 16.04 це:

main thread pid is 3615
child thread pid is 3615

ps вихід підтримує це.

Я здогадуюсь щось змінилося між 2001 і зараз.

Наступна підрозділ на наступній сторінці, 4.5.1 Обробка сигналів, ґрунтується на попередньому операторі:

Поведінка взаємодії між сигналами та потоками змінюється від однієї UNIX-подібної системи до іншої. У GNU / Linux поведінка продиктована тим, що потоки реалізуються як процеси.

І, схоже, це стане ще важливішим згодом у книзі. Може хтось пояснить, що тут відбувається?

Я бачив це. Чи справді нитки ядра Linux є процесами ядра? , але це не дуже допомагає. Я збентежений.

Це код 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
Я не розумію, що є джерелом вашої плутанини. Нитки реалізовані як процеси, що ділять адресний простір з батьком.
Йохан Мірен

2
@ JohanMyréen Тож чому плівкові нитки рівні?
Томаш

Ах, тепер я бачу. Так, щось справді змінилося. Дивіться відповідь @ ilkkachu.
Йохан Мірен

5
Нитки все ще реалізуються як процеси - однак тепер getpidповертає те, що називалося б ідентифікатором групи потоків, і отримує унікальний ідентифікатор для процесу, який потрібно використовувати gettid. Однак, крім ядра, більшість людей та інструментів називатимуть групу потоків процесом і називають процес потоком для узгодженості з іншими системами.
користувач253751

Не зовсім. Процес має свою власну пам'ять і дескриптори файлів, він ніколи не називається нитка, це було б в відповідно до іншими системами.
reinierpost

Відповіді:


50

Я думаю, що ця частина clone(2)чоловічої сторінки може усунути різницю. PID:

CLONE_THREAD (з Linux 2.4.0-test8)
Якщо встановлено CLONE_THREAD, дитина розміщується в тій же групі потоків, що і процес виклику.
Групи ниток були функцією, доданою в Linux 2.4 для підтримки поняття потоків POSIX про набір потоків, які мають спільний єдиний PID. Всередині цього спільного PID є так званим ідентифікатором групи потоків (TGID) для групи потоків. Оскільки Linux 2.4, виклики getpid (2) повертають TGID абонента.

Фраза "потоки реалізовані як процеси" відноситься до випуску потоків, які мали окремі PID в минулому. В основному, Linux спочатку не мав потоків у процесі, а лише окремі процеси (з окремими PID), які могли мати деякі спільні ресурси, наприклад, віртуальна пам'ять або дескриптори файлів. CLONE_THREADі розділення ідентифікатора процесу (*) та ідентифікатора потоку робить поведінку Linux більше схожим на інші системи та більше схоже на вимоги POSIX у цьому сенсі. Хоча технічно ОС все ще не має окремих реалізацій для потоків і процесів.

Обробка сигналів була ще однією проблемою для старої реалізації, про це більш докладно описано у статті, на яку @FooF посилається у своїй відповіді .

Як зазначається в коментарях, Linux 2.4 також вийшов у 2001 році, того ж року, що і книга, тому не дивно, що новини не потрапили до цього друку.


2
окремі процеси, які, можливо, мали деякі спільні ресурси, наприклад, віртуальна пам'ять або дескриптори файлів. Це майже все ще працює як потоки Linux, і проблеми, які ви згадуєте, були очищені. Я б сказав, що називати одиниці планування, які використовуються в ядрі, "потоки" або "процеси", дійсно не має значення. Те, що вони почали в Linux називатися лише "процесами", не означає, що це все, що зараз є.
Ендрю Генле

@AndrewHenle, так, відредагували трохи. Я сподіваюся, що захоплює вашу думку, хоча мені здається, що важко з формулюванням. (продовжуйте і відредагуйте цю частину, якщо вам подобається.) Я зрозумів, що деякі інші Unix-подібні ОС мають більш чітке розділення потоків проти процесів, при цьому Linux є винятком лише у тому, що дійсно має один тип обслуговування обидві функції. Але я недостатньо знаю про інші системи і не маю підручників, тому важко сказати щось конкретне.
ilkkachu

@tomas Зауважте, що ця відповідь пояснює, як працює Linux зараз. Як натякає ilkkachu, вона працювала інакше, коли писалася книга. Відповідь FooF пояснює, як працював Linux у той час.
Жиль

38

Ви маєте рацію, адже "щось мало змінитися між 2001 і зараз". Книга, яку ви читаєте, описує світ відповідно до першої історичної реалізації потоків POSIX в Linux, що називається LinuxThreads (див. Також статтю Вікіпедії для деяких).

У LinuxThreads виникли деякі проблеми сумісності зі стандартом POSIX - наприклад, потоки, які не поділяють PID, - та деякі інші серйозні проблеми. Щоб виправити ці недоліки, Red Hat очолив іншу реалізацію під назвою NPTL (Native POSIX Thread Library), щоб додати необхідну підтримку бібліотеки ядра та користувальницького простору для досягнення кращої відповідності POSIX (прийняття хороших частин ще одного конкуруючого проекту реімплементації IBM під назвою NGPT (" Нитки Posix наступного покоління "), див. Статтю Вікіпедії про NPTL ). Додаткові прапори, додані до clone(2)системного виклику (особливо CLONE_THREADце @ikkkachuвказується у його відповіді ), мабуть, є найбільш очевидною частиною модифікацій ядра. Частина роботи простору користувача була включена до бібліотеки GNU C.

Тим НЕ менше в даний час деякі вбудовані Linux SDKs використовувати стару реалізацію LinuxThreads , тому що вони використовують менше пам'яті слід версії LibC називається uClibc (також званий μClibc) , і треба було значну кількість років до реалізації призначеного для користувача простору NPTL від GNU LibC була перенесена і передбачається , як реалізація нитки POSIX за замовчуванням, як правило, ці спеціальні платформи не прагнуть слідувати останнім модам зі швидкістю блискавки. Це можна спостерігати, помічаючи, що дійсно PID-адреси для різних потоків на цих платформах також відрізняються на відміну від стандарту POSIX - як і книга, яку ви читаєте. Насправді одного разу ви зателефонувалиpthread_create(), ви раптом збільшили кількість процесів з однієї до трьох, оскільки потрібен додатковий процес, щоб зберегти безлад.

Сторінка керівництва pthreads (7) для Linux дає вичерпний та цікавий огляд відмінностей між ними. Ще одним освічуючим, хоч і застарілим, описом відмінностей є цей документ Ульріха Деппера та Інго Молнара про дизайн NPTL.

Я рекомендую вам не сприймати цю частину книги надто серйозно. Я замість цього рекомендую теми Buckinghof для програмування POSIX та сторінки з інструкціями POSIX та Linux щодо цієї теми. Багато навчальних посібників з цього приводу є неточними.


22

(Користувацький простір) потоки не реалізовані як такі процеси як Linux в Linux, оскільки вони не мають власного приватного адресного простору, вони все ще поділяють адресний простір батьківського процесу.

Однак ці потоки реалізовані для використання системи обліку ядра процесів, тому виділяються власні ідентифікатори потоку (TID), але їм надається той самий PID та 'ідентифікатор групи потоків' (TGID), як і батьківський процес - це на відміну від вилка, де створюються нові TGID і PID, а TID - це те саме, що і PID.

Отже, здається, що останні ядра мали окремий TID, який можна запитувати, саме для потоків це відрізняється, відповідний фрагмент коду, щоб показати це у кожній з основних () потокових функцій (), вище:

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

Отже весь код з цим буде:

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

Надання прикладу виводу:

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

3
@tomas einonm має рацію. Нехтувати тим, що йдеться в книзі, це жахливо заплутано. Данно знаю, яку ідею хотів передати автор, але він погано провалився. Так, у Linux у вас є потоки ядра та потоки користувачів. Потоки ядра - це по суті процеси без користувальницького простору. Нитки простору користувача - це звичайні потоки POSIX. Процеси користувальницького простору діляться дескрипторами файлів, можуть ділитися сегментами коду, але живуть у абсолютно окремих просторах віртуальних адрес. Потоки користувальницького простору в сегменті коду поділяють процес, статичну пам'ять і купу (динамічна пам'ять), але мають окремі набори процесорів і стеки.
Борис Бурков

8

В основному, інформація у вашій книзі є історично достовірною через ганебно погану історію реалізації потоків у Linux. Ця моя відповідь на пов’язане з цим питання також відповідає на ваше запитання:

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

Ці плутанини пов'язані з тим, що розробники ядра спочатку дотримувалися ірраціонального та неправильного уявлення про те, що потоки можуть бути реалізовані майже повністю у просторі користувачів, використовуючи процеси ядра як примітивні, доки ядро ​​пропонувало спосіб змусити їх ділитися пам'яттю та дескрипторами файлів . Це призвело до сумно поганої реалізації LinuxThreads потоків POSIX, що було скоріше помилкою, оскільки воно не дало нічого, що нагадувало б семантику потоків POSIX. Врешті-решт LinuxThreads було замінено (NPTL), але багато незрозумілої термінології та непорозумінь зберігаються.

Перше і найважливіше, що потрібно усвідомити, це те, що "PID" означає різні речі в просторі ядра та просторі користувача. Те, що ядро ​​викликає PID, - це фактично ідентифікатори потоків на рівні ядра (часто їх називають TID), не плутати з pthread_tякими є окремий ідентифікатор. Кожен потік у системі, будь то в одному або іншому процесі, має унікальний TID (або "PID" в термінології ядра).

Те, що вважається PID в POSIX-сенсі "процес", з іншого боку, називається "ідентифікатором групи потоків" або "TGID" в ядрі. Кожен процес складається з одного або декількох потоків (ядерних процесів), кожен з яких має свій власний TID (ядро PID), але всі вони обмінюються тим самим TGID, який дорівнює TID (PID ядра) початкового потоку, в якому mainпрацює.

Коли topви показуєте вам потоки, він показує TID (PID ядра), а не PID (TGID ядра), і тому кожен потік має окремий.

З появою NPTL більшість системних викликів, які приймають аргумент PID або діють на процес виклику, були змінені, щоб трактувати PID як TGID та діяти на всю "групу потоків" (процес POSIX).


8

Внутрішньо в ядрі Linux немає такого поняття, як процеси або потоки. Процеси та потоки - це в основному поняття userland, саме ядро ​​бачить лише "завдання", які є об'єктом, який може бути запланований, і який може не надавати жодним, деяким або всім його ресурсам іншим завданням. Нитки - це завдання, налаштовані на обмін більшою частиною його ресурсів (адресний простір, mmaps, труби, відкриті обробники файлів, розетки тощо) з батьківським завданням, а процеси - це завдання, налаштовані для спільного використання мінімальних ресурсів з батьківським завданням .

Якщо ви використовуєте Linux API безпосередньо ( clone () , а не fork () та pthread_create () ), ви маєте набагато більшу гнучкість у визначенні кількості ресурсів для спільного використання або не для спільного використання, і ви можете створювати завдання, які не є ні повністю процес ні повністю нитка. Якщо ви використовуєте ці дзвінки низького рівня безпосередньо, можливо також створити завдання за допомогою нового TGID (таким чином трактується як процес більшості інструментів користувача), який фактично ділиться всіма своїми ресурсами з батьківською задачею, або навпаки, для створення завдання зі спільним TGID (таким чином, трактується як потік у більшості інструментів користувальницької програми), що не має жодного ресурсу з його батьківським завданням.

У той час як Linux 2.4 реалізує TGID, це здебільшого стосується лише обліку ресурсів. Багато користувачів та інструмент простору користувачів вважають корисним можливість групувати пов'язані завдання та разом звітувати про використання ресурсів.

Реалізація завдань в Linux набагато більш текуча, ніж світогляд процесів і потоків, представлений засобами простору користувачів.


У статті @FooF посилається на опис декількох моментів, де ядро ​​повинно розглядати процеси та потоки як окремі сутності (наприклад, обробка сигналів та exec ()), тому, прочитавши його, я б не дуже сказав, що "такого немає річ як процеси або потоки в ядрі Linux ".
ilkkachu

5

Лінус Торвальдс заявив у публікації зі списком розсилки ядра в 1996 році, що "і потоки, і процеси трактуються як" контекст виконання ", що є" просто конгломератами всього стану цієї Колегії ".... включає такі речі, як CPU стан, стан MMU, дозволи та різні стани зв'язку (відкриті файли, обробники сигналів тощо) ".

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

Як ви бачите, ця програма породжує відразу 25 ниток, кожна з яких спить протягом 100 секунд, а потім знову приєднується до основної програми. Після того, як всі 25 потоків знову приєдналися до програми, програма виконана і вийде.

Використовуючи topви зможете побачити 25 екземплярів програми "теми2". Але дитині нудно. Вихід ps auwxнавіть менш цікавий ... Але НЕ ps -eLfстає цікавим.

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

Тут ви можете побачити всі 26 РЄ, які thread2програма створила. Всі вони мають однаковий ідентифікатор процесу (PID) та ідентифікатор батьківського процесу (PPID), але кожен має інший ідентифікатор LWP (легкий процес), а кількість LWP (NLWP) вказує, що 26 КС - основна програма та 25 ниток, породжених нею.


Правильно, нитка - це просто легкий процес (LWP)
fpmurphy

3

Що стосується Linux процесів, нитки - це те саме. Який повинен сказати , що вони створюються з допомогою тієї ж системи виклику: clone.

Якщо ви подумаєте про це, різниця між потоками та процесами полягає в тому, в якому об’єкти ядра будуть спільні між дитиною та батьком. Для процесів це не так багато: відкриті дескриптори файлів, сегменти пам'яті, до яких не було написано, напевно, кілька інших, про які я не можу придумати верхній частині голови. Для ниток поділяється набагато більше об'єктів, але не всіх.

Що робить нитки та об’єкти ближчими в Linux, це unshareсистемний виклик. Об'єкти ядра, які починаються як спільні, можуть бути видалені після створення потоку. Таким чином, ви можете, наприклад, мати два потоки одного процесу, які мають різний простір дескрипторів файлів (шляхом відкликання спільного використання дескрипторів файлів після створення потоків). Ви можете перевірити його самостійно, створивши потік, зателефонувавши unshareв обидва потоки, а потім закривши всі файли та відкривши нові файли, труби або об’єкти в обох потоках. Потім загляньте, /proc/your_proc_fd/task/*/fdі ви побачите, що кожен task(який ви створили як потік) матиме різні фд.

Насправді, як створення нових потоків, так і нових процесів - це підпрограми бібліотеки, які дзвонять cloneпід них і вказують, який із об'єктів ядра новостворений процес-потік-thingamajig (тобто task) буде спільним з викликовим процесом / потоком.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.