
Этот пост по сути является продолжениемпредыдущий вопросмой.
Из ответа на этот вопрос я понял, что не только не совсем понимаю всю концепцию «подоболочки», но и, в более общем плане, не понимаю взаимосвязи между fork
-ing и дочерними процессами.
Раньше я думал, что когда процесс X
выполняет a fork
, aновыйY
создается процесс , родитель которого — X
, но согласно ответу на этот вопрос,
[a] Подоболочка — это не совершенно новый процесс, а ответвление существующего процесса.
Подразумевается, что «разветвление» не является (или не приводит к) «совершенно новым процессом».
Сейчас я в полном замешательстве, на самом деле, слишком запутан, чтобы сформулировать связный вопрос, который бы напрямую развеял мое замешательство.
Однако я могу сформулировать вопрос, который может косвенно привести к просветлению.
Поскольку, согласно zshall(1)
, $ZDOTDIR/.zshenv
получает sourced всякий раз, когда запускается новый экземпляр zsh
, то любая команда, $ZDOTDIR/.zshenv
которая приводит к созданию "совершенно нового процесса [zsh]", приведет к бесконечному регрессу. С другой стороны, включение любой из следующих строк в файл $ZDOTDIR/.zshenv
делаетнетрезультат в бесконечной регрессии:
echo $(date; printenv; echo $$) > /dev/null #1
(date; printenv; echo $$) #2
Единственный найденный мной способ вызвать бесконечный регресс с помощью описанного выше механизма — это включить в файл строку, подобную следующей$ZDOTDIR/.zshenv
:
$SHELL -c 'date; printenv; echo $$' #3
У меня есть вопросы:
какая разница между командами
#1
, отмеченными#2
выше, и отмеченными#3
счетами из-за этой разницы в поведении?если оболочки, которые создаются в
#1
,#2
называются «подоболочками», то что из них похоже на ту, которая создается#3
?возможно ли рационализировать (а может быть, и обобщить) эмпирические/анекдотические выводы, описанные выше, в терминах «теории» (за неимением лучшего слова) процессов Unix?
Мотивация последнего вопроса – иметь возможность определитьдосрочно(т.е. не прибегая к экспериментам) какие команды привели бы к бесконечному регрессу, если бы они были включены в $ZDOTDIR/.zshenv
?
1 Конкретная последовательность команд date; printenv; echo $$
, которую я использовал в различных примерах выше, не так уж важна. Это команды, вывод которых был потенциально полезен для интерпретации результатов моих «экспериментов». (Я, однако, хотел, чтобы эти последовательности состояли из более чем одной команды, по причине, объясненнойздесь.)
решение1
Так как, согласно zshall(1), $ZDOTDIR/.zshenv становится источником всякий раз, когда запускается новый экземпляр zsh
Если вы сосредоточитесь на слове "начинается" здесь, у вас будет лучшее время вещей. Эффект заключается fork()
в создании другого процессакоторый начинается именно там, где уже находится текущий процесс. Это клонирование существующего процесса, с единственным отличием в возвращаемом значении fork
. В документации используется «starts» для обозначения входа в программу с самого начала.
Ваш пример №3 запускает $SHELL -c 'date; printenv; echo $$'
, запуская совершенно новый процесс с самого начала. Он пройдет через обычное поведение запуска. Вы можете проиллюстрировать это, например, заменив другую оболочку: run bash -c ' ... '
вместо zsh -c ' ... '
. В использовании здесь нет ничего особенного $SHELL
.
Примеры № 1 и № 2 запускают подоболочки. Оболочка fork
сама по себе и выполняет ваши команды внутри этого дочернего процесса, а затем продолжает свое собственное выполнение, когда дочерний процесс завершается.
Ответ на ваш вопрос №1 приведен выше: пример 3 запускает совершенно новую оболочку с самого начала, в то время как два других запускают подоболочки. Поведение при запуске включает загрузку .zshenv
.
Причина, по которой они специально описывают это поведение (что, вероятно, и приводит вас в замешательство), заключается в том, что этот файл (в отличие от некоторых других) загружается как в интерактивных, так и в неинтерактивных оболочках.
На ваш вопрос №2:
если оболочки, созданные в #1 и #2, называются «подоболочками», как называются те, что созданы в #3?
Если вам нужно имя, вы можете назвать его "дочерней оболочкой", но на самом деле это ничто. Это ничем не отличается от любого другого процесса, который вы запускаете из оболочки, будь то та же самая оболочка, другая оболочка или cat
.
На ваш вопрос №3:
возможно ли рационализировать (а может быть, и обобщить) эмпирические/анекдотические выводы, описанные выше, в терминах «теории» (за неимением лучшего слова) процессов Unix?
fork
создает новый процесс с новым PID, который начинает работать параллельно с того места, где остановился предыдущий.exec
заменяет текущий исполняемый код новой программой, загруженной откуда-то, работающей с самого начала. Когда вы порождаете новую программу, сначала вы fork
сами, а затем exec
эта программа в потомке. Это фундаментальная теория процессов, которая применяется везде, внутри и снаружи оболочек.
Подоболочки — это fork
s, и каждая не встроенная команда, которую вы запускаете, приводит как к a, так fork
и к exec
.
Обратите внимание, что это $$
расширяется до PID родительской оболочки.в любой POSIX-совместимой оболочке, поэтому вы можете не получить ожидаемый вывод в любом случае. Обратите внимание также, что zsh в любом случае агрессивно оптимизирует выполнение подоболочки и обычно exec
s является последней командой или вообще не порождает подоболочку, если все команды безопасны без нее.
Вот полезная команда для проверки вашей интуиции:
strace -e trace=process -f $SHELL -c ' ... '
Это выведет в стандартную ошибку все события, связанные с процессом (и никаких других) для команды, которую ...
вы запускаете в новой оболочке. Вы можете увидеть, что выполняется и не выполняется в новом процессе, и где exec
происходят s.
Другая потенциально полезная команда — pstree -h
, которая выведет и выделит дерево родительских процессов текущего процесса. Вы можете увидеть, на скольких уровнях вы находитесь в выводе.
решение2
Когда в руководстве говорится, что команды в .zshenv
"sourced", это означает, что они выполняются в оболочке, в которой они запущены. Они не вызывают вызов fork()
, поэтому они не порождают подоболочку. Ваш третий пример явно запускает подоболочку, вызывая вызов вызова fork()
, и таким образом бесконечно рекурсирует. Я считаю, что это должно (по крайней мере частично) ответить на ваш первый вопрос.
В командах 1 и 2 ничего не «создается», поэтому нечего и называть — эти команды выполняются в контексте исходной оболочки.
Обобщение — это разница между «вызовом» процедуры или программы оболочки и «источником» процедуры или программы оболочки — последний обычно применим только к командам/скриптам оболочки, а не к внешним программам. «Источник» сценария оболочки обычно выполняется через
. <scriptname>
в отличие от./<scriptname>
или/full/path/to/script
— обратите внимание на последовательность «точка-пробел» в начале директивы источника. Источник также может быть вызван с помощьюsource <scriptname>
, при этомsource
команда является внутренней для оболочки.
решение3
fork
, предполагая, что все идет хорошо, возвращает дважды. Один возврат происходит в родительском процессе (который имеет исходный идентификатор процесса), а другой — в новом дочернем процессе (другой идентификатор процесса, но в остальном имеющий много общего с родительским процессом). В этот момент дочерний процесс может exec(3)
что-то сделать, что приведет к загрузке в этот процесс «нового» двоичного файла, хотя дочернему процессу это делать не нужно, и он может запустить другой код, уже загруженный через родительский процесс (например, функции zsh). Следовательно, a fork
может привести или не привести к «совершенно новому» процессу, если «совершенно новый» подразумевает что-то, загруженное через exec(3)
системный вызов.
Угадать, какие команды вызывают бесконечную регрессию заранее, сложно; помимо случая «форк-вызов-форка» (также известного как «форкбомба»), есть еще один простой способ — использовать наивную функцию-обертку вокруг некоторой команды.
function ssh() {
ssh -o UseRoaming=no "$@"
}
который вместо этого, вероятно, следует записать как
function ssh() {
=ssh -o UseRoaming=no "$@"
}
или command ssh ...
чтобы избежать бесконечных вызовов функции, ssh
вызывающей ssh
функцию, вызывающую ... Это никоим образом не затрагивает fork
, поскольку вызовы функций являются внутренними для процесса ZSH, но будут без проблем происходить до бесконечности, пока этот единственный процесс ZSH не упрется в какой-то предел.
strace
, как всегда, удобно для точного отображения того, какие системные вызовы задействованы для любой команды (в частности, здесь fork
и, возможно, в некоторых exec
вызовах); оболочки могут быть отлажены с помощью -x
или аналогичного, который показывает, что оболочка делает внутри (например, вызовы функций). Для более подробного чтения, Стивенс в "Расширенном программировании в среде Unix" имеет несколько глав, связанных с созданием и обработкой новых процессов.