Когда подстановка команд порождает больше подоболочек, чем одни и те же команды по отдельности?

Вчера мне подсказали, что использование подстановки команд в bash приводит к созданию ненужной подоболочки. Совет относился к этому варианту использования:

# Extra subshell spawned
foo=$(command; echo $?)

# No extra subshell
command
foo=$?

Насколько я могу судить, это кажется правильным для этого варианта использования. Однако быстрый поиск, пытающийся проверить это, приводит к множеству запутанных и противоречивых советов. Кажется, народная мудрость гласит, что ВСЕ использование подстановки команд порождает подоболочку. Например:

Подстановка команд расширяется до вывода команд. Эти команды выполняются в подоболочке, и их данные stdout — это то, до чего расширяется синтаксис подстановки. (источник)

Это кажется достаточно простым, если вы не продолжите копать, и в этом случае вы начнете находить ссылки на предположения, что это не так.

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

Это кажется разумным, но так ли это? Этот ответ на вопрос, связанный с подоболочкой, подсказал мне, что man bash имеет следующее замечание:

Каждая команда в конвейере выполняется как отдельный процесс (т. е. в подоболочке).

Это подводит меня к главному вопросу. Что именно приведет к тому, что подстановка команд порождает подоболочку, которая в любом случае не была бы создана для выполнения тех же команд по отдельности?

Пожалуйста, рассмотрите следующие случаи и объясните, какие из них несут накладные расходы на дополнительную подоболочку:

# Case #1
command1
var=$(command1)

# Case #2
command1 | command2
var=$(command1 | command2)

# Case #3
command1 | command 2 ; var=$?
var=$(command1 | command2 ; echo $?)

Выполняется ли для каждой из этих пар одинаковое количество подоболочек? Есть ли разница в реализациях POSIX и bash? Есть ли другие случаи, когда использование подстановки команд порождает подоболочку, тогда как выполнение одного и того же набора команд по отдельности не приводит к возникновению?


person Caleb    schedule 24.01.2014    source источник
comment
Этот вопрос содержит некоторую связанную информацию: подстановка команд и подстановка процессов   -  person Caleb    schedule 24.01.2014
comment
Я не думаю, что восприму вашу вторую цитату как какую-либо авторитетную информацию о том, как реализован bash. Однако я бы отметил, что подоболочка != process; подоболочка (в смысле новой области действия для переменных) не требуется для порождения нового процесса для его запуска. (Это третий пункт в принятом ответе на ваш связанный вопрос.)   -  person chepner    schedule 24.01.2014
comment
@chepner Если бы я считал это авторитетным, я бы, вероятно, не спрашивал здесь. Суть здесь в том, чтобы немного демистифицировать проблему в рецензируемой среде. Я понимаю, что такое подоболочка, вероятно, нужно будет уточнить в ответе, чтобы дать разумное объяснение того, когда используются ненужные ресурсы.   -  person Caleb    schedule 24.01.2014
comment
Комментарий ко второй цитате. Нет никаких встроенных функций, которые явно означают «подоболочку»; в лучшем случае вводит в заблуждение и, на мой взгляд, абсолютно ошибочно (из-за чего я не стал читать остальную часть статьи). ( ... ) явно создает подоболочку; команды в круглых скобках должны выполняться в дополнительной оболочке (это означает, что любые изменения, внесенные в переменные и т. д., не должны влиять на основную оболочку). Раньше это делалось путем разветвления и предоставления дочернему элементу выполнения содержимого сценария вложенной оболочки, в то время как родитель ожидает его завершения. Оболочка может избежать этого, если у нее достаточно хорошие возможности определения области видимости.   -  person Jonathan Leffler    schedule 24.01.2014
comment
@JonathanLeffler, если честно, если вы читаете обмен мнениями ниже, он отрекается, хотя и несколько тупым образом.   -  person kojiro    schedule 28.01.2014
comment
@kojiro: да, он это делает, но поскольку публикация оставляет двусмысленное сообщение, а не переписывается, чтобы недвусмысленно представить окончательное мнение или указывать на то, где окончательное мнение представлено недвусмысленно, оно оставляет публикацию как «сомнительную ценность» как источник информации.   -  person Jonathan Leffler    schedule 28.01.2014
comment
См. stackoverflow.com/a/41315367/717267.   -  person Eduardo Cuomo    schedule 24.12.2016


Ответы (2)


Обновите и предупредите:

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

Я существенно пересмотрел - и в основном выпотрошил - этот ответ после того, как @kojiro указал, что мои методы тестирования ошибочны (первоначально я использовал ps для поиска дочерних процессов, но это слишком медленно, чтобы всегда их обнаруживать) ; новый метод тестирования описан ниже.

Первоначально я утверждал, что не все подоболочки bash работают в своих собственных дочерних процессах, но это оказалось не так.

Как утверждает @kojiro в своем ответе, некоторые оболочки, кроме bash, иногда ДЕЙСТВИТЕЛЬНО избегают создания дочерних процессов для подоболочек, поэтому, вообще говоря, в мире оболочек один не следует предполагать, что подоболочка подразумевает дочерний процесс.

Что касается случаев OP в bash (предполагается, что command{n} экземпляра являются простыми командами):

# Case #1
command1         # NO subshell
var=$(command1)  # 1 subshell (command substitution)

# Case #2
command1 | command2         # 2 subshells (1 for each pipeline segment)
var=$(command1 | command2)  # 3 subshells: + 1 for command subst.

# Case #3
command1 | command2 ; var=$?         # 2 subshells (due to the pipeline)
var=$(command1 | command2 ; echo $?) # 3 subshells: + 1 for command subst.;
                                     #   note that the extra command doesn't add 
                                     #   one

Похоже, что использование подстановки команд ($(...)) всегда добавляет дополнительную подоболочку в bash, как и включение любой команды в (...).

Верю, но не уверен, что эти результаты верны; вот как я тестировал (bash 3.2.51 на OS X 10.9.1) - пожалуйста, сообщите мне, если этот подход ошибочен:

  • Убедился, что запущены только 2 интерактивные оболочки bash: одна для запуска команд, другая для мониторинга.
  • Во 2-й оболочке я отслеживал вызовы fork() в 1-й с помощью sudo dtruss -t fork -f -p {pidOfShell1} (-f необходим для "транзитивной" трассировки вызовов fork(), т.е. для включения тех, которые созданы самими подоболочками).
  • Использовались только встроенные : (no-op) в тестовых командах (чтобы не запутать картинку дополнительными fork() вызовами внешних исполняемых файлов); конкретно:

    • :
    • $(:)
    • : | :
    • $(: | :)
    • : | :; :
    • $(: | :; :)
  • Учитывались только те выходные строки dtruss, которые содержали ненулевой PID (поскольку каждый дочерний процесс также сообщает о вызове fork(), который его создал, но с PID 0).

  • Из полученного числа вычтено 1, так как запуск даже встроенной функции из интерактивной оболочки, по-видимому, требует как минимум 1 fork().
  • Наконец, предположим, что результирующий счетчик представляет собой количество созданных подоболочек.

Ниже приведено то, что я все еще считаю правильным из моего исходного сообщения: когда bash создает подоболочки.


bash создает подоболочки в следующих случаях:

  • for an expression surrounded by parentheses ( (...) )
    • except directly inside [[ ... ]], where parentheses are only used for logical grouping.
  • for every segment of a pipeline (|), including the first one
    • Note that every subshell involved is a clone of the original shell in terms of content (process-wise, subshells can be forked from other subshells (before commands are executed)).
      Thus, modifications of subshells in earlier pipeline segments do not affect later ones.
      (By design, commands in a pipeline are launched simultaneously - sequencing only happens through their connected stdin/stdout pipes.)
    • bash 4.2+ имеет параметр оболочки lastpipe (по умолчанию ВЫКЛ), из-за которого последний сегмент конвейера НЕ запускается в подоболочке.
  • для подстановки команд ($(...))

  • для замены процесса (<(...))

  • фоновое выполнение (&)

Объединение этих конструкций приведет к более чем одной подоболочке.

person mklement0    schedule 24.01.2014
comment
Как вы тестировали создание нового процесса? - person RedX; 24.01.2014
comment
Я не думаю, что ваши результаты верны, потому что ps недостаточно быстр, чтобы захватить некоторые из тех подоболочек, которые вы создали выше. Например, вы говорите, что выражение, заключенное в круглые скобки, не выполняется в дочернем процессе для простых команд, но пытаетесь постоянно выводить ps при выполнении for i in {0..999999}; do ( : ); done. Вы не увидите каждый новый процесс, но некоторые из них вы увидите, и количество PID, через которые проходит система, быстро увеличивается. - person kojiro; 25.01.2014
comment
@kojiro: Отличный улов, спасибо. Не то чтобы это уже имело значение, но как бы вы тестировали в отсутствие $BASHPID? Я обновлю свой ответ. - person mklement0; 25.01.2014
comment
@mklement0 Это сложно. Мои первые попытки были на Mavericks (в котором все еще есть Bash 3, вздох), и в итоге я попробовал длинные циклы с множеством крошечных подоболочек в них. В Linux с Bash 3 вы можете использовать /proc/self, но поскольку в OS X нет /proc, Bash 3 не имеет BASHPID, а ( sh -c 'echo $$PPID' ) нарушает некоторые инварианты вопроса, я не чувствую, что есть чистое решение. - person kojiro; 25.01.2014
comment
@kojiro: Спасибо; Я придумал что-то на основе sudo dtruss -t fork -f -p {pid} для OSX; не могли бы вы взглянуть на обновленный ответ и сказать мне, выглядит ли это правильно? - person mklement0; 26.01.2014

В Bash подоболочка всегда выполняется в новом пространстве процесса. Вы можете довольно просто проверить это в Bash 4, который имеет переменные окружения $BASHPID и $$:

  • $$ Заменяется идентификатором процесса оболочки. В подоболочке () он расширяется до идентификатора процесса текущей оболочки, а не подоболочки.
  • BASHPID Заменяется идентификатором текущего процесса bash. Это отличается от $$ при определенных обстоятельствах, таких как подоболочки, которые не требуют повторной инициализации bash.

на практике:

$ type echo
echo is a shell builtin
$ echo $$-$BASHPID
4671-4671
$ ( echo $$-$BASHPID )
4671-4929
$ echo $( echo $$-$BASHPID )
4671-4930
$ echo $$-$BASHPID | { read; echo $REPLY:$$-$BASHPID; }
4671-5086:4671-5087
$ var=$(echo $$-$BASHPID ); echo $var
4671-5006

Единственным случаем, когда оболочка может исключить дополнительную подоболочку, является когда вы подключаетесь к явной подоболочке:

$ echo $$-$BASHPID | ( read; echo $REPLY:$$-$BASHPID; )
4671-5118:4671-5119

Здесь подоболочка, подразумеваемая каналом, применяется явно, но не дублируется.

Это отличается от некоторых других оболочек, которые очень стараются избежать fork-ing. Поэтому, хотя я считаю, что аргумент, сделанный в js-shell-parse, вводит в заблуждение, это правда, что не все оболочки всегда fork для всех подоболочек.

person kojiro    schedule 25.01.2014