Wyjście zastępowania procesu jest poza kolejnością

16

The

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

polecenie daje nieoczekiwany wynik.

Przeczytałem to: W jaki sposób podstawianie procesów jest realizowane w bash? i wiele innych artykułów na temat zastępowania procesów w Internecie, ale nie rozumiem, dlaczego tak się zachowuje.

Oczekiwany wynik:

one
two
three

Rzeczywista wydajność:

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

Te dwa polecenia powinny być równoważne z mojego punktu widzenia, ale nie są:

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

Dlaczego myślę, że powinny być takie same? Ponieważ oba łączą seqwyjście z catwejściem przez anonimowy potok - Wikipedia, proces podstawiania .

Pytanie: Dlaczego tak się zachowuje? Gdzie jest mój błąd? Pożądana jest kompleksowa odpowiedź (wraz z wyjaśnieniem, jak bashto zrobić pod maską).

MiniMax
źródło
2
Nawet jeśli na pierwszy rzut oka nie jest to tak jasne, to tak naprawdę duplikat czekania na proces w procesie podstawiania, nawet jeśli polecenie jest nieprawidłowe
Stéphane Chazelas
2
W rzeczywistości byłoby lepiej, gdyby to drugie pytanie zostało oznaczone jako duplikat tego pytania, ponieważ jest to kwestia bardziej istotna. Dlatego skopiowałem tam swoją odpowiedź.
Stéphane Chazelas,

Odpowiedzi:

21

Tak, bashpodobnie jak w ksh(skąd pochodzi funkcja), procesy wewnątrz substytucji procesu nie są czekane (przed uruchomieniem następnego polecenia w skrypcie).

dla <(...)jednego, to zwykle w porządku, jak w:

cmd1 <(cmd2)

powłoka będzie na nią czekać cmd1i cmd1zwykle będzie na nią czekać cmd2, czytając ją do końca pliku na potoku, który jest podstawiany, a ten koniec pliku zwykle ma miejsce, gdy cmd2umiera. To z tego samego powodu kilka muszli (nie bash) nie przeszkadza czeka na cmd2w cmd2 | cmd1.

Na cmd1 >(cmd2)ogół jednak tak nie jest, ponieważ cmd2zwykle więcej czeka na cmd1niego, więc zazwyczaj kończy się później.

Zostało to naprawione, zshponieważ czeka na cmd2to (ale nie, jeśli napiszesz je jako cmd1 > >(cmd2)i cmd1nie jest ono wbudowane, użyj {cmd1} > >(cmd2)zamiast tego jak udokumentowano ).

kshnie czeka domyślnie, ale pozwala poczekać na to z waitwbudowanym (udostępnia również pid $!, choć to nie pomaga, jeśli tak zrobisz cmd1 >(cmd2) >(cmd3))

rc(ze cmd1 >{cmd2}składnią), tak samo jak kshz tym wyjątkiem, że można uzyskać pids wszystkich procesów w tle za pomocą $apids.

es(również z cmd1 >{cmd2}) czeka na cmd2podobne w zsh, a także czeka na przekierowania cmd2w <{cmd2}procesie.

bashudostępnia pid cmd2(lub dokładniej podpowłoki, ponieważ działa cmd2w procesie potomnym tej podpowłoki, mimo że jest to ostatnia tam dostępna komenda) $!, ale nie pozwala na to czekać.

Jeśli musisz użyć bash, możesz obejść problem za pomocą polecenia, które będzie czekać na oba polecenia za pomocą:

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

To sprawia, że zarówno cmd1i cmd2mają fd 3 otwarte na rurze. catbędzie czekać na EOF na drugim końcu, więc zazwyczaj tylko wyjść, gdy oba cmd1i cmd2są martwe. I powłoka będzie czekać na to catpolecenie. Można to zobaczyć jako sieć przechwytującą zakończenie wszystkich procesów w tle (możesz użyć jej do innych rzeczy rozpoczętych w tle, takich jak &, coprocs, a nawet polecenia, które same w tle, pod warunkiem, że nie zamykają wszystkich swoich deskryptorów plików, jak zwykle demony ).

Zauważ, że dzięki wspomnianemu wyżej procesowi zmarnowanej podpowłoki działa, nawet jeśli cmd2zamyka swój fd 3 (polecenia zwykle tego nie robią, ale niektóre lubią sudolub sshrobią). Przyszłe wersje bashmogą ostatecznie przeprowadzić tam optymalizację, tak jak w innych powłokach. Wtedy potrzebujesz czegoś takiego:

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

Aby upewnić się, że jest jeszcze dodatkowy proces powłoki z otwartym fd 3 czekającym na to sudopolecenie.

Zauważ, że catnic nie przeczyta (ponieważ procesy nie piszą na swoim fd 3). Jest tylko do synchronizacji. Wykona tylko jedno read()wywołanie systemowe, które na końcu nie wróci.

Można faktycznie uniknąć uruchamiania cat, używając synchronizacji komend do synchronizacji potoku:

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

Tym razem to powłoka zamiast cattego czyta z potoku, którego drugi koniec jest otwarty na fd 3 z cmd1i cmd2. Używamy przypisania zmiennej, więc status wyjścia cmd1jest dostępny w $?.

Lub możesz ręcznie podstawić proces, a następnie możesz nawet użyć systemu, shponieważ stałoby się to standardową składnią powłoki:

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

zauważ jednak, jak wspomniano wcześniej, że nie wszystkie shimplementacje będą czekać cmd1po cmd2zakończeniu (choć jest to lepsze niż na odwrót). Ten czas $?zawiera status wyjścia cmd2; Choć bashand zshmake cmd1„s status wyjścia dostępne ${PIPESTATUS[0]}i $pipestatus[1]odpowiednio (patrz również pipefailopcję w kilku pocisków więc $?może zgłosić awarię elementów rurowych inne niż ostatnio)

Pamiętaj, że yashma podobne problemy z funkcją przekierowywania procesów . cmd1 >(cmd2)zostanie cmd1 /dev/fd/3 3>(cmd2)tam napisane . Ale cmd2nie jest czekany i nie można też waitna niego czekać, a jego pid również nie jest dostępny w $!zmiennej. Używałbyś tych samych obejść, co dla bash.

Stéphane Chazelas
źródło
Najpierw próbowałem echo one; { { echo two > >(cat); } 3>&1 >&4 4>&- | cat; } 4>&1; echo three;, a potem uprościłem go echo one; echo two > >(cat) | cat; echo three;i wyświetla wartości we właściwej kolejności. Czy wszystkie te manipulacje deskryptorem 3>&1 >&4 4>&-są konieczne? Również tego nie rozumiem >&4 4>&- następuje przekierowanie stdoutdo czwartej fd, następnie zamykamy czwarty fd, a następnie ponownie 4>&1go używamy . Dlaczego to potrzebne i jak działa? Może powinienem utworzyć nowe pytanie na ten temat?
MiniMax
1
@MiniMax, ale tam, masz wpływu na stdout cmd1i cmd2punkt z małym tańca z deskryptora pliku jest przywrócenie oryginalnych i tylko przy użyciu dodatkowej rury dla czekając zamiast kierowania również wyjście poleceń.
Stéphane Chazelas,
@MiniMax Trochę czasu zajęło mi zrozumienie, że wcześniej nie dostałem rur na tak niskim poziomie. Po prawej stronie 4>&1tworzy deskryptor pliku (fd) 4 dla listy poleceń zewnętrznego nawiasu klamrowego i wyrównuje go do standardowego wyjścia nawiasów klamrowych. Wewnętrzne nawiasy klamrowe mają automatycznie skonfigurowane stdin / stdout / stderr, aby połączyć się z zewnętrznymi nawiasami klamrowymi. Jednak 3>&1sprawia , że fd 3 łączy się ze stdinem zewnętrznego nawiasu klamrowego. >&4łączy stdout wewnętrznych nawiasów z zewnętrznymi nawiasami fd 4 (Ten, który wcześniej stworzyliśmy). 4>&-zamyka fd 4 z wewnętrznych nawiasów klamrowych (Ponieważ stdout wewnętrznych nawiasów klamrowych jest już podłączony do fd 4 nawiasów klamrowych).
Nicholas Pipitone
@MiniMax Myląca część była częścią od prawej do lewej, 4>&1jest wykonywana jako pierwsza, przed innymi przekierowaniami, więc nie „ponownie używasz 4>&1”. Ogólnie rzecz biorąc, wewnętrzne nawiasy klamrowe wysyłają dane do swojego standardowego wyjścia, które zostało zastąpione dowolnym podanym fd 4. Fd 4, które podano wewnętrznym nawiasom, to fd 4 nawiasów zewnętrznych, który jest równy pierwotnemu stopniowi nawiasów.
Nicholas Pipitone
Bash sprawia, że ​​wydaje się, że 4>5oznacza „4 idzie do 5”, ale tak naprawdę „fd 4 jest nadpisywany przez fd 5”. I przed wykonaniem, fd 0/1/2 są automatycznie łączone (wraz z dowolnym fd zewnętrznej powłoki) i możesz je zastąpić, jak chcesz. To przynajmniej moja interpretacja dokumentacji bash. Jeśli zrozumiałeś z tego coś innego , lmk.
Nicholas Pipitone
4

Drugie polecenie możesz potokować do drugiego cat, co będzie czekać na zamknięcie jego potoku wejściowego. Dawny:

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

Krótkie i proste.

==========

Choć wydaje się to proste, wiele dzieje się za kulisami. Możesz zignorować resztę odpowiedzi, jeśli nie jesteś zainteresowany tym, jak to działa.

Gdy masz echo two > >(cat); echo three, >(cat)jest rozwidlony przez interaktywną powłokę i działa niezależnie od echo two. W ten sposób echo twokończy się, a następnie echo threezostaje wykonany, ale przed końcem >(cat). Kiedy bashpobiera dane z >(cat)momentu, gdy się ich nie spodziewał (kilka milisekund później), daje to szybką sytuację, w której musisz nacisnąć nowy wiersz, aby wrócić do terminala (tak samo, jakby inny użytkownik mesgcię edytował).

Biorąc jednak pod uwagę echo two > >(cat) | cat; echo three, tworzone są dwie podpowłoki (zgodnie z dokumentacją |symbolu).

Jedna podpowłoka o nazwie A jest przeznaczona dla echo two > >(cat), a jedna podpowłoka o nazwie B jest przeznaczona dla cat. A jest automatycznie łączone z B (stdout A to stdin B). Następnie echo twoi >(cat)rozpocznij wykonywanie. >(cat)Stdout jest ustawiony na stdout A, który jest równy stdin B. Po echo twozakończeniu A wychodzi, zamykając stdout. Jednak >(cat)wciąż trzyma odniesienie do standardowego wejścia B.. Drugi catstdin trzyma stdin B i to catnie wyjdzie, dopóki nie zobaczy EOF. EOF jest podawany tylko wtedy, gdy nikt nie ma już otwartego pliku w trybie zapisu, więc standardowe >(cat)wyjście blokuje drugie cat. B nadal czeka na tę sekundę cat. Od czasu echo twowyjścia, w >(cat)końcu dostaje EOF, więc>(cat)opróżnia bufor i wychodzi. Nikt już nie trzyma catstdin B / sekundy , więc drugi catczyta EOF (B wcale nie czyta stdin, nie ma znaczenia). Ten EOF powoduje, że drugi catopróżnia bufor, zamyka stdout i wychodzi, a następnie B wychodzi, ponieważ został catzakończony, a B czekał cat.

Jedynym zastrzeżeniem jest to, że bash również tworzy podpowłokę >(cat)! Z tego powodu to zobaczysz

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

nadal będzie czekał 5 sekund przed wykonaniem echo three, nawet jeśli sleep 5nie trzyma standardowej wartości B. Wynika to z tego, że ukryta podpowłoka C, odrodzona, >(sleep 5)czeka sleep, a C trzyma stdin B. Możesz zobaczyć jak

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

Nie będzie jednak czekać, ponieważ sleepnie trzyma standardowego B i nie ma żadnej duchowej podpowłoki C, która trzymałaby standardową B (exec wymusi uśpienie, aby zastąpić C, w przeciwieństwie do rozwidlania i zmuszania C do oczekiwania sleep). Niezależnie od tego zastrzeżenia

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

będzie nadal poprawnie wykonywać funkcje w kolejności, jak opisano wcześniej.

Nicholas Pipitone
źródło
Jak zauważono w konwersji z @MiniMax w komentarzach do mojej odpowiedzi, ma to jednak wadę polegającą na wpłynięciu na standardowe polecenie i oznacza, że ​​wyjście musi zostać przeczytane i napisane dodatkowy czas.
Stéphane Chazelas
Wyjaśnienie nie jest dokładne. Anie czeka na catspawn >(cat). Jak już wspomniałem w mojej odpowiedzi, powodem, dla którego echo two > >(sleep 5 &>/dev/null) | cat; echo threewyniki threebashgenerowane po 5 sekundach, jest to, że obecne wersje marnują dodatkowy proces powłoki, >(sleep 5)który czeka, sleepa proces ten wciąż przechodzi do tego, pipeco uniemożliwia zakończenie drugiego cat. Jeśli zastąpisz go, echo two > >(exec sleep 5 &>/dev/null) | cat; echo threeaby wyeliminować ten dodatkowy proces, przekonasz się, że natychmiast wraca.
Stéphane Chazelas
Czy zagnieżdżona podpowłoka? Próbowałem przyjrzeć się implementacji bash, aby to rozgryźć, jestem prawie pewien, echo two > >(sleep 5 &>/dev/null)że przynajmniej ma własną podpowłokę. Czy jest to nieudokumentowany szczegół implementacji, który powoduje sleep 5również uzyskanie własnej podpowłoki? Jeśli jest to udokumentowane, byłby to uzasadniony sposób, aby zrobić to przy użyciu mniejszej liczby znaków (chyba że istnieje ścisła pętla, nie sądzę, aby ktokolwiek zauważył problemy z wydajnością podpowłoki lub kota) `. Jeśli nie jest to udokumentowane, zgrywaj, ale fajny hack nie będzie działał w przyszłych wersjach.
Nicholas Pipitone
$(...), <(...)rzeczywiście dotyczą podpowłoki, ale ksh93 lub zsh uruchomiłyby ostatnią komendę w tej podpowłoce w tym samym procesie, nie bashdlatego jest jeszcze inny proces utrzymujący potok w pozycji otwartej, podczas gdy sleepuruchomiony i nie utrzymujący potoku w pozycji otwartej. Przyszłe wersje bashmogą implementować podobną optymalizację.
Stéphane Chazelas,
1
@ StéphaneChazelas Zaktualizowałem moją odpowiedź i myślę, że obecne wyjaśnienie krótszej wersji jest poprawne, ale wydaje się, że znasz szczegóły implementacji powłok, abyś mógł je zweryfikować. Myślę, że to rozwiązanie powinno być stosowane w przeciwieństwie do tańca deskryptora pliku, ponieważ nawet poniżej execdziała on zgodnie z oczekiwaniami.
Nicholas Pipitone