В вычислительной технике , особенно в контексте операционной системы Unix и ее аналогов , fork — это операция, посредством которой процесс создает свою копию. Это интерфейс, который требуется для соответствия стандартам POSIX и Single UNIX Specification . Обычно он реализуется как оболочка стандартной библиотеки C для fork, clone или других системных вызовов ядра . Fork — это основной метод создания процесса в операционных системах типа Unix .
В многозадачных операционных системах процессам (запущенным программам) нужен способ создания новых процессов, например, для запуска других программ. Fork и его варианты обычно являются единственным способом сделать это в Unix-подобных системах. Чтобы процесс начал выполнение другой программы, он сначала разветвляется, чтобы создать свою копию. Затем копия, называемая « дочерним процессом », вызывает системный вызов exec, чтобы наложить себя на другую программу: он прекращает выполнение своей прежней программы в пользу другой.
Операция fork создает отдельное адресное пространство для дочернего процесса. Дочерний процесс имеет точную копию всех сегментов памяти родительского процесса. В современных вариантах UNIX, которые следуют модели виртуальной памяти из SunOS -4.0, реализована семантика копирования при записи , и физическая память не обязательно должна быть фактически скопирована. Вместо этого страницы виртуальной памяти в обоих процессах могут ссылаться на одни и те же страницы физической памяти , пока один из них не запишет на такую страницу: затем она копируется. Эта оптимизация важна в общем случае, когда fork используется вместе с exec для выполнения новой программы: обычно дочерний процесс выполняет только небольшой набор действий, прежде чем он прекращает выполнение своей программы в пользу запускаемой программы, и ему требуется очень мало, если вообще требуется, структур данных его родителя .
Когда процесс вызывает fork, он считается родительским процессом , а вновь созданный процесс — его дочерним. После fork оба процесса не только запускают одну и ту же программу, но и возобновляют выполнение, как будто оба вызвали системный вызов. Затем они могут проверить возвращаемое значение вызова , чтобы определить свой статус, дочерний или родительский, и действовать соответствующим образом.
Одно из самых ранних упоминаний концепции ветвления появилось в работе Мелвина Конвея « Проект многопроцессорной системы» , опубликованной в 1962 году. [1] Работа Конвея побудила Л. Питера Дойча реализовать ветвление в системе разделения времени GENIE , откуда эта концепция была заимствована Кеном Томпсоном для ее самого раннего появления [2] в Research Unix . [3] [4] Позднее ветвление стало стандартным интерфейсом в POSIX . [5]
Дочерний процесс начинается с копии файловых дескрипторов своего родителя . [5] Для межпроцессного взаимодействия родительский процесс часто создает один или несколько каналов , а затем после разветвления процессов закрывает концы каналов, которые им не нужны. [6]
Vfork — это вариант fork с тем же соглашением о вызовах и почти такой же семантикой, но используемый только в ограниченных ситуациях. Он возник в версии 3BSD Unix, [7] [8] [9] первой Unix, поддерживающей виртуальную память. Он был стандартизирован POSIX, что позволило vfork иметь точно такое же поведение, как fork, но был отмечен как устаревший в издании 2004 года [10] и был заменен на posix_spawn () (который обычно реализуется через vfork) в последующих изданиях.
При вызове системного вызова vfork родительский процесс будет приостановлен до тех пор, пока дочерний процесс не завершит выполнение или не будет заменен новым исполняемым образом с помощью одного из системных вызовов семейства " exec ". Дочерний процесс заимствует настройку блока управления памятью у родительского процесса, и страницы памяти совместно используются родительским и дочерним процессами без копирования и, в частности, без семантики копирования при записи ; [10] следовательно, если дочерний процесс вносит изменения в любую из общих страниц, новая страница не будет создана, а измененные страницы будут видны и родительскому процессу. Поскольку копирование страниц абсолютно не задействовано (потребление дополнительной памяти), этот метод является оптимизацией по сравнению с простым fork в средах с полным копированием при использовании с exec. В POSIX использование vfork для любых целей, кроме как в качестве прелюдии к немедленному вызову функции из семейства exec (и нескольким другим выбранным операциям), приводит к неопределенному поведению . [10] Как и в случае с vfork, дочерний процесс заимствует структуры данных, а не копирует их. vfork по-прежнему быстрее, чем форк, использующий семантику копирования при записи.
System V не поддерживала этот вызов функции до появления System VR4 [ необходима ссылка ], поскольку вызываемое им совместное использование памяти подвержено ошибкам:
Vfork не копирует таблицы страниц, поэтому он быстрее, чем реализация fork System V. Но дочерний процесс выполняется в том же физическом адресном пространстве, что и родительский процесс (до exec или exit ), и, таким образом, может перезаписать данные и стек родителя. Опасная ситуация может возникнуть, если программист неправильно использует vfork , поэтому ответственность за вызов vfork лежит на программисте. Разница между подходом System V и подходом BSD философская: должно ли ядро скрывать особенности своей реализации от пользователей или оно должно предоставлять искушенным пользователям возможность воспользоваться реализацией для более эффективного выполнения логической функции?
— Морис Дж. Бах [11]
Аналогично, страница руководства Linux для vfork настоятельно не рекомендует его использовать: [7] [ неудачная проверка ] [ обсуждение ]
Довольно прискорбно, что Linux возродил этот призрак из прошлого. На странице руководства BSD говорится: "Этот системный вызов будет устранен, когда будут реализованы надлежащие механизмы совместного использования системы. Пользователи не должны зависеть от семантики совместного использования памяти vfork(), поскольку в этом случае он станет синонимом fork(2)".
Другие проблемы с vfork включают взаимоблокировки , которые могут возникнуть в многопоточных программах из-за взаимодействия с динамическим связыванием . [12] В качестве замены интерфейса vfork , POSIX представил семейство функций posix_spawn , которые объединяют действия fork и exec. Эти функции могут быть реализованы как библиотечные процедуры в терминах fork , как это сделано в Linux, [12] или в терминах vfork для лучшей производительности, как это сделано в Solaris, [12] [13] но спецификация POSIX отмечает, что они были «разработаны как операции ядра », особенно для операционных систем, работающих на ограниченном оборудовании и системах реального времени . [14]
Хотя реализация 4.4BSD избавилась от реализации vfork, в результате чего vfork стал вести себя так же, как fork, позднее она была восстановлена в операционной системе NetBSD из соображений производительности. [8]
Некоторые встраиваемые операционные системы, такие как uClinux, пропускают fork и реализуют только vfork, поскольку им необходимо работать на устройствах, где невозможно реализовать копирование при записи из-за отсутствия блока управления памятью.
Операционная система Plan 9 , созданная разработчиками Unix, включает в себя fork, а также вариант под названием «rfork», который позволяет осуществлять тонкое разделение ресурсов между родительскими и дочерними процессами, включая адресное пространство (за исключением сегмента стека , который уникален для каждого процесса), переменные окружения и пространство имен файловой системы; [15] это делает ее унифицированным интерфейсом для создания как процессов, так и потоков внутри них. [16] И FreeBSD [17], и IRIX переняли системный вызов rfork из Plan 9, последний переименовал его в «sproc». [18]
clone
системный вызов в ядре Linux , который создает дочерний процесс, который может делить части своего контекста выполнения с родительским. Подобно rfork FreeBSD и sproc IRIX, клон Linux был вдохновлен rfork Plan 9 и может использоваться для реализации потоков (хотя программисты приложений обычно используют интерфейс более высокого уровня, такой как pthreads , реализованный поверх clone). Функция «отдельных стеков» из Plan 9 и IRIX была опущена, поскольку (по словам Линуса Торвальдса ) она вызывает слишком много накладных расходов. [18]
В оригинальном проекте операционной системы VMS (1977) операция копирования с последующей мутацией содержимого нескольких определенных адресов для нового процесса, как при разветвлении, считалась рискованной. [ необходима цитата ] Ошибки в текущем состоянии процесса могут быть скопированы в дочерний процесс. Здесь используется метафора порождения процесса: каждый компонент структуры памяти нового процесса заново создается с нуля. Метафора порождения была позже принята в операционных системах Microsoft (1993).
Компонент совместимости POSIX VM/CMS (OpenExtensions) обеспечивает очень ограниченную реализацию fork, в которой родительский процесс приостанавливается, пока выполняется дочерний процесс, а дочерний и родительский процессы совместно используют одно и то же адресное пространство. [19] По сути, это vfork, обозначенный как fork . (Это применимо только к гостевой операционной системе CMS; другие гостевые операционные системы VM, такие как Linux, предоставляют стандартную функциональность fork.)
Следующий вариант программы "Hello, World!" демонстрирует механику системного вызова fork на языке программирования C. Программа разветвляется на два процесса, каждый из которых решает, какую функциональность он выполняет, основываясь на возвращаемом значении системного вызова fork. Шаблонный код , такой как включение заголовков, был опущен.
int main ( void ) { pid_t pid = fork (); if ( pid == -1 ) { perror ( "fork failed " ); exit ( EXIT_FAILURE ); } else if ( pid == 0 ) { printf ( "Привет от дочернего процесса! \n " ); _exit ( EXIT_SUCCESS ); } else { int status ; ( void ) waitpid ( pid , & status , 0 ); } return EXIT_SUCCESS ; }
Далее следует анализ этой программы.
pid_t pid = fork ();
Первый оператор в main вызывает системный вызов fork для разделения выполнения на два процесса. Возвращаемое значение fork записывается в переменную типа pid_t , которая является типом POSIX для идентификаторов процессов (PID).
если ( pid == -1 ) { perror ( "форк не удался" ); выход ( EXIT_FAILURE ); }
Минус один указывает на ошибку в fork : новый процесс не был создан, поэтому выводится сообщение об ошибке.
Если fork был успешным, то теперь есть два процесса, оба выполняют основную функцию с точки, куда fork вернулся. Чтобы заставить процессы выполнять разные задачи, программа должна разветвляться на возвращаемом значении fork , чтобы определить, выполняется ли он как дочерний процесс или как родительский процесс.
else if ( pid == 0 ) { printf ( "Привет от дочернего процесса! \n " ); _exit ( EXIT_SUCCESS ); }
В дочернем процессе возвращаемое значение отображается как ноль (что является недопустимым идентификатором процесса). Дочерний процесс печатает желаемое приветственное сообщение, затем завершает работу. (По техническим причинам здесь должна использоваться функция POSIX _exit вместо стандартной функции выхода C. )
иначе { int status ; ( void ) waitpid ( pid , & status , 0 ); }
Другой процесс, родитель, получает от fork идентификатор процесса потомка, который всегда является положительным числом. Родительский процесс передает этот идентификатор системному вызову waitpid, чтобы приостановить выполнение до тех пор, пока потомок не завершит работу. Когда это происходит, родитель возобновляет выполнение и завершает работу с помощью оператора return .