Как использовать fork () в Unix?Почему не что-то вроде форка (pointerToFunctionToRun)? - PullRequest
8 голосов
/ 12 ноября 2010

У меня возникли проблемы с пониманием того, как использовать Unix fork().Я привык, когда нужно распараллеливать, порождая потоки в моем приложении.Это всегда что-то вроде

CreateNewThread(MyFunctionToRun());

void myFunctionToRun() { ... }

Теперь, когда я узнал о fork() Unix, мне были даны примеры вида:

fork();
printf("%d\n", 123);

, в котором код после форка"разделить".Я не могу понять, как fork () может быть полезным.Почему fork () не имеет синтаксиса, аналогичного описанному выше CreateNewThread (), где вы передаете ему адрес функции, которую хотите запустить?

Чтобы выполнить нечто подобное CreateNewThread (), я быЯ должен быть креативным и делать что-то вроде

//pseudo code
id = fork();

if (id == 0) { //im the child
    FunctionToRun();
} else { //im the parent
    wait();
}

Может быть, проблема в том, что я так привык к созданию потоков способом .NET, что я не могу ясно об этом думать.Что мне здесь не хватает?Каковы преимущества fork() над CreateNewThread()?

PS: я знаю, fork() создаст новый процесс , в то время как CreateNewThread() создаст новый поток.

Спасибо

Ответы [ 8 ]

9 голосов
/ 12 ноября 2010

fork() говорит: «скопируйте текущее состояние процесса в новый процесс и запустите его прямо здесь».Поскольку код выполняется в двух процессах, он фактически возвращается дважды: один раз в родительском процессе (где он возвращает идентификатор процесса дочернего процесса) и один раз в дочернем (где он возвращает ноль).

ТамЕсть множество ограничений на то, что безопасно вызывать в дочернем процессе после fork() (см. ниже).Ожидается, что вызов fork() был первой частью процесса запуска нового процесса, запускающего новый исполняемый файл с собственным состоянием.Вторая часть этого процесса - это вызов execve() или один из его вариантов, который указывает путь к исполняемому файлу, который будет загружен в текущий запущенный процесс, аргументы, которые должны быть предоставлены этому процессу, и переменные окружения, которые его окружают.процесс.(Ничто не мешает вам повторно выполнить исполняемый в данный момент исполняемый файл и предоставить флаг, который позволит ему выбрать, где остановился родитель, если это то, что вы действительно хотите.)

UNIX fork()-exec()танец примерно эквивалентен Windows CreateProcess().Более новая функция еще больше похожа на это: posix_spawn().

В качестве практического примера использования fork() рассмотрим оболочку, например bash.fork() все время используется командной оболочкой.Когда вы указываете оболочке запустить программу (например, echo "hello world"), она разветвляется и затем запускает эту программу.Конвейер представляет собой набор разветвленных процессов с stdout и stdin, соответствующим образом подключенными родительским процессом между fork() и exec().

Если вы хотите создать новый поток, вам следует использоватьбиблиотека потоков Posix.Вы создаете новую тему Posix (pthread), используя pthread_create().Ваш пример CreateNewThread() будет выглядеть следующим образом:

#include <pthread.h>

/* Pthread functions are expected to accept and return void *. */ 
void *MyFunctionToRun(void *dummy __unused);

pthread_t thread;
int error = pthread_create(&thread,
        NULL/*use default thread attributes*/,
        MyFunctionToRun,
        (void *)NULL/*argument*/);

До того, как стали доступны потоки, fork() была самой близкой вещью, которую UNIX предоставил для многопоточности.Теперь, когда потоки доступны, использование fork() почти полностью ограничено порождением нового процесса для выполнения другого исполняемого файла.

ниже: ограничения заключаются в том, что fork() предшествует многопоточности, поэтому только поток, вызывающийfork() продолжает выполняться в дочернем процессе.По POSIX :

Процесс должен быть создан с одним потоком.Если многопоточный процесс вызывает fork (), новый процесс должен содержать реплику вызывающего потока и всего его адресного пространства, возможно, включая состояния мьютексов и других ресурсов.Следовательно, чтобы избежать ошибок, дочерний процесс может выполнять только асинхронно-безопасные операции до тех пор, пока не будет вызвана одна из функций exec.[THR] [Option Start] Обработчики вил можно установить с помощью функции pthread_atfork (), чтобы поддерживать инварианты приложения в вызовах fork ().[Option End]

Когда приложение вызывает fork () из обработчика сигнала, а любой из обработчиков fork, зарегистрированных pthread_atfork (), вызывает функцию, которая не безопасна для асинхронного сигнала, поведение не определено.

Поскольку любая библиотечная функция, которую вы вызываете, могла породить поток от вашего имени, параноидальное предположение состоит в том, что вы всегда ограничены выполнением асинхронно-безопасных операций в дочернем процессе между вызовом fork() иexec().

7 голосов
/ 12 ноября 2010

За исключением истории, существуют некоторые принципиальные различия в отношении владения ресурсом и времени жизни между процессами и потоками.

Когда вы выполняете ветвление, новый процесс занимает совершенно отдельное пространство памяти.Это очень важное отличие от создания новой темы.В многопоточных приложениях вы должны учитывать, как вы получаете доступ к общим ресурсам и манипулируете ими.Обработанные, которые были разветвлены, должны явно обмениваться ресурсами, используя средства межпроцессного взаимодействия, такие как общая память, каналы, удаленные вызовы процедур, семафоры и т. Д.

Другое отличие состоит в том, что дочерние объекты fork () могут пережить своего родителягде все потоки умирают после завершения процесса.

В архитектуре клиент-сервер, где ожидается очень и очень длительное время безотказной работы, использование fork () вместо создания потоков может быть допустимой стратегией для борьбы с утечками памяти.Вместо того, чтобы беспокоиться об устранении утечек памяти в ваших потоках, вы просто отключаете новый дочерний процесс для обработки каждого клиентского запроса, а затем убиваете дочерний процесс, когда он завершен.В этом случае единственным источником утечек памяти будет родительский процесс, который отправляет события.

Аналогия: порождения потоков можно рассматривать как открытие вкладок внутри одного окна браузера, а разветвление - это как открытие отдельных окон браузера.

6 голосов
/ 12 ноября 2010

Было бы правильнее спросить, почему CreateNewThread не просто возвращает идентификатор потока, как fork() делает ... в конце концов fork() устанавливает прецедент. Ваше мнение просто окрашено тем, что вы видели одно перед другим. Сделайте шаг назад и подумайте, что fork() дублирует процесс и продолжает выполнение ... что может быть лучше, чем в следующей инструкции? Зачем усложнять ситуацию, добавляя вызов функции в сделку (а затем тот, который занимает всего void*)?

В вашем комментарии к Майку написано «Я не могу понять, в каких контекстах вы хотите его использовать». . В основном вы используете его, когда хотите:

  • запустить другой процесс, используя семейство функций exec
  • выполнять некоторую параллельную обработку независимо (с точки зрения использования памяти, обработки сигналов, ресурсов, безопасности, надежности), например:
    • каждый процесс может иметь навязчивые ограничения на количество дескрипторов файлов, которыми он может управлять, или в 32-разрядной системе - объем памяти: второй процесс может совместно использовать работу, получая свои собственные ресурсы
    • веб-браузеры, как правило, разветвляют отдельные процессы, потому что они могут выполнить некоторую инициализацию, а затем вызвать функции операционной системы, чтобы навсегда уменьшить свои привилегии (например, изменить идентификатор пользователя с меньшим доверием, изменить корневой каталог, в котором они могут обращаться к файлам, или сделать некоторые страницы памяти доступными только для чтения); большинство ОС не допускают одинаковую степень детальной настройки разрешений для каждого потока; Другое преимущество состоит в том, что если дочерний процесс seg-faults или аналогичный родительский процесс может обработать это и продолжить, тогда как аналогичные ошибки в многопоточном коде вызывают вопросы о том, была ли повреждена память - или удерживались блокировки - потоком сбоя, так что остальные темы скомпрометированы

Кстати / использование UNIX / Linux не означает, что вы должны отказаться от потоков для fork() процессов ... вы можете использовать pthread_create() и связанные с ними функции, если вам удобнее Парадигма потоков.

2 голосов
/ 12 ноября 2010

Форк был с нами очень и очень долго.Форк думал о том, что идея «запустить поток, выполняющий определенную функцию» ни у кого не мерцала.

Люди не используют fork, потому что это «лучше», мы используем его, потому что этоединственная непривилегированная функция создания процесса в пользовательском режиме, которая работает во всех вариациях Linux.Если вы хотите создать процесс , вам нужно позвонить fork.И для некоторых целей вам нужен процесс, а не нить.

Возможно, вы захотите изучить ранние статьи по этому вопросу.

2 голосов
/ 12 ноября 2010

Оставляя разницу между порождением процесса и потока, отложенного на секунду: в основном, fork () является более фундаментальным примитивом.В то время как SpawnNewThread должен выполнить некоторую фоновую работу, чтобы установить счетчик программ в нужное место, fork не выполняет такую ​​работу, он просто копирует (или фактически копирует) память вашей программы и продолжает счетчик.

1 голос
/ 12 ноября 2010

Стоит отметить, что многопоточная обработка не совсем такая же, как многопоточность многопоточность . Новый процесс, созданный fork, имеет очень маленький контекст со старым, что сильно отличается от случая с потоками.

Итак, давайте посмотрим на unixy thread system: pthread_create имеет семантику, аналогичную CreateNewThread.

Или, чтобы перевернуть его, давайте посмотрим на windows (или java или другую систему, которая зарабатывает на жизнь потоками), способ порождать процесс, идентичный тому, который вы в данный момент выполняете (что и делает fork). на Unix) ... ну, мы могли бы, за исключением того, что не один: это просто не часть модели "все потоки все время". (Что неплохо, заметьте, просто другое).

1 голос
/ 12 ноября 2010

Вы fork всякий раз, когда вы хотите более чем одну вещь одновременно. Это называется многозадачностью и действительно полезно.

Вот, например, программа, подобная telnetish:

#!/usr/bin/perl
use strict;
use IO::Socket;
my ($host, $port, $kidpid, $handle, $line);

unless (@ARGV == 2) { die "usage: $0 host port" }
($host, $port) = @ARGV;

# create a tcp connection to the specified host and port
$handle = IO::Socket::INET->new(Proto     => "tcp",
                                PeerAddr  => $host,
                                PeerPort  => $port)
       or die "can't connect to port $port on $host: $!";

$handle->autoflush(1);              # so output gets there right away
print STDERR "[Connected to $host:$port]\n";

# split the program into two processes, identical twins
die "can't fork: $!" unless defined($kidpid = fork());

if ($kidpid) {                      
    # parent copies the socket to standard output
    while (defined ($line = <$handle>)) {
        print STDOUT $line;
    }
    kill("TERM" => $kidpid);        # send SIGTERM to child
}
else {                              
    # child copies standard input to the socket
    while (defined ($line = <STDIN>)) {
        print $handle $line;
    }
}
exit;

Видишь, как это просто?

0 голосов
/ 15 ноября 2013

Наиболее популярное использование Fork () - это способ клонирования сервера для каждого нового клиента connect () (потому что новый процесс наследует все файловые дескрипторы в том состоянии, в котором они существуют).Но я также использовал его для запуска новой (локально работающей) службы по требованию клиента.Эту схему лучше всего выполнить с двумя вызовами fork () - один остается в родительском сеансе до тех пор, пока сервер не будет запущен и не сможет подключиться, другой (я отключаю его от дочернего) становится сервером и покидает родительский сеанстак что больше не может быть достигнут (скажем) SIGQUIT.

...