Почему оболочки вызывают fork ()?

32

Когда процесс запускается из оболочки, почему оболочка разветвляется перед выполнением процесса?

Например, когда пользователь вводит данные grep blabla foo, почему оболочка не может просто вызвать exec()grep без дочерней оболочки?

Кроме того, когда оболочка разветвляется в эмуляторе терминала GUI, запускается ли другой эмулятор терминала? (например, pts/13запуск pts/14)

user3122885
источник

Ответы:

34

Когда вы вызываете execсемейный метод, он не создает новый процесс, а execзаменяет текущую память процесса, набор инструкций и т. Д. Процессом, который вы хотите запустить.

В качестве примера, вы хотите запустить с grepпомощью EXEC. bashэто процесс (который имеет отдельную память, адресное пространство). Теперь при вызове exec(grep)exec заменит данные памяти текущего процесса, адресного пространства, набора инструкций и т grep's. Д. Это означает, что bashпроцесс больше не будет существовать. В результате вы не можете вернуться к терминалу после выполнения grepкоманды. Вот почему методы семейства exec никогда не возвращаются. Вы не можете выполнить любой код после exec; это недоступно.

Шантану
источник
Почти нормально --- Я заменил Терминал на bash. ;-)
Rmano
2
Кстати, вы можете сказать bash выполнить grep без предварительной разветвления, используя команду exec grep blabla foo. Конечно, в данном конкретном случае это будет не очень полезно (поскольку окно терминала будет закрываться сразу после завершения работы grep), но иногда это может быть удобно (например, если вы запускаете другую оболочку, возможно, через ssh) / sudo / screen, и не намерены возвращаться к исходному, или если процесс оболочки, на котором вы запускаете этот процесс, является вспомогательной оболочкой, которая в любом случае никогда не должна выполнять более одной команды).
Илмари Каронен,
7
Набор инструкций имеет очень специфическое значение. И это не тот смысл, в котором вы его используете.
Андрей Савиных,
@IlmariKaronen Это было бы полезно в сценариях-обёртках, где вы хотите подготовить аргументы и среду для команды. И вы упомянули случай, когда bash никогда не должен запускать более одной команды, то есть фактически, bash -c 'grep foo bar'и при вызове exec существует форма оптимизации, которую bash выполняет автоматически
Сергей Колодяжный,
3

По словам pts, проверьте сами: в оболочке, запустить

echo $$ 

чтобы узнать ваш идентификатор процесса (PID), я, например,

echo $$
29296

Затем запустите, например, sleep 60а затем, в другом терминале

(0)samsung-romano:~% ps -edao pid,ppid,tty,command | grep 29296 | grep -v grep
29296  2343 pts/11   zsh
29499 29296 pts/11   sleep 60

Так что нет, в целом у вас есть тот же tty, связанный с процессом. (Обратите внимание, что это ваша, sleepпотому что она имеет вашу оболочку как родитель)

Rmano
источник
2

TL; DR : потому что это оптимальный метод для создания новых процессов и сохранения контроля в интерактивной оболочке

fork () необходим для процессов и труб

Чтобы ответить на конкретную часть этого вопроса, если grep blabla fooбы он вызывался через exec()напрямую в parent, родительский узел решил бы существовать, и его PID со всеми ресурсами был бы передан grep blabla foo.

Впрочем, давайте в общем поговорим об exec()а fork(). Основная причина такого поведения заключается в том, что fork()/exec()это стандартный метод создания нового процесса в Unix / Linux, а это не специфическая для bash вещь; этот метод существует с самого начала и на него влияет тот же метод из уже существующих операционных систем того времени. Чтобы несколько перефразировать ответ Златовласки по связанному вопросу, fork()создать новый процесс проще, поскольку ядру не нужно много работы для распределения ресурсов и множества свойств (таких как дескрипторы файлов, окружение и т. Д.) - все могут наследоваться от родительского процесса (в данном случае от bash).

Во-вторых, что касается интерактивных оболочек, вы не можете запустить внешнюю команду без разветвления. Чтобы запустить исполняемый файл, который живет на диске (например, /bin/df -h), вы должны вызвать одну из exec()функций семейства, например execve(), которая заменит родительский процесс новым процессом, перехватит его PID и существующие файловые дескрипторы и т. Д. Для интерактивной оболочки вы хотите, чтобы элемент управления вернулся к пользователю и позволил родительской интерактивной оболочке продолжить работу. Таким образом, наилучшим способом является создание подпроцесса через fork(), и позволить этому процессу быть принятым через execve(). Таким образом, интерактивная оболочка PID 1156 будет порождать дочерний процесс с помощью fork()PID 1157, а затем вызывать execve("/bin/df",["df","-h"],&environment), что приводит к /bin/df -hзапуску с PID 1157. Теперь оболочке остается только дождаться выхода процесса и вернуть ему управление.

В случае, когда вам нужно создать канал между двумя или более командами, скажем df | grep, вам нужен способ создать два файловых дескриптора (это конец чтения и записи канала, которые приходят из pipe()syscall), а затем каким-то образом позволить двум новым процессам наследовать их. Это делается для разветвления нового процесса и затем путем копирования конца записи канала с помощью dup2()вызова на его stdoutfd 1 (так что если конец записи равен fd 4, мы делаем dup2(4,1)). Когда происходит exec()порождение df, дочерний процесс ничего не думает о нем stdoutи пишет в него, не зная (если он не проверяет активно), что его вывод на самом деле идет по конвейеру. Тот же процесс происходит grep, за исключением того fork(), что мы берем конец чтения канала с помощью fd 3 и dup(3,0)перед порождением grepс помощьюexec(), Все это время родительский процесс все еще там, ожидая восстановления контроля после завершения конвейера.

В случае встроенных команд, как правило, оболочки нет fork(), за исключением sourceкоманды. Подоболочки требуют fork().

Одним словом, это необходимый и полезный механизм.

Недостатки разветвления и оптимизации

Теперь это отличается для неинтерактивных оболочек , таких как bash -c '<simple command>'. Несмотря на то, fork()/exec()что это оптимальный метод, при котором вам нужно обрабатывать много команд, это пустая трата ресурсов, когда у вас есть только одна команда. Цитировать Стефана Шазеля из этого поста :

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

Следовательно, многие оболочки (не только bash) используют, exec()чтобы разрешить эту bash -c ''задачу одной простой командой. И именно по причинам, указанным выше, минимизация конвейеров в сценариях оболочки лучше. Часто вы можете увидеть, как новички делают что-то вроде этого:

cat /etc/passwd | cut -d ':' -f 6 | grep '/home'

Конечно, это будет fork()3 процесса. Это простой пример, но рассмотрим большой файл в диапазоне гигабайт. Было бы гораздо эффективнее с одним процессом:

awk -F':' '$6~"/home"{print $6}' /etc/passwd

Потеря ресурсов на самом деле может быть формой атаки типа «отказ в обслуживании», и, в частности, бомбы- форки создаются с помощью функций оболочки, которые вызывают себя в конвейере, который разветвляется на несколько копий. В настоящее время это смягчается путем ограничения максимального числа процессов в cgroups на systemd , которое Ubuntu также использует начиная с версии 15.04.

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

Смотрите также

Сергей Колодяжный
источник
1

Для каждой команды (пример: grep), которую вы вводите в приглашении bash, вы фактически намереваетесь запустить новый процесс и затем вернуться к приглашению bash после выполнения.

Если процесс оболочки (bash) вызывает exec () для запуска grep, процесс оболочки будет заменен на grep. Grep будет работать нормально, но после выполнения элемент управления не сможет вернуться в оболочку, поскольку процесс bash уже заменен.

По этой причине bash вызывает fork (), который не заменяет текущий процесс.

FlowRaja
источник