Вопрос или проблема
Судя по тому, что я читал, выполнение команды в скобках должно происходить в подпроцессе, аналогично выполнению скрипта. Если это правда, как он видит переменную x, если x не экспортирована?
x=1
Выполнение (echo $x)
в командной строке возвращает 1
Выполнение echo $x
в скрипте возвращает ничего, как и ожидалось
Подпроцесс в начале представляет собой практически идентичную копию оригинального процесса оболочки. Внутри оболочка вызывает системный вызов fork
1, который создает новый процесс, код и память которого являются копиями2. Когда создается подпроцесс, различий между ним и его родителем очень немного. В частности, у них одинаковые переменные. Даже специальная переменная $$
сохраняет одно и то же значение в подпроцессах: это идентификатор процесса оригинальной оболочки. Аналогично $PPID
— это PID родителя оригинальной оболочки.
Некоторые оболочки изменяют некоторые переменные в подпроцессе. Bash ≥4.0 устанавливает BASHPID
на PID процесса оболочки, который изменяется в подпроцессах. Bash, zsh и mksh обеспечивают, чтобы $RANDOM
выдавал разные значения у родителя и в подпроцессе. Но кроме встроенных специальных случаев, таких как эти, все переменные имеют одно и то же значение в подпроцессе, как в оригинальной оболочке, тот же статус экспорта, тот же статус только для чтения и т.д. Все определения функций, определения псевдонимов, параметры оболочки и другие настройки также наследуются.
Подпроцесс, созданный (…)
, имеет те же дескрипторы файлов, что и его создатель. Некоторые другие способы создания подпроцессов изменяют некоторые дескрипторы файлов перед выполнением пользовательского кода; например, левая сторона канала выполняется в подпроцессе3 с подключенным стандартным выводом к каналу. Подпроцесс также начинает с той же текущей директории, с той же маской сигналов и т.д. Одним из немногих исключений является то, что подпроцессы не наследуют пользовательские ловушки: игнорируемые сигналы (trap '' SIGNAL
) остаются игнорируемыми в подпроцессе, но другие ловушки (trap CODE
SIGNAL) сбрасываются на действие по умолчанию4.
Таким образом, подпроцесс отличается от выполнения скрипта. Скрипт — это отдельная программа. Эта отдельная программа может случайно быть также скриптом, который выполняется тем же интерпретатором, что и родитель, но это совпадение не дает отдельной программе никакой специальной видимости на внутренние данные родителя. Неэкспортированные переменные — это внутренние данные, поэтому, когда интерпретатор для дочернего скрипта оболочки выполняется, он не видит эти переменные. Экспортированные переменные, т.е. переменные окружения, передаются выполняемым программам.
Таким образом:
x=1
(echo $x)
выводит 1
, потому что подпроцесс является репликацией оболочки, которая его создала.
x=1
sh -c 'echo $x'
случайно запускает оболочку как дочерний процесс оболочки, но x
на второй строке не имеет большей связи с x
на второй строке, чем в
x=1
perl -le 'print $x'
или
x=1
python -c 'print x'
1 Если только оболочка не оптимизирует создание подпроцессов, но эмулирует создание подпроцессов настолько, насколько это необходимо для сохранения поведения кода, который она выполняет. Ksh93 оптимизирует много, другие оболочки в основном нет.
2 Семантически это копии. С точки зрения реализации идет много дележа.
3 Для правой стороны это зависит от оболочки.
4 Если вы это протестируете, имейте в виду, что вещи типа $(trap)
могут сообщать ловушки оригинальной оболочки. Также обратите внимание, что у многих оболочек есть ошибки в крайних случаях, связанных с ловушками. Например, ninjalj отмечает, что с bash 4.3, bash -x -c 'trap "echo ERR at \$BASH_SUBSHELL \$BASHPID" ERR; set -E; false; echo one subshell; (false); echo two subshells; ( (false) )'
запускает ловушку ERR
из вложенного подпроцесса в случае “двух подпроцессов”, но не ловушку ERR
из промежуточного подпроцесса — параметр set -E
должен распространять ловушку ERR
на все подпроцессы, но промежуточный подпроцесс оптимизирован и поэтому не выполняет свою ловушку ERR
.
Очевидно, да, как говорит вся документация, команда в скобках выполняется в подпроцессе.
Подпроцесс наследует копию всех переменных родителя. Различие в том, что любые изменения, которые вы делаете в подпроцессе, не будут произведены и в родителе.
Страница man для ksh делает это немного яснее, чем для bash:
man ksh
:
Команда в скобках выполняется в подпроцессе, не удаляя неэкспортированные переменные.
man bash
:
(
list)
список выполняется в окружении подпроцесса (см. COMMAND
EXECUTION ENVIRONMENT ниже). Присвоения переменных и
встроенные команды, которые влияют на окружение оболочки, не
остаются в действии после завершения команды.
COMMAND EXECUTION ENVIRONMENT
Оболочка имеет окружение выполнения, которое состоит из следующего:
[…] параметры оболочки, которые устанавливаются с помощью
присвоения переменных […].
Замещение команд, команды, сгруппированные в скобках, и асинхронные команды
вызываются в окружении подпроцесса, которое является дубликатом окружения оболочки, […]
Цена подпроцесса
Другой способ рассмотреть этот вопрос — посмотреть на стоимость выполнения команд в скобках. Если команды выполняются в подпроцессе, должна быть некоторая стоимость в процессорных циклах и времени. Например, две функции, которые идентичны, кроме фигурных скобок ({
) и скобок ((
), для инкапсуляции списка команд:
# выполняем команды в текущем контексте
foo() {
for i in $( seq $1 )
do
{
let 'j = 2*i - i'
echo "итерация $j (уровень: $BASH_SUBSHELL)"
}
done
}
# создаем подпроцесс для каждой итерации цикла
bar() {
for i in $( seq $1 )
do
(
let 'j = 2*i - i'
echo "итерация $j (уровень: $BASH_SUBSHELL)"
)
done
}
time foo 1000
# реальное 0m0.013s
time bar 1000
# реальное 0m0.596s
Поскольку было выполнено 1000 итераций каждого цикла, стоимость создания каждого подпроцесса составила ~ 0.6 мс.
Я обнаружил, что стоимость схожа, независимо от того, выполняются ли простые или более сложные команды в подпроцессе: это фиксированная стоимость создания подпроцесса, независимо от содержимого. Это было выполнено в Bash 5.2.21.
Ответ или решение
Использование скобок для выполнения команд в подсистеме: разбор
При использовании скобок заключенные в них команды выполняются в отдельной подсистеме (subshell) в Unix-подобных системах. Это поведение может вызывать вопросы относительно области видимости переменных, особенно если переменные не экспортированы. Рассмотрим подробнее, как это работает.
Принципы работы подсистем
Подсистема создается при помощи системного вызова fork
, который создает новый процесс, копируя память родительского процесса — текущей оболочки. Это значит, что новая подсистема начинается с идентичной копии всех переменных среды родительского процесса, включая неэкспортированные переменные.
Важным моментом является то, что изменения, выполненные в переменных в подсистеме, не влияют на переменные родительского процесса. Таким образом, если вы выполните:
x=1
(echo $x)
На экране будет отображено 1
, поскольку подсистема унаследовала значение переменной x
из родительской оболочки. Однако, если вы попытаетесь обратиться к переменной x
в скрипте или в другом процессе, вы не получите ожидаемого результата, если переменная не была экспортирована, так как скрипты и внешние команды не видят неэкспортированные переменные.
Описание взаимодействия с переменными
Как упоминалось выше, все переменные, включая неэкспортированные, доступны в рамках подсистемы. Это подтверждается и документированием, как в man ksh
, который утверждает, что "команда в скобках выполняется в подсистеме, не удаляя неэкспортированные переменные". Однако, при выполнении команд, запускаемых с помощью программы (например, в случае с sh -c 'echo $x'
), неэкспортированные переменные не будут доступны, так как новый процесс не унаследует их:
x=1
sh -c 'echo $x'
В результате здесь вы не увидите 1
, так как переменная x
не была экспортирована в окружение дочернего процесса.
Разница между подсистемами и скриптами
Важно отметить, что выполнение скрипта — это отдельная операция, которая запускает новую программу (интерпретатор оболочки), и эта программа не получает доступ к неэкспортированным переменным. Подсистема, с другой стороны, лишь создает временную копию текущей оболочки, где сохраняется доступ ко всем переменным до завершения её работы.
Затраты на создание подсистемы
Создание подсистемы, как ожидается, может привести к некоторым накладным расходам, особенно если команды выполняются в циклах. Для примера, если сравнить выполнение с использованием фигурных скобок и скобок:
foo() {
for i in $( seq $1 )
do
{
let 'j = 2*i - i'
echo "iteration $j (level: $BASH_SUBSHELL)"
}
done
}
bar() {
for i in $( seq $1 )
do
(
let 'j = 2*i - i'
echo "iteration $j (level: $BASH_SUBSHELL)"
)
done
}
Результаты тестирования показывают, что использование подсистемы (это функция bar
) требует значительно больше времени по сравнению с функцией foo
, которая работает в текущем контексте, что подтверждает накладные расходы на создание новой подсистемы.
Заключение
Таким образом, конструкции с использованием скобок действительно создают подсистемы, которые унаследуют все переменные от родительского процесса, включая неэкспортированные. Это поведение делает подсистемы мощным инструментом для выполнения определенных задач в оболочке, при этом важно понимать, как работает передача переменных между процессами. Выбор между использованием скобок и запуском внешних скриптов зависит от ваших нужд и необходимых переменных.