Почему fork () работает так, как работает - PullRequest
33 голосов
/ 28 ноября 2011

Итак, я использовал fork() и знаю, что он делает. Как новичок я очень боялся этого (и до сих пор не понимаю его полностью). Общее описание fork(), которое вы можете найти в Интернете, состоит в том, что он копирует текущий процесс и назначает другой PID, родительский PID, и процесс будет иметь другое адресное пространство. Все хорошо, однако, учитывая это описание функциональности, начинающий задастся вопросом: «Почему эта функция так важна ... зачем мне копировать мой процесс?». Поэтому я удивился, и в конце концов я обнаружил, что именно так вы можете вызывать другие процессы внутри вашего текущего процесса с помощью семейства execve().

Чего я до сих пор не понимаю, так почему вы должны делать это таким образом? Самым логичным было бы иметь функцию, которую вы можете вызвать как

create_process("executable_path+name",params..., more params); 

, который будет порождать новый процесс и запускать его в начале функции main () и возвращать новый PID.

Что беспокоит меня, так это ощущение, что решение fork / execve выполняет потенциально ненужную работу. Что если мой процесс использует тонны памяти? Копирует ли ядро ​​мои таблицы страниц и тому подобное. Я уверен, что это действительно не выделяет реальную память, если я не коснулся этого Кроме того, что произойдет, если у меня есть темы? Мне просто кажется, что это слишком грязно.

Почти все описание того, что делает fork, скажем, что оно просто копирует процесс, и новый процесс запускается после вызова fork(). Это действительно то, что происходит, но почему это происходит именно так и почему fork / execve - единственный способ порождать новые процессы, и каков наиболее общий способ Unix создать новый процесс из вашего текущего? Есть ли другой, более эффективный способ порождения процесса? ** Какой бы не требовалось копировать больше памяти.

Этот поток говорит о той же проблеме, но я нашел ее не вполне удовлетворительной:

Спасибо.

Ответы [ 14 ]

19 голосов
/ 09 марта 2014

Это связано с историческими причинами.Как объяснено в https://www.bell -labs.com / usr / dmr / www / hist.html , в очень ранних версиях Unix не было ни fork(), ни exec*(), и оболочка выполняла команды следующим образом:

  • Выполните необходимую инициализацию (открытие stdin / stdout ).
  • Прочитайте командную строку.
  • Откройте команду, загрузите загрузчиккод и перейти к нему.
  • Код начальной загрузки считывает открытую команду (перезаписывает память оболочки) и переходит к ней.
  • Как только команда завершится, она вызовет exit(),который затем работал, перезагружая оболочку (перезаписывая память команды) и переходя к ней, возвращаясь к шагу 1.

Оттуда fork() было простым добавлением (27 сборочных линий),повторное использование оставшегося кода.

На этом этапе разработки Unix выполнение команды стало:

  • Чтение командной строки.
  • fork() дочернийобработать и дождаться его (отправив ему сообщение).
  • Дочерний процесс загрузил команду (болеезаписывать память ребенка) и перейти к ней.
  • Как только команда закончится, она вызовет exit(), что теперь стало проще.Он просто очистил запись процесса и отказался от управления.

Первоначально fork() не делал копирование при записи.Поскольку это делало fork() дорогим, а fork() часто использовалось для порождения новых процессов (за которым часто следовал exec*()), появилась оптимизированная версия fork(): vfork(), которая разделяла память между родителем иребенок.В этих реализациях vfork() родительский объект будет приостановлен до тех пор, пока дочерний элемент exec*() 'ed или _exit()' ed, таким образом, не освободит память родителя.Позднее fork() был оптимизирован для копирования при записи, делая копии страниц памяти только тогда, когда они начали различаться между родителем и потомком.vfork() позже увидел возобновление интереса к портам для систем! MMU (например, если у вас есть маршрутизатор ADSL, он, вероятно, работает под управлением Linux на процессоре! MMU MIPS), который не может выполнять оптимизацию COW и, более того, не может поддерживать fork() 'ed обрабатывает эффективно.

Другой источник неэффективности в fork() заключается в том, что он изначально дублирует адресное пространство (и таблицы страниц) родителя, что может сделать запуск коротких программ из огромных программ относительно медленным,или может заставить ОС отказать в fork(), думая, что ей может не хватить памяти (чтобы обойти это, вы могли бы увеличить пространство подкачки или изменить настройки перегрузки памяти в вашей ОС).Как анекдот, Java 7 использует vfork()/posix_spawn(), чтобы избежать этих проблем.

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

10 голосов
/ 28 ноября 2011

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

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

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

Другие системные вызовы (execve) отвечают за загрузку нового исполняемого файла и т. Д.

Разделение их (а также предоставление системных вызовов pipe и dup2) дает большую гибкость.

И в современных системах fork реализован очень эффективно (через ленивое копирование в методах разбиения на страницы записи).Известно, что механизм fork делает процесс создания Unix довольно быстрым (например, быстрее, чем в Windows или в VAX / VMS, где системные вызовы создают процессы, более схожие с тем, что вы предлагаете).

Существует такжесистемный вызов vfork , который я не потрудился использовать.

И API posix_spawn намного сложнее, чем один fork или execve, как это иллюстрируетчто fork проще ...

5 голосов
/ 28 ноября 2011

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

2 голосов
/ 07 марта 2014

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

fork () создает новый процесс, дублируя вызывающий процесс.

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

Новый процесс, называемый дочерним, является точной копией вызывающего процесса (называемого родительским). За исключением:

  • У ребенка есть собственный уникальный идентификатор процесса, и этот идентификатор не совпадает идентификатор любой существующей группы процессов.
  • Идентификатор родительского процесса дочернего процесса совпадает с идентификатором родительского процесса.
  • Ребенок не наследует блокировки памяти своего родителя.
  • Использование ресурсов процесса и счетчики времени ЦП сбрасываются на ноль у ребенка.
  • Дочерний набор ожидающих сигналов изначально пуст.
  • Дочерний объект не наследует настройки семафора от своего родителя.
  • Дочерний объект не наследует блокировки записей от своего родителя.
  • Ребенок не наследует таймеры от своего родителя.
  • Дочерний объект не наследует невыполненные асинхронные операции ввода-вывода от своего родителя, и при этом он не наследует какие-либо контексты асинхронного ввода-вывода от своего родителя.

Вывод:

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

Источники:

http://www.quora.com/Linux-Kernel/After-a-fork-where-exactly-does-the-childs-execution-start http://learnlinuxconcepts.blogspot.in/2014/03/process-management.html

2 голосов
/ 07 марта 2014

Итак, как сказали другие, fork реализован очень быстро, поэтому это не проблема. Но почему не такая функция, как create_process()? Ответ: простота для гибкости. Все системные вызовы в Unix запрограммированы на выполнение только одного действия. Функция типа create_process будет делать две вещи: создавать процесс и загружать в него двоичный файл.

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

Пример с трубами:

  • Создание трубы
  • Форк дочерний, который наследует дескриптор трубы
  • Ребенок закрывает входную сторону
  • Родитель закрывает выходную сторону

Невозможно без fork()

Другим важным фактом является то, что весь Unix API имеет всего несколько функций. Каждый программист мог легко запомнить используемые функции. Но посмотрите на Windows API: более тысячи функций никто не помнит.

Итак, подведем итоги и скажем еще раз: простота для гибкости

2 голосов
/ 28 ноября 2011

Когда fork создает новый процесс путем копирования текущего процесса, он выполняет копирование при записи. Это означает, что память нового процесса используется совместно с родительским процессом, пока он не будет изменен. Когда память изменяется, она копируется, чтобы убедиться, что у каждого процесса есть собственная действительная копия памяти. При выполнении execve сразу после fork ing копия памяти отсутствует, поскольку новый процесс просто загружает новый исполняемый файл и, следовательно, новое пространство памяти.

Что касается вопроса, почему это делается, я точно не знаю, но, похоже, это часть Unix-пути - делать одну вещь хорошо. Вместо создания функции, которая создает новый процесс и загружает новый исполняемый файл, операция делится на две функции. Это дает разработчику максимальную гибкость. Хотя я еще не использовал ни одну из этих функций ...

2 голосов
/ 28 ноября 2011

Взгляните на spawn и друзей.

1 голос
/ 29 сентября 2016

Другие ответы хорошо объяснили, почему fork быстрее, чем кажется, и как он изначально появился.Но есть также веские основания для сохранения комбо fork + exec, и это гибкость, которую он предлагает.

Часто, когда порождает дочерний процесс, необходимо выполнить подготовительные шаги, прежде чемказнить ребенка.Например: вы можете создать пару каналов, используя pipe (читатель и записывающее устройство), затем перенаправить stdout или stderr дочернего процесса на записывающее устройство или использовать считыватель как stdin - илилюбой другой дескриптор файла, в этом отношении.Или вы можете установить переменные окружения (но только в дочернем).Или установите ограничения ресурсов с помощью setrlimit, чтобы ограничить количество ресурсов, которое может использовать дочерний элемент (без ограничения родительского элемента).Или измените пользователей с помощью setuid / seteuid (без изменения родительского элемента).И т. Д. И т. Д.

Конечно, вы можете сделать все это с помощью гипотетической функции create_process.Но это много вещей для покрытия!Почему бы не предложить гибкость при запуске fork, делать все, что вы хотите, чтобы настроить дочерний процесс, а затем запускать exec?

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

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

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

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

И наконец: как только вы примете этоfork и exec оба имеют свое собственное использование, независимо друг от друга, возникает вопрос - зачем создавать отдельную функцию, которая объединяет оба?Говорят, что философия Unix заключалась в том, чтобы ее инструменты «делали одно и делали это хорошо».Предоставляя вам fork и exec как отдельные строительные блоки - и делая каждый из них максимально быстрым и эффективным - они обеспечивают гораздо большую гибкость, чем одна функция create_process.

1 голос
/ 07 марта 2014

Итак, ваша главная задача: fork () приводит к ненужному копированию памяти.

Ответ: нет, нет потери памяти.Короче говоря, fork () родился, когда память была очень ограниченным ресурсом, поэтому никто бы даже не подумал о том, чтобы тратить ее таким образом.

Хотя у каждого процесса есть свое собственное адресное пространство, нет однозначногоотображение между страницей физической памяти и страницей виртуальной памяти процесса.Вместо этого одна страница физической памяти может быть сопоставлена ​​с несколькими виртуальными страницами (для получения дополнительной информации ищите TLB ЦП).

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

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

1 голос
/ 28 ноября 2011

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

...