Зачем нам форк для создания новых процессов?

95

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

Почему мы сначала создаем копию родительского процесса, а не создаем новый процесс напрямую?

Sarthak
источник
2
См. Также unix.stackexchange.com/questions/31118/…
Эллен Спертус

Ответы:

61

Короткий ответ - forkв Unix, потому что в то время его было легко вписать в существующую систему, и потому что в предшествующей системе в Беркли использовалась концепция вилок.

Из «Эволюции системы разделения времени Unix» (соответствующий текст выделен ):

Управление процессом в его современном виде было разработано и реализовано в течение нескольких дней. Удивительно, как легко он вписался в существующую систему; в то же время легко увидеть, как некоторые из немного необычных особенностей дизайна присутствуют именно потому, что они представляют собой небольшие, легко кодируемые изменения того, что существовало . Хорошим примером является разделение функций fork и exec. Наиболее распространенная модель для создания новых процессов включает в себя указание программы для выполнения процесса; в Unix разветвленный процесс продолжает запускать ту же программу, что и его родитель, пока он не выполнит явное exec. Разделение функций, безусловно, не является уникальным для Unix, и фактически оно присутствовало в системе разделения времени Беркли, которая была хорошо известна Томпсону., Тем не менее, кажется разумным предположить, что он существует в Unix, главным образом, из-за легкости, с которой можно реализовать fork без каких-либо изменений . Система уже обработала несколько (т.е. два) процесса; там была таблица процессов, и процессы переключались между основной памятью и диском. Требуется только первоначальная реализация fork

1) Расширение таблицы процессов

2) Добавление вызова вилки, который скопировал текущий процесс в область подкачки диска, используя уже существующие примитивы ввода-вывода подкачки, и внес некоторые изменения в таблицу процессов.

Фактически, для вызова вызова PDP-7 требовалось ровно 27 строк кода сборки. Конечно, требовались другие изменения в операционной системе и пользовательских программах, и некоторые из них были довольно интересными и неожиданными. Но комбинированный fork-exec был бы значительно сложнее , хотя бы потому, что exec как такового не существовало; его функция уже была выполнена с использованием явного ввода-вывода оболочкой.

С тех пор Unix развивался. forkс последующим execбольше не единственный способ запустить программу.

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

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

Марк Плотник
источник
5
Хороший ответ, но я бы добавил, что vfork больше не должен использоваться. Разница в производительности сейчас незначительна, и ее использование может быть опасным. См. Этот вопрос SO stackoverflow.com/questions/4856255/…, этот сайт ewontfix.com/7 и "Расширенное программирование Unix" страница 299 о vfork
Рафаэль Аренс
4
Махинации (настройка структуры данных), которые необходимо использовать posix_spawn()для выполнения тех же заданий повторной репликации, которые можно легко выполнить с помощью fork()встроенного кода, являются убедительным аргументом в пользу того, fork()что их гораздо проще использовать.
Джонатан Леффлер
34

[Я повторю часть моего ответа отсюда .]

Почему бы просто не иметь команду, которая создает новый процесс с нуля? Разве не абсурдно и неэффективно копировать тот, который будет заменен сразу?

На самом деле это, вероятно, будет не так эффективно по нескольким причинам:

  1. «Копия» производится fork()немного абстракции, так как ядро использует копирования при записи системы ; все, что действительно нужно создать, это карта виртуальной памяти. Если копия немедленно вызывает exec(), большая часть данных, которые были бы скопированы, если бы она была изменена в результате действия процесса, фактически никогда не должна копироваться / создаваться, потому что процесс не делает ничего, требующего его использования.

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

Чтобы объяснить # 1 немного дальше, память, которая «копируется», но к которой впоследствии не осуществляется доступ, никогда не копируется, по крайней мере, в большинстве случаев. Исключением в этом контексте может быть то, что если вы разветвили процесс, а потом родительский процесс завершил работу, прежде чем дочерний процесс заменил себя на exec(). Я бы сказал, потому что большая часть родительского объекта может быть кэширована, если имеется достаточно свободной памяти, и я не уверен, в какой степени это будет использовано (что будет зависеть от реализации ОС).

Конечно, это не делает использование копии более эффективным, чем использование чистого листа - за исключением того, что «чистый лист» не является буквально ничем и должен включать распределение. Система может иметь общий пустой / новый шаблон процесса, который копируется таким же образом 1, но тогда это ничего не спасет по сравнению с форком копирования при записи. Итак, № 1 просто демонстрирует, что использование «нового» пустого процесса не будет более эффективным.

Пункт № 2 объясняет, почему использование вилки, вероятно, более эффективно. Окружение дочернего объекта наследуется от его родителя, даже если это совершенно другой исполняемый файл. Например, если родительский процесс представляет собой оболочку, а дочерний веб-браузер $HOMEвсе еще одинаков для них обоих, но, поскольку каждый из них может впоследствии изменить его, это должны быть две отдельные копии. Тот, что в ребенке, производится оригиналом fork().

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

лютик золотистый
источник
3
Хотя обе точки верны, ни одна из них не поддерживает, почему был выбран метод разветвления вместо создания нового процесса из данного исполняемого файла.
SkyDan
3
Я думаю, что это действительно отвечает на вопрос. Fork используется потому, что в случаях, когда создание нового процесса является наиболее эффективным способом, стоимость использования fork вместо этого тривиальна (вероятно, менее 1% от стоимости создания процесса). С другой стороны, есть много мест, где fork значительно эффективнее или намного проще API (например, обработка файловых дескрипторов). Решение, принятое Unix, состояло в том, чтобы поддерживать только один API, упрощая спецификацию.
Cort Ammon
1
@SkyDan Вы правы, это скорее ответ на вопрос « почему нет» , чем « почему» , на который Марк Плотник отвечает более прямо - что я хотел бы интерпретировать не только как то, что это был самый простой выбор, но также и, что он был, вероятно, наиболее эффективным выбор (согласно цитате Денниса Ричи: «Для вызова вызова PDP-7 потребовалось ровно 27 строк сборки ... exec как такового не существовало; его функция уже была выполнена»). Так что это «почему не» действительно размышляла о двух стратегий , в котором один поверхностно кажется более простым и более эффективным, когда , возможно , это не (свидетель сомнительную участь ...
Златовласка
1
Златовласка это правильно. Есть ситуации, когда разветвление и изменение дешевле, чем создание нового с нуля. Конечно, самый экстремальный пример - это любое время, когда вам нужно поведение вилки. fork()может это очень быстро (как упомянул GL, порядка 27 линий сборки). Если вы хотите «создать процесс с нуля» в другом направлении, это будет fork()стоить лишь чуть-чуть дороже, чем начинать с пустого процесса (27 строк сборки + стоимость закрытия файловых дескрипторов). Так что forkобрабатывает обе вилки и создает хорошо, в то время как createможет обрабатывать только хорошо.
Корт Аммон
2
Ваш ответ касался аппаратных улучшений: виртуальная память, копирование при записи. До этого forkфактически копировалась вся память процесса, и это было очень дорого.
Бармар
6

Я думаю, что причина, по которой Unix имела только forkфункцию создания новых процессов, является результатом философии Unix

Они строят одну функцию, которая делает одну вещь хорошо. Это создает дочерний процесс.

Что делать с новым процессом, решать программисту. Он может использовать одну из exec*функций и запустить другую программу, или он не может использовать exec и использовать два экземпляра одной и той же программы, что может быть полезно.

Таким образом, вы получаете большую степень свободы, так как вы можете использовать

  1. вилка без exec *
  2. вилка с exec * или
  3. просто exec * без вилки

и, кроме того, вам нужно запомнить forkтолько exec*вызовы функций и функций, которые вам приходилось делать в 1970-х годах.

Рафаэль Аренс
источник
3
Я понимаю, как работают вилки и как их использовать. Но зачем мне создавать новый процесс, когда я могу делать то же самое, но с меньшими усилиями? Например, мой учитель дал мне задание, в котором я должен был создать процесс для каждого числа, переданного в argv, чтобы проверить, является ли число простым числом. Но разве это не обходной путь в конечном итоге сделать то же самое? Я мог бы просто использовать массив и использовать функцию для каждого числа ... Так почему же мы создаем дочерние процессы, а не выполняем всю обработку в основном процессе?
user1534664
2
Рискну сказать, что вы понимаете, как работают вилки и как их использовать, потому что у вас когда-то был учитель, который дал вам задание, в котором вы должны были создать группу процессов (с номером, указанным во время выполнения), контролировать их, координировать их и общаться между ними. Конечно, никто не сделал бы что-то тривиальное в реальной жизни. Но если у вас есть большая проблема, которую легко разложить на части, которые можно обрабатывать параллельно (например, обнаружение краев в изображении), разветвление позволяет вам использовать несколько ядер ЦП одновременно.
Скотт
5

Существует две философии создания процесса: разветвление с наследованием и создание с аргументами. Unix использует fork, очевидно. (Например, OSE и VMS используют метод create.) Unix обладает МНОГИМИ наследуемыми характеристиками, а другие периодически добавляются. По наследству эти новые характеристики могут быть добавлены БЕЗ ИЗМЕНЕНИЯ СУЩЕСТВУЮЩИХ ПРОГРАММ! Используя модель создания с аргументами, добавление новых характеристик будет означать добавление новых аргументов к вызову создания. Модель Unix проще.

Это также дает очень полезную модель ветвления без exec, где процесс может быть разделен на несколько частей. Это было жизненно важно еще тогда, когда не было формы асинхронного ввода-вывода, и полезно, когда используются преимущества нескольких процессоров в системе. (Предварительные темы.) Я делал это много за эти годы, даже недавно. По сути, он позволяет объединять несколько «программ» в одну программу, поэтому абсолютно нет места для искажений, несовпадений версий и т. Д.

Модель fork / exec также дает возможность конкретному дочернему элементу наследовать радикально странную среду, установленную между fork и exec. Такие вещи, как унаследованные файловые дескрипторы, особенно. (Расширение stdio fd.) Модель create не предлагает возможности наследовать все, что не было предусмотрено создателями вызова create.

Некоторые системы также могут поддерживать динамическую компиляцию нативного кода, когда процесс фактически создает свою собственную программу с нативным кодом. Другими словами, ему нужна новая программа, которую он пишет сам на лету, БЕЗ необходимости проходить цикл исходного кода / компилятора / компоновщика и занимать место на диске. (Я полагаю, что существует языковая система Verilog, которая делает это.) Модель fork поддерживает это, модель создания обычно не будет.

Джим Кэти
источник
Файловые дескрипторы не являются «расширением stdio»; Файловые указатели stdio являются оболочкой для файловых дескрипторов. Сначала появились файловые дескрипторы, и они являются основными дескрипторами ввода-вывода Unix. Но, в остальном, это хороший момент.
Скотт
2

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

введите описание изображения здесь

как показано, когда процесс является отцом fork () возвращает идентификатор процесса сына, PID иначе он возвращает0

Например, вы можете использовать его, если у вас есть процесс (веб-сервер), который получает запросы, и при каждом запросе он создает son processдля обработки этого запроса, здесь у отца и его сыновей разные задания.

ТАК, не запускать копию процесса не совсем так, как fork ().

Сетевик
источник
5
Хотя это правда, это не отвечает на вопрос. Зачем нужна ветвь для создания процесса, если я хочу запустить другой исполняемый файл?
SkyDan
1
Я согласен со SkyDan - это не отвечает на вопрос. posix_spawn - несколько более причудливая версия того, что можно было представить 30 лет назад (до появления Posix) как функцию fork_execve ; тот, который создает новый процесс, инициализируя его образ из исполняемого файла, даже не намекая на копирование образа родительского процесса (за исключением списка аргументов, среды и атрибутов процесса (например, рабочего каталога)), и возвращает PID нового процесса для вызывающей стороны (родительский процесс) .
Скотт
1
Есть и другие способы передачи «родительской» информации ребенку. Метод с возвращаемым значением, оказывается, является наиболее эффективным способом сделать это, fork если вы предполагаете, что хотите forkв первую очередь
Cort Ammon
0

Перенаправление ввода / вывода наиболее легко реализовать после fork и перед exec. Дочерний, зная, что это дочерний элемент, может закрывать файловые дескрипторы, открывать новые, dup () или dup2 (), чтобы получить их на правильный номер fd и т. Д., И все это без воздействия на родителя. После этого и, возможно, изменения любых желаемых переменных среды (также не влияющих на родительский), он может выполнить новую программу в специализированной среде.

Ричард Гамильтон
источник
Все, что вы делаете здесь, это повторение третьего абзаца ответа Джима Кэти с чуть-чуть более подробной информацией.
Скотт
-2

Я думаю, что все здесь знают, как работает fork, но вопрос в том, почему нам нужно создать точную копию родительского элемента с помощью fork? Ответ ==> Возьмем пример сервера (без разветвления), когда клиент-1 обращается к серверу, если в это же время прибыл второй клиент-2 и хочет получить доступ к серверу, но сервер не дает разрешения вновь прибывшему client-2, потому что сервер занят обслуживанием client-1, поэтому client-2 должен ждать. После того, как все службы для client-1 завершены, client-2 теперь может получить доступ к серверу. Теперь рассмотрим, если в то же время клиент-3 прибывает, поэтому клиент-3 должен ждать, пока все службы для клиента-2 не будут завершены. Возьмите сценарий, когда тысячи клиентов должны получить доступ к серверу одновременно ... тогда все клиенты должны подождите (сервер занят !!).

Этого можно избежать, создав (используя fork) точную дубликатную копию (т. Е. Дочернюю) сервера, где каждый дочерний элемент (точная дублирующая копия его родительского, т. Е. Сервера) выделен вновь поступившему клиенту, таким образом, одновременно все клиенты получают доступ к одному и тому же сервер.

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