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

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

Вчера мне было предложено, что использование подстановки команд в 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? Существуют ли другие случаи, когда использование подстановки команд создавало бы подоболочку, в которой выполнение одного и того же набора команд в отдельности не было бы?

4b9b3361

Ответ 1

Обновление и оговорка:

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

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

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

Как утверждает @kojiro в своем ответе, некоторые оболочки - кроме bash - DO иногда избегают создания дочерних процессов для подоболочек, поэтому, вообще говоря, в мире оболочек не следует предполагать, что подоболочка подразумевает, что ребенок процесс.

Что касается случаев 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 создает подоболочки в следующих ситуациях:

  • для выражения, окруженного круглыми скобками ((...))
    • кроме непосредственно внутри [[ ... ]], где круглые скобки используются только для логической группировки.
  • для каждого сегмента трубопровода (|), включая первый
    • Обратите внимание, что каждая вовлеченная подоболочка является клоном исходной оболочки с точки зрения содержимого (по сути, подоболочки могут быть выделены из других подоболочек (до выполнения команд)).
      Таким образом, модификации подоболочек в предыдущих сегментах трубопровода не влияют на более поздние. (По дизайну команды в конвейере запускаются одновременно - последовательность выполняется только через их подключенные трубы stdin/stdout.)
    • bash 4.2+ имеет параметр оболочки lastpipe (по умолчанию отключен), что приводит к тому, что последний сегмент сегмента НЕ запускается в подоболочке.
  • для подстановки команд ($(...))

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

    • обычно создает 2 подоболочки; в случае простой команды @konsolebox придумал технику, чтобы создать только 1: добавьте простую команду с помощью exec (<(exec ...)).
  • фоновое исполнение (&)

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

Ответ 2

В 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 для всех подоболочек.