Подтвердить что ты не робот

Почему fork() работает так, как он делает

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

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

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

который вызовет новый процесс и запустит его в начале main() и вернет новый PID.

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

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

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

Спасибо.

4b9b3361

Ответ 1

Это связано с историческими причинами. Как объяснялось в 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() делает создание нескольких экземпляров одного и того же процесса очень эффективным: например, веб-сервер может иметь несколько одинаковых процессов, обслуживающих разные клиенты. Другие платформы поддерживают нитки, потому что стоимость нереста другого процесса намного больше, чем стоимость дублирования текущего процесса, который может быть немного больше, чем для нереста нового потока. К сожалению, поскольку общие потоки - это магнит для ошибок.

Ответ 2

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

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

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

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

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

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

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

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

Ответ 3

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

Ответ 4

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

Ответ 5

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

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

Ответ 6

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

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

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

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

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

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

Итак, чтобы подвести итог и сказать это снова: простота для гибкости

Ответ 7

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

Ответ 8

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

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

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

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

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

Ответ 9

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

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

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

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

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

Заключение:

Основная цель 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

Ответ 10

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

Ответ 11

Основной причиной использования fork является скорость выполнения.

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

Также в большинстве случаев программа будет ".so" или ".dll", поэтому исполняемые инструкции не будут скопированы, только копия будет скопирована в стек и куча памяти.

Ответ 12

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

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

Если вам нужно читать или писать из файла, но не можете ждать, вы можете просто использовать fork, и ваш клон выполняет ввод-вывод, пока вы продолжаете выполнять свою первоначальную задачу. В Windows вы можете рассматривать нерестовые потоки или использовать перекрывающиеся ввода-вывода во многих ситуациях, когда простая простая вилка будет работать в Unix. В частности, процессы не имеют таких же проблем с масштабируемостью, как потоки. Это особенно актуально для 32-битных систем. Простое форсирование гораздо удобнее, чем иметь дело с сложностями перекрытия ввода-вывода. В то время как процессы имеют собственное пространство памяти, потоки живут в одном и том же состоянии, и, следовательно, существует ограничение на количество потоков, которые вы должны учитывать в 32-битном процессе. Создание 32-битного серверного приложения с вилкой очень просто, а создание 32-битного серверного приложения с потоками может стать кошмаром. Итак, если вы программируете на 32-битной Windows, вам придется прибегать к другим решениям, таким как перекрытие ввода-вывода, которое является PITA для работы.

Поскольку процессы не используют глобальные ресурсы, такие как потоки (например, глобальная блокировка в malloc), это гораздо более масштабируемо. Хотя потоки часто блокируют друг друга, процессы выполняются независимо.

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

Если вы имеете дело с интерпретируемыми языками, где обычно есть блокировка глобального интерпретатора (Python, Ruby, PHP...), ОС, которая дает вам возможность fork, незаменима. В противном случае ваша способность использовать несколько процессоров намного ограничена.

Другое дело, что здесь есть security isse. Процессы не разделяют пространство памяти и не могут испортить внутренние детали друг друга. Это приводит к повышению стабильности. Если у вас есть сервер, который использует потоки, авария в одном потоке приведет к удалению всего серверного приложения. При развороте аварии только снимут раздвоенный клон. Это также упрощает обработку ошибок. Часто бывает достаточно, чтобы ваш раздвоенный клон прерывался, так как он не имеет никакого значения для исходного приложения.

Существует также проблема с безопасностью. Если раздвоенному процессу вводят вредоносный код, он не может повлиять на родителя. Современные веб-браузеры используют это, например, для защиты одной вкладки от другой. Все это гораздо удобнее для программирования, если у вас есть системный вызов fork.

Ответ 13

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

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

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

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

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

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

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

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