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()
вызова на его stdout
fd 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()
если это возможно.
Смотрите также