Выход замещения процесса вышел из строя

16

echo one; echo two > >(cat); echo three; 

Команда дает неожиданный вывод.

Я прочитал это: Как замена процесса реализована в Bash? и много других статей о замене процесса в Интернете, но не понимаю, почему он так себя ведет.

Ожидаемый результат:

one
two
three

Реальный выход:

prompt$ echo one; echo two > >(cat); echo three;
one
three
prompt$ two

Кроме того, эти две команды должны быть эквивалентны с моей точки зрения, но они не:

##### first command - the pipe is used.
prompt$ seq 1 5 | cat
1
2
3
4
5
##### second command - the process substitution and redirection are used.
prompt$ seq 1 5 > >(cat)
prompt$ 1
2
3
4
5

Почему я думаю, они должны быть одинаковыми? Потому что оба соединяют seqвывод с catвходом через анонимный канал - Википедия, Процесс замещения .

Вопрос: почему так себя ведет? Где моя ошибка? Требуется исчерпывающий ответ (с объяснением того, как это bashделается под капотом).

MiniMax
источник
2
Даже если это не так понятно с первого взгляда, на самом деле это дубликат bash wait для процесса при замене процесса, даже если команда недействительна
Стефан
2
На самом деле, было бы лучше, если бы этот другой вопрос был помечен как дубликат этого, поскольку этот вопрос более важен. Вот почему я скопировал свой ответ там.
Стефан

Ответы:

21

Да, в том же bashдухе ksh(откуда берется эта особенность) процессы внутри подстановки процессов не ожидаются (перед выполнением следующей команды в сценарии).

для <(...)одного, это обычно хорошо, как в:

cmd1 <(cmd2)

оболочка будет ожидать cmd1и cmd1будет обычно ожидать, поскольку cmd2она читает до конца файла в канале, который заменяется, и этот конец файла обычно происходит, когда cmd2умирает. Это та же причина , несколько снарядов (не bash) не беспокоить ждут cmd2в cmd2 | cmd1.

Для cmd1 >(cmd2), однако, что это вообще не так, как это более , cmd2что , как правило , ждет cmd1там так будет вообще выход после.

Это исправлено в zshтом, что ждет cmd2там (но не, если вы пишете это как cmd1 > >(cmd2)и cmd1не встроено, используйте{cmd1} > >(cmd2) вместо этого как документировано ).

kshне ждет по умолчанию, но позволяет ждать его с помощью waitвстроенного (это также делает pid доступным в$! , хотя это не поможет, если вы это сделаете cmd1 >(cmd2) >(cmd3))

rccmd1 >{cmd2}синтаксисом), так же, какksh вы можете получить pids всех фоновых процессов с $apids.

es(также с cmd1 >{cmd2}) ждет, cmd2как в zsh, а также ждетcmd2<{cmd2} перенаправления в процессе.

bashделает pid cmd2(или, точнее, подоболочки, когда она запускаетсяcmd2 в дочернем процессе этого subshell, даже если это последняя команда) $!, но не позволяет вам ждать его.

Если вам нужно использовать bash, вы можете обойти проблему, используя команду, которая будет ожидать обеих команд с:

{ { cmd1 >(cmd2); } 3>&1 >&4 4>&- | cat; } 4>&1

Это делает оба, cmd1и cmd2их FD 3 открыты для трубы. catбудет ожидать конца файла на другом конце, поэтому обычно будет выходить только тогда, когда оба cmd1и cmd2мертвы. И оболочка будет ждать этогоcat команды. Вы можете видеть, что в качестве сети можно отследить завершение всех фоновых процессов (вы можете использовать его для других вещей, запускаемых в фоновом режиме, таких как &, например, с помощью coprocs или даже команд, которые работают в фоновом режиме при условии, что они не закрывают все свои файловые дескрипторы, как это обычно делают демоны). ).

Обратите внимание, что благодаря упомянутому выше потраченному впустую процессу подоболочки, он работает, даже если cmd2закрывает свой fd 3 (команды обычно этого не делают, но некоторые любят sudoили sshделают). Будущие версии bashмогут в конечном итоге сделать оптимизацию там, как и в других оболочках. Тогда вам нужно что-то вроде:

{ { cmd1 >(sudo cmd2; exit); } 3>&1 >&4 4>&- | cat; } 4>&1

Чтобы убедиться, что еще есть дополнительный процесс оболочки с этим открытым fd 3, ожидающий эту sudoкоманду.

Обратите внимание, что catничего не будет читать (поскольку процессы не пишут на своем fd 3). Это просто для синхронизации. Он сделает только один read()системный вызов, который в конце вернется ни с чем.

Вы можете избежать запуска cat, используя подстановку команд для синхронизации канала:

{ unused=$( { cmd1 >(cmd2); } 3>&1 >&4 4>&-); } 4>&1

На этот раз это оболочка, а не catта, которая читает из канала, другой конец которого открыт на fd 3 of cmd1и cmd2. Мы используем присвоение переменной, поэтому статус выхода cmd1доступен в$? .

Или вы можете выполнить подстановку процесса вручную, а затем даже использовать систему, так shкак это станет стандартным синтаксисом оболочки:

{ cmd1 /dev/fd/3 3>&1 >&4 4>&- | cmd2 4>&-; } 4>&1

хотя обратите внимание, как отмечалось ранее, что не все shреализации будут ждать cmd1после cmd2завершения (хотя это лучше, чем наоборот). Это время $?содержит статус выхода cmd2; хотя bashи zshсделать cmd1состояние выхода доступным в ${PIPESTATUS[0]}и $pipestatus[1]соответственно (см. также pipefailпараметр в нескольких оболочках, чтобы $?можно было сообщать о сбое компонентов трубы, отличных от последнего)

Обратите внимание, что yashесть похожие проблемы с функцией перенаправления процесса . cmd1 >(cmd2)будет написано cmd1 /dev/fd/3 3>(cmd2)там. Но cmd2его не ждут, и вы тоже не можете waitего ждать, и его pid также не доступен в $!переменной. Вы бы использовали те же обходные пути, что и для bash.

Стефан Шазелас
источник
Сначала я попробовал echo one; { { echo two > >(cat); } 3>&1 >&4 4>&- | cat; } 4>&1; echo three;, затем упростил его до echo one; echo two > >(cat) | cat; echo three;и он выводит значения в правильном порядке. Нужны ли все эти дескрипторные манипуляции 3>&1 >&4 4>&-? Кроме того, я не понимаю этого >&4 4>&- мы перенаправляем stdoutна четвертый раздел, затем закрываем четвертый раздел и снова используем 4>&1его. Зачем это нужно и как это работает? Может быть, я должен создать новый вопрос на эту тему?
MiniMax
1
@MiniMax, но вы воздействуете на стандартный вывод cmd1и cmd2, смысл небольшого танца с файловым дескриптором - восстановить исходные и использовать только дополнительный канал для ожидания вместо того, чтобы также направлять вывод команд.
Стефан
@MiniMax Мне потребовалось некоторое время, чтобы понять, я не получал трубы на таком низком уровне раньше. Крайний правый 4>&1создает дескриптор файла (fd) 4 для списка команд внешних фигурных скобок и делает его равным стандартному выводу внешних фигурных скобок. Внутренние скобки имеют автоматическую настройку stdin / stdout / stderr для подключения к внешним скобкам. Тем не менее, 3>&1заставляет fd 3 подключаться к stdin внешних фигурных скобок. >&4соединяет стандартный вывод внутренних скобок с внешними скобками fd 4 (тот, который мы создали ранее). 4>&-закрывает fd 4 из внутренних фигурных скобок (поскольку стандартный вывод внутренних фигурных скобок уже связан с fd 4 из внешних фигурных скобок).
Николас Пипитоне
@MiniMax Запутанная часть была частью справа налево, 4>&1выполняется сначала, перед другими перенаправлениями, поэтому вы не «снова используете 4>&1». В целом, внутренние скобки отправляют данные на свой стандартный вывод, который был перезаписан с учетом того, что fd 4 было дано. Fd 4, который был задан внутренними скобками, это fd 4 внешних скобок, который равен исходному stdout внешних скобок.
Николас Пипитоне
Bash дает ощущение, что это 4>5означает «4 идет к 5», но на самом деле «fd 4 перезаписывается с помощью fd 5». И перед выполнением, fd 0/1/2 автоматически подключаются (вместе с любым fd внешней оболочки), и вы можете перезаписать их, как пожелаете. Это, по крайней мере, моя интерпретация документации bash. Если вы поняли что-то еще из этого , lmk.
Николас Пипитоне
4

Вы можете catнаправить вторую команду в другую , которая будет ждать, пока ее входной канал не закроется. Пример:

prompt$ echo one; echo two > >(cat) | cat; echo three;
one
two
three
prompt$

Коротко и просто.

==========

Как бы просто это ни казалось, многое происходит за кулисами. Вы можете игнорировать остальную часть ответа, если вам не интересно, как это работает.

Когда у вас есть echo two > >(cat); echo three, >(cat)он отключается интерактивной оболочкой и работает независимо от echo two. Таким образом, echo twoзаканчивается, а затемecho three исполняется, но до >(cat)окончания. Когда bashполучает данные, >(cat)когда они этого не ожидали (через пару миллисекунд), это дает вам ситуацию, похожую на подсказку, когда вам нужно нажать на новую строку, чтобы вернуться в терминал (То же самое, как если бы другой пользователь mesgвас редактировал).

Тем не менее, учитывая echo two > >(cat) | cat; echo three, две подоболочки порождены (согласно документации |символа).

Одна подоболочка с именем A предназначена для echo two > >(cat), а одна подоболочка с именем B предназначена для cat. A автоматически подключается к B (стандартный вывод A - стандартный B). Затем echo twoи >(cat)приступайте к выполнению. >(cat)Stdout 's установлен на стандартный вывод A, который равен стандартному выводу B'. После echo twoзавершения A выходит, закрывая свой стандартный вывод. Тем не менее, >(cat)до сих пор держит ссылку на стандартный ввод Б. Stdin второго catсодержит stdin B, и catон не выйдет, пока не увидит EOF. EOF дается только тогда, когда никто больше не открывает файл в режиме записи, поэтому >(cat)stdout блокирует второй cat. B ждет в эту секунду cat. После echo twoвыхода очищает буфер и завершает работу. Никто не держит Б / сек>(cat) конце концов получает EOF, так>(cat)cat ввод, поэтому второй catчитает EOF (B вообще не читает свой стандартный вывод, ему все равно). Этот EOF заставляет секунду catочищать свой буфер, закрывать стандартный вывод и завершать работу, а затем B завершается, поскольку завершается catи B ожидает cat.

Предостережение в том, что bash также порождает подоболочку >(cat)! Из-за этого вы увидите, что

echo two > >(sleep 5) | cat; echo three

все еще будет ждать 5 секунд перед выполнением echo three, даже если sleep 5не удерживает стандартный ввод B. Это связано с тем, что скрытый подоболочек C, для >(sleep 5)которого он был создан, ожидает sleep, а C держит стандартный ввод B. Вы можете увидеть, как

echo two > >(exec sleep 5) | cat; echo three

Однако не будет ждать, так sleepкак не удерживает стандартный ввод B, и нет призрачной подоболочки C, которая содержит стандартный вывод B (exec заставит sleep заменить C, в отличие от разветвления и заставления C ждатьsleep ). Независимо от этого предостережения,

echo two > >(exec cat) | cat; echo three

все равно будет правильно выполнять функции в порядке, как описано ранее.

Николас Пипитоне
источник
Как отмечалось в преобразовании с @MiniMax в комментариях к моему ответу, это, однако, имеет обратную сторону, влияющую на стандартный вывод команды, и означает, что вывод необходимо читать и записывать в дополнительное время.
Стефан Шазелас
Объяснение не точное. Aне дожидаясь catпорождал в >(cat). Как я уже упоминал в своем ответе, причина, по которой echo two > >(sleep 5 &>/dev/null) | cat; echo threeвыходные данные threeчерез 5 секунд состоят в том, что текущие версии bashтратят впустую дополнительный процесс оболочки, >(sleep 5)который ожидает, sleepи у этого процесса все еще есть стандартный вывод, pipeкоторый препятствует завершению второго cat. Если вы замените его echo two > >(exec sleep 5 &>/dev/null) | cat; echo threeна этот дополнительный процесс, вы обнаружите, что он сразу же возвращается.
Стефан Шазелас
Это делает вложенную подоболочку? Я пытался разобраться в реализации bash, чтобы понять это, я почти уверен, что echo two > >(sleep 5 &>/dev/null)как минимум получает свою собственную оболочку. Это недокументированная деталь реализации, которая также заставляет sleep 5получать свою собственную подоболочку? Если это задокументировано, то это будет законный способ сделать это с меньшим количеством символов (если только нет замкнутого цикла, я не думаю, что кто-то заметит проблемы с производительностью с подоболочкой или кошкой) `. Если это не задокументировано, тогда rip, хороший хак, не будет работать на будущих версиях.
Николас Пипитоне
$(...), <(...)действительно включает в себя подоболочку, но ksh93 или zsh будут запускать последнюю команду в этой подоболочке в том же процессе, bashпоэтому еще один процесс удерживает канал открытым, в то время sleepкак выполняется не удерживая канал открытым. В будущих версиях bashможет быть реализована аналогичная оптимизация.
Стефан Шазелас
1
@ StéphaneChazelas Я обновил свой ответ, и я думаю, что текущее объяснение более короткой версии является правильным, но вы, кажется, знаете детали реализации оболочек, чтобы вы могли проверить. Я думаю, что это решение следует использовать в противоположность танцу дескриптора файла, хотя, даже если execон работает, он работает как ожидалось.
Николас Пипитоне