Вихід процесу заміни не в порядку


16

The

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входу через анонімну трубку - Wikipedia, Process substitution .

Питання: Чому він так поводиться? Де моя помилка? Потрібна вичерпна відповідь (з поясненням того, як це bashробиться під капотом).


2
Навіть якщо з першого погляду це не так зрозуміло, це насправді дублікат башти, чекайте процесу заміни процесу, навіть якщо команда недійсна
Stéphane Chazelas

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))

rc(із cmd1 >{cmd2}синтаксисом), так само, як kshви можете отримати підказки всіх фонових процесів $apids.

es(також з cmd1 >{cmd2}) чекає, cmd2як у zsh, а також чекає cmd2в <{cmd2}перенаправленнях процесів.

bashробить pid cmd2(або точніше підкабелі, оскільки він працює cmd2в дочірньому процесі цієї підпакеті, хоча це остання команда там) доступний $!, але не дозволяє вам чекати на нього.

Якщо вам доведеться використовувати bash, ви можете вирішити проблему, скориставшись командою, яка буде чекати обох команд із:

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

Це робить і те, cmd1і cmd2їх fd 3 відкрито до труби. catбуде чекати закінчення файлу на іншому кінці, тому, як правило, вийде лише тоді, коли вони обидва cmd1та cmd2мертві. І оболонка буде чекати цієї catкоманди. Ви можете бачити, що це як мережа, щоб зафіксувати завершення всіх фонових процесів (ви можете використовувати це для інших речей, розпочатих у фоновому режимі, як, наприклад &, для копроків або навіть команд цього фону за умови, що вони не закривають усі дескриптори файлів, як, наприклад, демони ).

Зауважте, що завдяки тому, що був згаданий вище підзарядний процес, він працює, навіть якщо 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 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на четвертий fd, потім закриваємо четвертий fd, а потім знову використовуємо 4>&1його. Для чого це було потрібно і як це працює? Можливо, я повинен створити нове запитання на цю тему?
MiniMax

1
@MiniMax, але там ти впливаєш на stdout cmd1і cmd2, суть з маленьким танцем з дескриптором файлів полягає у відновленні оригінальних і використання лише додаткової труби для очікування, а не каналізації результатів команд.
Стефан Шазелас

@MiniMax Знадобилося певний час, щоб зрозуміти, я раніше не отримував труби на такому низькому рівні. Крайній правий край 4>&1створює дескриптор файлу (fd) 4 для списку команд зовнішніх дужок і робить його рівним викладенню зовнішніх дужок. Внутрішні дужки автоматично встановлюють stdin / stdout / stderr для підключення до зовнішніх брекетів. Однак, 3>&1змушує fd 3 підключитися до stdin зовнішніх брекетів. >&4змушує stdout внутрішніх брекетів з'єднуватися із зовнішніми дужками fd 4 (тим, який ми створили раніше). 4>&-закриває fd 4 від внутрішніх брекетів (Оскільки внутрішній брекет 'stdout вже підключений до зовнішніх брекетів' fd 4).
Микола Піпітон

@MiniMax Заплутана частина була правою наліво частиною, 4>&1виконується першою, перед іншими переадресаціями, тому ви більше не використовуєте їх 4>&1. Загалом, внутрішні дужки передають дані у свій stdout, який був перезаписаний тим, що було надано fd 4. Fd 4, який отримали внутрішні дужки, - це зовнішні дужки 'fd 4, що дорівнює початковій вершині зовнішніх брекетів.
Микола Піпітон

Bash дає відчуття, що 4>5означає "4 іде на 5", але насправді "fd 4 перезаписано з fd 5". Перед виконанням функції fd 0/1/2 підключаються автоматично (разом із будь-яким fd зовнішньої оболонки), і ви можете перезаписати їх за своїм бажанням. Принаймні, це моє тлумачення баш-документації. Якщо ви зрозуміли щось інше з цього , 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 - stdin B). Потім echo twoі >(cat)починайте виконувати. >(cat)'s stoutout' встановлюється в 'stdout' A, що дорівнює stdin B. Після echo twoзакінчення A виходить, закриваючи свою виворіт. Тим не менш, >(cat)досі тримається посилання на STdin. Другий catstdin утримує stdin B, і catвін не вийде, поки він не побачить EOF. EOF надається лише тоді, коли ніхто не має відкритого файлу в режимі запису, тому >(cat)stdout блокує другий cat. Б залишається чекати тієї секунди cat. З моменту echo twoвиходу >(cat)зрештою отримує EOF, значить>(cat)змиває буфер і виходить. Ніхто більше не тримає BD / секунду catstdin, тому другий catчитає EOF (B зовсім не читає його stdin, це не хвилює). Цей EOF змушує другий catзмивати свій буфер, закривати його stdout і виходити, а потім B виходить, тому що catвийшов, а B чекав на cat.

Застереження цього полягає в тому, що баш також породжує нижню частину >(cat)! Через це ви це побачите

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

ще будете чекати 5 секунд перед виконанням echo three, навіть якщо sleep 5він не тримає BD. Це пояснюється тим, що чекає прихований підмножина C, породжена для >(sleep 5)якої sleep, і C тримає стдин B. Ви можете бачити, як

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

Однак не чекатимемо, оскільки sleepне тримає stdin B, і немає жодної привидної підпрограми C, яка тримає stdin B (exec змусить сон замінити C, на відміну від розщеплення і змушення C чекати sleep). Незалежно від цього застереження,

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

все одно належним чином виконуватимуть функції в порядку, як описано раніше.


Як було зазначено в перетворенні з @MiniMax в коментарях до моєї відповіді, це, однак, має і мінус впливу на stdout команди і означає, що вихід повинен бути прочитаний і записаний додатковий час.
Стефан Шазелас

Пояснення не є точним. Aне чекає catпородженого в >(cat). Як я згадую у своїй відповіді, причина, по якій echo two > >(sleep 5 &>/dev/null) | cat; echo threeвиводиться threeчерез 5 секунд, полягає в тому, що поточні версії bashвитрачають додатковий процес оболонки, >(sleep 5)який чекає, sleepі цей процес все ще має stdout, що переходить до того, pipeщо заважає catзавершити другу . Якщо замінити його echo two > >(exec sleep 5 &>/dev/null) | cat; echo threeна усунення цього зайвого процесу, ви виявите, що він повертається відразу.
Стефан Шазелас

Це робить вкладений нижній корпус? Я намагався вивчити реалізацію bash, щоб зрозуміти це, я майже впевнений, echo two > >(sleep 5 &>/dev/null)що мінімум отримує власну нижню частину. Це не документально підтверджена деталізація впровадження, яка також спричиняє sleep 5отримання власної передплати? Якщо це задокументовано, то це було б законним способом зробити це з меншою кількістю символів (Якщо тільки немає щільного циклу, я не думаю, що хтось помітить проблеми з працездатністю з нижньою частиною або котом) `. Якщо це не задокументовано, тоді рип, приємний хак, не буде працювати на майбутніх версіях.
Микола Піпітон

$(...), <(...)дійсно залучайте нижню частину корпусу, але ksh93 або zsh запустить останню команду в цій підпакеті в тому самому процесі, bashтому не існує ще одного процесу, який тримає трубу відкритою, а sleepвона не тримає відкриту трубу. Майбутні версії bashможуть впровадити подібну оптимізацію.
Стефан Шазелас

1
@ StéphaneChazelas Я оновив свою відповідь і вважаю, що поточне пояснення коротшої версії є правильним, але ви, здається, знаєте деталі реалізації оболонок, щоб ви могли перевірити. Я думаю, що це рішення слід використовувати на відміну від танцювального файлу дескриптора, хоча, навіть навіть під exec, воно працює як очікувалося.
Микола Піпітон
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.