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

Время отходов execv() и fork()

В настоящее время я изучаю fork() и execv(), и у меня возник вопрос относительно эффективности комбинации.

Мне был показан следующий стандартный код:

pid = fork();
if(pid < 0){
    //handle fork error
}
else if (pid == 0){
    execv("son_prog", argv_son);
//do father code

Я знаю, что fork() клонирует весь процесс (копирование всей кучи и т.д.) и что execv() заменяет текущее адресное пространство на то, что в новой программе. Имея это в виду, не делает ли это очень неэффективным использование этой комбинации? Мы копируем все адресное пространство процесса и сразу же перезаписываем его.

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

4b9b3361

Ответ 1

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

Вам нужно как-то создать новый процесс. Для пользовательской программы очень мало возможностей для этого. POSIX имел vfork() alognside fork(), а некоторые системы могут иметь свои собственные механизмы, такие как Linux clone(), но с 2008 года POSIX указывает только fork() и posix_spawn(). Трасса fork + exec более традиционна, хорошо понятна и имеет несколько недостатков (см. Ниже). Семейство posix_spawn предназначено для использования в контексте, представляющем трудности для fork(); вы можете найти подробности в разделе "Обоснование" его спецификации.

Эта выдержка из справочной страницы Linux для vfork() может быть освещена:

В Linux fork (2) реализуется с использованием страниц копирования на запись, поэтому единственным штрафом, налагаемым fork (2), является время и память, необходимые для дублирования таблиц страниц родителей, и создать уникальную структуру задач для дочернего. Однако в старые добрые времена fork (2) потребовалось бы сделать полную копию пространства данных вызывающих абонентов, часто ненужно, так как обычно сразу после этого выполняется exec (3). Таким образом, для большей эффективности BSD ввела системный вызов vfork(), который не полностью скопировал адресное пространство родительского процесса, но заимствовал память родителей и поток управления до вызова execve (2) или произошел выход. Родительский процесс был приостановлен, пока ребенок использовал свои ресурсы. Использование vfork() было сложным: например, не изменять данные в родительском процессе зависело от того, какие переменные хранятся в регистре.

(добавлен акцент)

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

Ответ 2

В другом ответе говорится:

Однако в плохие старые дни вилка (2) потребовала бы сделать полную копию пространства данных вызывающих абонентов, часто ненужно, так как обычно сразу после этого выполняется exec (3).

Очевидно, что один человек, плохой старый день, намного моложе других, помните.

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

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

Верно, что был период времени, когда fork + exec был неудобным: когда были MMU, которые обеспечивали сопоставление между логическим и физическим адресным пространством, но ошибки страницы не сохраняли достаточную информацию, которая копирует-на-запись и число других схем виртуальной памяти/спроса-пейджинга были осуществимы.

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

Ответ 3

Не больше. Там что-то называется COW (Copy On Write), только когда один из двух процессов (родительский/дочерний) пытается записать в общие данные, он копируется.

В прошлом:
Системный вызов fork() скопировал адресное пространство вызывающего процесса (родителя) для создания нового процесса (дочернего). Копирование родительского адресного пространства в дочерний объект было самой дорогой частью операции fork().

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

По этой причине более поздние версии Unix использовали аппаратное обеспечение виртуальной памяти, чтобы позволить родительскому и дочернему пользователям совместно использовать память, отображаемую в их соответствующие адресные пространства, до тех пор, пока один из процессов фактически не изменит ее. Этот метод известен как copy-on-write. Для этого в fork() ядро ​​скопирует сопоставления адресного пространства из родительского элемента в дочерний элемент вместо содержимого отображаемых страниц и в то же время пометит теперь общие страницы только для чтения. Когда один из двух процессов пытается записать на одну из этих разделяемых страниц, процесс выполняет ошибку страницы. На этом этапе ядро ​​Unix понимает, что эта страница действительно была "виртуальной" или "копируемой на запись", и поэтому она создает новую, приватную, доступную для записи копию страницы для процесса сбоя. Таким образом, содержимое отдельных страниц на самом деле не копируется до тех пор, пока они не будут записаны. Эта оптимизация делает fork(), а затем exec() у ребенка намного дешевле: ребенку, вероятно, нужно будет только скопировать одну страницу (текущую страницу своего стека), прежде чем она назовет exec().

Ответ 4

Оказывается, все эти ошибки страницы COW совсем не дешевы, когда процесс имеет несколько гигабайт записываемой ОЗУ. Они все будут виноваты, даже если ребенок уже давно вызвал exec(). Поскольку дочерний элемент fork() больше не может выделять память даже для однопоточного случая (вы можете поблагодарить яблоко за это), организовать вместо этого вызов vfork()/exec() теперь вряд ли будет труднее.

Реальное преимущество для модели vfork()/exec() заключается в том, что вы можете установить дочерний элемент с произвольным текущим каталогом, произвольными переменными среды и произвольными дескрипторами fs (а не только stdin/stdout/stderr), произвольным сигналом маска и некоторая произвольная разделяемая память (с использованием системных вызовов с общей памятью) без использования API-интерфейсов с двумя аргументами CreateProcess(), который получает несколько аргументов каждые несколько лет.

Оказалось, что "oops я пропустил ручки, открываемые другим потоком", из первых попыток потоковой обработки было зафиксировано в пользовательском пространстве без блокировки по всему процессу благодаря /proc. То же самое не было бы в гигантской модели CreateProcess() без новой версии ОС, и убедить всех назвать новый API.

Итак, у вас это есть. Авария дизайна оказалась намного лучше, чем прямо разработанное решение.

Ответ 5

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