Реализуются ли потоки как процессы в 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;
}
Tomasz
источник
1
Я не понимаю, в чем причина вашего замешательства. Потоки реализованы как процессы, совместно использующие адресное пространство со своим родителем.
Йохан Мирен
2
@ JohanMyréen Так почему же нити pids равны?
Томаш
Ах, теперь я вижу. Да, что-то действительно изменилось. Смотрите ответ @ ilkkachu.
Йохан Мирен
5
Потоки по- прежнему реализованы как процессы, однако теперь они getpidвозвращают так называемый идентификатор группы потоков и получают уникальный идентификатор для процесса, который вам нужно использовать gettid. Однако, кроме ядра, большинство людей и инструментов будут вызывать поток, группирующий процесс, и вызывать процесс потоком для согласованности с другими системами.
user253751
На самом деле, нет. Процесс имеет свою собственную память и дескрипторы файлов, он никогда не называется нить, это было бы в соответствии с другими системами.
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 году, в том же году, что и книга, так что неудивительно, что новости не попали в эту печать.

ilkkachu
источник
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 SDK используют старую реализацию LinuxThreads, поскольку они используют уменьшенную версию LibC, занимающую меньший объем памяти, называемую uClibc (также называемую µClibc) , и потребовалось немало лет, прежде чем реализация пространства пользователя NPTL из GNU LibC была портирована и принята за реализация потоков POSIX по умолчанию, как правило, эти специальные платформы не стремятся с молниеносной скоростью следовать новейшим модам. Это можно заметить, заметив, что действительно PID для разных потоков на этих платформах также отличаются в отличие от спецификаций стандарта POSIX - точно так же, как и в книге, которую вы читаете. На самом деле, как только вы позвонилиpthread_create()Вы неожиданно увеличили количество процессов с одного до трех, так как для сохранения беспорядка потребовался дополнительный процесс.

Страница руководства Linux pthreads (7) содержит всесторонний и интересный обзор различий между ними. Другое поучительное, хотя и устаревшее описание различий - это статья Ульриха Деппера и Инго Молнара о разработке NPTL.

Я рекомендую вам не относиться к этой части книги слишком серьезно. Вместо этого я рекомендую Butenhof's Programming потоки POSIX и справочные страницы POSIX и Linux по этому вопросу. Многие учебники по этому вопросу неточны.

FOOF
источник
22

Потоки (пользовательского пространства) не реализованы как процессы как таковые в Linux, так как у них нет собственного частного адресного пространства, они все еще совместно используют адресное пространство родительского процесса.

Однако эти потоки реализованы так, чтобы использовать систему учета процессов ядра, поэтому им присваивается собственный идентификатор потока (TID), но им присваиваются тот же PID и «идентификатор группы потоков» (TGID), что и у родительского процесса - это в отличие от форк, где создаются новые TGID и PID, и TID такой же, как PID.

Таким образом, кажется, что последние ядра имели отдельный TID, который можно запрашивать, это то, что отличается для потоков, подходящий фрагмент кода, чтобы показать это в каждом из main () thread_function () выше:

    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
einonm
источник
3
@tomas einonm прав. Не обращайте внимания на то, что написано в книге, это очень запутанно. Не знаю, какую идею хотел донести автор, но он сильно провалился. Итак, в Linux у вас есть потоки ядра и потоки пространства пользователя. Потоки ядра - это, по сути, процессы без пользовательского пространства вообще. Потоки пользовательского пространства - это обычные потоки POSIX. Процессы пользовательского пространства совместно используют файловые дескрипторы, могут совместно использовать сегменты кода, но живут в совершенно отдельных виртуальных адресных пространствах. Потоки пространства пользователя внутри сегмента кода процесса совместно используют статическую память и кучу (динамическую память), но имеют отдельные наборы и стеки регистров процессора.
Борис Бурков
8

По сути, информация в вашей книге исторически точна из-за позорно плохой истории реализации потоков в Linux. Этот мой ответ на связанный вопрос о SO также служит ответом на ваш вопрос:

https://stackoverflow.com/questions/9154671/distinction-between-processes-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 не существует таких процессов или потоков. Процессы и потоки являются в основном концепцией пользовательского пространства, само ядро ​​видит только «задачи», которые являются планируемым объектом, который не может совместно использовать ни один, некоторые или все свои ресурсы с другими задачами. Потоки - это задачи, которые были настроены для совместного использования большей части своих ресурсов (адресного пространства, mmaps, каналов, обработчиков открытых файлов, сокетов и т. Д.) С родительской задачей, а процессы - это задачи, которые были настроены для совместного использования минимальных ресурсов с родительской задачей. ,

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

В то время как Linux 2.4 реализует TGID, это в основном только для учета ресурсов. Многие пользователи и инструмент пространства пользователей считают полезным иметь возможность группировать связанные задачи вместе и сообщать об использовании их ресурсов вместе.

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

Ли Райан
источник
Бумага @FooF связана описывает ряд точек , где ядро имеет рассматривать процессы и потоки как отдельные объекты (например , обработку сигнала и Exec ()), так что после прочтения, я бы на самом деле не сказать , что «нет такого вещь как процессы или потоки в ядре Linux. "
ilkkachu
5

В 1996 году Линус Торвальдс заявил в списке рассылки ядра, что «и потоки, и процессы рассматриваются как« контекст выполнения »», то есть «просто конгломерат всего состояния этого CoE… включает такие вещи, как 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 экземпляров программы "threads2". Но почка скучная. Вывод 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 CoEs, которые thread2создала программа. Все они имеют один и тот же идентификатор процесса (PID) и идентификатор родительского процесса (PPID), но каждый из них имеет свой идентификатор LWP (легкий процесс), а количество LWP (NLWP) указывает на то, что существует 26 CoE - основная программа и 25 потоков, созданных им.

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

Когда дело касается Linux, процессы и потоки - это одно и то же. Который должен сказать , что они создаются с помощью той же системы вызова: clone.

Если вы подумаете об этом, разница между потоками и процессами заключается в том, какие объекты ядра будут совместно использоваться дочерним и родительским объектами. Для процессов это не так уж много: открытые файловые дескрипторы, сегменты памяти, в которые еще не было записано, возможно, несколько других, о которых я не могу вспомнить. Для потоков доступно гораздо больше объектов, но не для всех.

Что делает потоки и объекты ближе в Linux, так это unshareсистемный вызов. Объекты ядра, которые начинаются как общие, могут не быть разделены после создания потока. Таким образом, вы можете, например, иметь два потока одного и того же процесса, которые имеют разное пространство файловых дескрипторов (отменив совместное использование файловых дескрипторов после создания потоков). Вы можете проверить это самостоятельно, создав поток, вызвав unshareоба потока, а затем закрыв все файлы и открыв новые файлы, каналы или объекты в обоих потоках. Затем загляните внутрь, /proc/your_proc_fd/task/*/fdи вы увидите, что у каждого task(который вы создали как поток) будут разные fd.

Фактически, как создание новых потоков, так и новых процессов являются библиотечными процедурами, которые вызывают cloneнижележащие объекты и указывают, какой из объектов ядра вновь созданный процесс-поток-штуковина (то есть task) поделится с вызывающим процессом / потоком.

Дмитрий Рубанович
источник