Jak korzystać z polecenia coproc w różnych powłokach?

Odpowiedzi:

118

koprocesy są kshcechą (już w ksh88). zshma tę funkcję od samego początku (wczesne lata 90.), a dopiero niedawno została dodana bashw 4.0(2009).

Jednak zachowanie i interfejs różnią się znacznie między 3 powłokami.

Pomysł jest jednak ten sam: pozwala rozpocząć zadanie w tle i móc wysyłać dane wejściowe i czytać dane wyjściowe bez konieczności uciekania się do nazwanych potoków.

Odbywa się to za pomocą nienazwanych potoków z większością powłok i par gniazd z najnowszymi wersjami ksh93 w niektórych systemach.

W a | cmd | b, aprzekazuje dane cmdi bodczytuje dane wyjściowe. Działając cmdjako koproces, powłoka może być jednocześnie ai b.

koprocesy ksh

W kshrozpoczniesz koproces jako:

cmd |&

Podajesz dane cmd, wykonując takie czynności jak:

echo test >&p

lub

print -p test

I czytaj cmdwyniki takich rzeczy jak:

read var <&p

lub

read -p var

cmdjest uruchamiany jako każdą pracę w tle, można użyć fg, bg, killna nim i przekazać ją przez %job-numberlub za pośrednictwem $!.

Aby zamknąć koniec, z którego czytany cmdjest potok , możesz wykonać:

exec 3>&p 3>&-

I aby zamknąć koniec odczytu drugiego potoku (ten cmdpisze do):

exec 3<&p 3<&-

Nie można rozpocząć drugiego koprocesu, chyba że najpierw zapiszesz deskryptory pliku potoku na innym dysku FDS. Na przykład:

tr a b |&
exec 3>&p 4<&p
tr b c |&
echo aaa >&3
echo bbb >&p

koprocesory zsh

W zsh, koprocesy są prawie identyczne jak te w ksh. Jedyną prawdziwą różnicą jest to, że zshkoprocesy rozpoczynane są od coprocsłowa kluczowego.

coproc cmd
echo test >&p
read var <&p
print -p test
read -p var

Robić:

exec 3>&p

Uwaga: To nie przenosi coprocdeskryptora pliku do fd 3(jak w ksh), ale powiela go. Tak więc nie ma wyraźnego sposobu na zamknięcie rury zasilającej lub czytającej, inne rozpoczynając inną coproc .

Na przykład, aby zamknąć koniec podawania:

coproc tr a b
echo aaaa >&p # send some data

exec 4<&p     # preserve the reading end on fd 4
coproc :      # start a new short-lived coproc (runs the null command)

cat <&4       # read the output of the first coproc

Oprócz koprocesów opartych na potoku zsh(od wersji 3.1.6-dev19, wydanej w 2000 r.) Ma konstrukcje oparte na pseudo-tty expect. Aby współdziałać z większością programów, koprocesy w stylu ksh nie będą działać, ponieważ programy zaczynają buforować, gdy ich wyjściem jest potok.

Oto kilka przykładów.

Rozpocznij koproces x:

zmodload zsh/zpty
zpty x cmd

(Tutaj cmdjest proste polecenie. Ale możesz robić bardziej wyszukane rzeczy za pomocą evallub funkcji.)

Wprowadź dane z procesu równoległego:

zpty -w x some data

Czytaj dane z jednoczesnego przetwarzania (w najprostszym przypadku):

zpty -r x var

Na przykład expectmoże czekać na dane wyjściowe z koprocesu pasujące do danego wzorca.

koprocesy bash

Składnia bash jest znacznie nowsza i opiera się na nowej funkcji ostatnio dodanej do ksh93, bash i zsh. Zapewnia składnię umożliwiającą obsługę dynamicznie przydzielanych deskryptorów plików powyżej 10.

bashoferuje podstawową coproc i rozszerzoną składnię .

Podstawowa składnia

Podstawowa składnia do rozpoczęcia koprocesu wygląda następująco zsh:

coproc cmd

W kshlub zsh, rury do i z koprocesu są dostępne za pomocą >&pi <&p.

Ale bashwewnątrz deskryptory plików potoku z koprocesu i drugiego potoku do koprocesu są zwracane w $COPROCtablicy (odpowiednio ${COPROC[0]}i ${COPROC[1]}. Więc…

Przesyłaj dane do koprocesu:

echo xxx >&"${COPROC[1]}"

Czytaj dane z koprocesu:

read var <&"${COPROC[0]}"

Dzięki podstawowej składni możesz jednocześnie uruchomić tylko jeden koproces.

Rozszerzona składnia

W rozszerzonej składni możesz nazwać swoje koprocesy (jak w zshkoprocesach zpty):

coproc mycoproc { cmd; }

Polecenie musi być poleceniem złożonym. (Zauważ, jak przypomina powyższy przykład function f { ...; }.)

Tym razem deskryptory plików znajdują się w ${mycoproc[0]}i ${mycoproc[1]}.

Można uruchomić więcej niż jeden proces współpracy w czasie, ale trzeba zrobić dostać ostrzeżenie, kiedy rozpocząć proces współpracy, a jeden wciąż działa (nawet w trybie non-interaktywnym).

W przypadku korzystania z rozszerzonej składni można zamknąć deskryptory plików.

coproc tr { tr a b; }
echo aaa >&"${tr[1]}"

exec {tr[1]}>&-

cat <&"${tr[0]}"

Pamiętaj, że zamknięcie w ten sposób nie działa w wersjach bash wcześniejszych niż 4.3, w których musisz to napisać:

fd=${tr[1]}
exec {fd}>&-

Podobnie jak w kshi zshte deskryptory plików potoków są oznaczone jako close-on-exec.

Ale bashjedynym sposobem, aby przejść do tych wykonywanych poleceń jest powielać je do FDS 0, 1lub 2. Ogranicza to liczbę koprocesów, z którymi można współdziałać w ramach jednego polecenia. (Zobacz poniżej przykład.)

proces yash i przekierowanie potoku

yashsam w sobie nie ma funkcji współprocesowej, ale tę samą koncepcję można wdrożyć dzięki funkcji przekierowywania potoku i procesu . yashma interfejs do pipe()wywołania systemowego, więc można tego dokonać stosunkowo łatwo ręcznie.

Rozpocząłbyś koproces z:

exec 5>>|4 3>(cmd >&5 4<&- 5>&-) 5>&-

Który najpierw tworzy pipe(4,5)(5 koniec zapisu, 4 koniec odczytu), a następnie przekierowuje fd 3 do potoku do procesu, który działa ze stdin na drugim końcu, a stdout przechodzi do utworzonego wcześniej potoku. Następnie zamykamy koniec pisania tego potoku w rodzicu, którego nie będziemy potrzebować. Więc teraz w powłoce mamy fd 3 podłączony do standardowego wejścia cmd i fd 4 podłączony do standardowego wyjścia cmd za pomocą rur.

Zauważ, że flaga close-on-exec nie jest ustawiona dla tych deskryptorów plików.

Aby karmić dane:

echo data >&3 4<&-

Aby odczytać dane:

read var <&4 3>&-

I możesz zamknąć FDS jak zwykle:

exec 3>&- 4<&-

Dlaczego nie są tak popularne?

prawie żadna korzyść z używania nazwanych potoków

Współprocesy można łatwo wdrożyć za pomocą standardowych nazwanych rur. Nie wiem, kiedy wprowadzono dokładnie nazwane potoki, ale możliwe jest, że kshpojawiły się koprocesy (prawdopodobnie w połowie lat 80., ksh88 został „wydany” w 88, ale sądzę, że kshbył używany wewnętrznie w AT&T kilka lat wcześniej to), co tłumaczy dlaczego.

cmd |&
echo data >&p
read var <&p

Można napisać za pomocą:

mkfifo in out

cmd <in >out &
exec 3> in 4< out
echo data >&3
read var <&4

Interakcja z nimi jest prostsza - szczególnie jeśli potrzebujesz uruchomić więcej niż jeden koproces. (Zobacz przykłady poniżej.)

Jedyną zaletą używania coprocjest to, że po użyciu nie trzeba czyścić nazwanych rur.

podatne na impas

Pociski używają rur w kilku konstrukcjach:

  • Rury: Shell cmd1 | cmd2 ,
  • Zmiana polecenie: $(cmd) ,
  • i podstawianie procesów: <(cmd) , >(cmd).

W tych danych dane przepływają tylko w jednym kierunku między różnymi procesami.

Jednak dzięki jednoczesnym procesom i nazwanym potokom łatwo jest wpaść w impas. Musisz śledzić, które polecenie ma otwarty deskryptor pliku, aby zapobiec pozostawaniu otwartemu i utrzymywaniu procesu. Zakleszczenia mogą być trudne do zbadania, ponieważ mogą wystąpić nie deterministycznie; na przykład tylko wtedy, gdy wysyłanych jest tyle danych, ile potrzeba do wypełnienia jednej rury.

działa gorzej niż expectdo tego, do czego został zaprojektowany

Głównym celem koprocesów było zapewnienie powłoce sposobu interakcji z poleceniami. Jednak to nie działa tak dobrze.

Najprostsza z wymienionych wyżej form impasu:

tr a b |&
echo a >&p
read var<&p

Ponieważ dane wyjściowe nie trafiają do terminala, trbuforowane są dane wyjściowe. Więc nic nie wyświetli, dopóki nie zobaczy na końcu pliku stdinlub nie zgromadzi bufora pełnego danych do wydrukowania. Powyżej, po tym, jak powłoka ma wyjście a\n(tylko 2 bajty), readblokuje się na czas nieokreślony, ponieważ trczeka, aż powłoka wyśle ​​mu więcej danych.

Krótko mówiąc, potoki nie nadają się do interakcji z poleceniami. Koprocesy mogą być używane tylko do interakcji z poleceniami, które nie buforują danych wyjściowych lub z poleceniami, których nie można buforować; na przykład za pomocą stdbufniektórych poleceń w najnowszych systemach GNU lub FreeBSD.

Właśnie dlatego expectlub zptyzamiast tego użyj pseudo-terminali. expectto narzędzie zaprojektowane do interakcji z poleceniami i robi to dobrze.

Obsługa deskryptorów plików jest skomplikowana i trudna do uzyskania

Współprocesy można wykorzystać do bardziej skomplikowanych prac hydraulicznych, niż pozwalają na to proste rury płaszczowe.

ta inna odpowiedź Unix.SE zawiera przykład użycia coproc.

Oto uproszczony przykład: Wyobraź sobie, że potrzebujesz funkcji, która przesyła kopię danych wyjściowych polecenia do 3 innych poleceń, a następnie łączy dane wyjściowe tych 3 poleceń.

Wszystko za pomocą rur.

Na przykład: nakarmić wyjście printf '%s\n' foo bardo tr a b, sed 's/./&&/g'i cut -b2-aby otrzymać coś takiego:

foo
bbr
ffoooo
bbaarr
oo
ar

Po pierwsze, niekoniecznie jest to oczywiste, ale istnieje możliwość impasu i zacznie się to dziać już po kilku kilobajtach danych.

Następnie, w zależności od powłoki, napotkasz wiele różnych problemów, które należy rozwiązać inaczej.

Na przykład zshzrobiłbyś to z:

f() (
  coproc tr a b
  exec {o1}<&p {i1}>&p
  coproc sed 's/./&&/g' {i1}>&- {o1}<&-
  exec {o2}<&p {i2}>&p
  coproc cut -c2- {i1}>&- {o1}<&- {i2}>&- {o2}<&-
  tee /dev/fd/$i1 /dev/fd/$i2 >&p {o1}<&- {o2}<&- &
  exec cat /dev/fd/$o1 /dev/fd/$o2 - <&p {i1}>&- {i2}>&-
)
printf '%s\n' foo bar | f

Powyżej, fds koprocesów mają ustawioną flagę close-on-exec, ale nie te, które są z nich duplikowane (jak w {o1}<&p). Aby uniknąć zakleszczeń, musisz upewnić się, że są zamknięte w procesach, które ich nie potrzebują.

Podobnie musimy użyć podpowłoki i użyć jej exec catna końcu, aby upewnić się, że nie ma procesu powłoki leżącego na utrzymaniu otwartej rury.

Z ksh(tutaj ksh93) musiałoby to być:

f() (
  tr a b |&
  exec {o1}<&p {i1}>&p
  sed 's/./&&/g' |&
  exec {o2}<&p {i2}>&p
  cut -c2- |&
  exec {o3}<&p {i3}>&p
  eval 'tee "/dev/fd/$i1" "/dev/fd/$i2"' >&"$i3" {i1}>&"$i1" {i2}>&"$i2" &
  eval 'exec cat "/dev/fd/$o1" "/dev/fd/$o2" -' <&"$o3" {o1}<&"$o1" {o2}<&"$o2"
)
printf '%s\n' foo bar | f

( Uwaga: To nie zadziała w systemach, w których kshużywa socketpairszamiast pipes, i gdzie /dev/fd/ndziała jak w Linuksie.)

W ksh, fds powyżej 2są oznaczone flagą close-on-exec, chyba że są przekazywane jawnie w wierszu poleceń. Dlatego nie musimy zamykać nieużywanych deskryptorów plików, jak w przypadku zsh- ale właśnie dlatego musimy to zrobić {i1}>&$i1i użyć evaltej nowej wartości $i1, która ma zostać przekazana teei cat

W bashtym nie da się zrobić, ponieważ nie można uniknąć Close-on-exec flagi.

Powyżej jest to stosunkowo proste, ponieważ używamy tylko prostych poleceń zewnętrznych. Staje się to bardziej skomplikowane, gdy zamiast tego chcesz użyć tam konstrukcji powłoki i zaczynasz napotykać błędy powłoki.

Porównaj powyższe z tym samym, używając nazwanych potoków:

f() {
  mkfifo p{i,o}{1,2,3}
  tr a b < pi1 > po1 &
  sed 's/./&&/g' < pi2 > po2 &
  cut -c2- < pi3 > po3 &

  tee pi{1,2} > pi3 &
  cat po{1,2,3}
  rm -f p{i,o}{1,2,3}
}
printf '%s\n' foo bar | f

Wniosek

Jeśli chcesz współpracować z poleceniem, użytkowania expectlub zsh„s zptylub nazwanych potoków.

Jeśli chcesz wykonać fantazyjną instalację wodociągową za pomocą rur, użyj nazwanych rur.

Współprocesy mogą wykonać niektóre z powyższych czynności, ale bądź przygotowany na poważne podrapanie się w głowę za wszystko, co nie jest łatwe.

Stéphane Chazelas
źródło
Rzeczywiście świetna odpowiedź. Nie wiem, kiedy konkretnie zostało to ustalone, ale przynajmniej bash 4.3.11wy, Mogę teraz zamknij deskryptory plików coproc bezpośrednio, bez konieczności stosowania AUX. zmienna; jeśli chodzi o przykład z twojej odpowiedzi, exec {tr[1]}<&- to teraz by działało (aby zamknąć stdin coproc; zwróć uwagę, że twój kod (pośrednio) próbuje zamknąć {tr[1]}używając >&-, ale {tr[1]}jest stdin coproc i musi zostać zamknięty <&-). Poprawka musiała pojawić się gdzieś pomiędzy 4.2.25, co wciąż pokazuje problem, a 4.3.11co nie.
mklement0
1
@ mklement0, dzięki. exec {tr[1]}>&-wydaje się, że rzeczywiście działa z nowszymi wersjami i jest przywoływany we wpisie CWRU / changelog ( pozwól, aby słowa takie jak {array [ind]} były prawidłowe przekierowanie ... 01.09.2012). exec {tr[1]}<&-(lub bardziej poprawny >&-odpowiednik, ale to nie robi różnicy, ponieważ wymaga tylko close()obu), nie zamyka standardowego wejścia coproc, ale koniec zapisu potoku do tego coproc.
Stéphane Chazelas,
1
@ mklement0, uwaga, zaktualizowałem go i dodałem yash.
Stéphane Chazelas,
1
Zaletą mkfifojest to, że nie musisz martwić się warunkami wyścigu i bezpieczeństwem dostępu do rur. Nadal musisz martwić się impasem z fifo.
Otheus,
1
Informacje o zakleszczeniach: stdbufpolecenie może pomóc w zapobieganiu przynajmniej niektórym z nich. Użyłem go pod Linuksem i bash. W każdym razie uważam, że @ StéphaneChazelas ma rację we Wniosku: faza „drapania się po głowie” zakończyła się dla mnie dopiero, gdy wróciłem do nazwanych potoków.
shub
7

Koprocesy zostały po raz pierwszy wprowadzone w języku skryptowym powłoki z ksh88powłoką (1988), a później w zsh1993 r.

Składnia do rozpoczęcia współpracy w ramach procesu ksh jest command |&. Poczynając od tego, możesz pisać na commandstandardowe wejście print -pi czytać standardowe wyjście za pomocą read -p.

Ponad kilkadziesiąt lat później bash, któremu brakowało tej funkcji, ostatecznie wprowadził ją w wersji 4.0. Niestety wybrano niekompatybilną i bardziej złożoną składnię.

W wersji bash 4.0 i nowszych możesz uruchomić koproces z coprocpoleceniem, np .:

$ coproc awk '{print $2;fflush();}'

Następnie możesz przekazać coś do polecenia stdin w ten sposób:

$ echo one two three >&${COPROC[1]}

i czytaj wyjście awk za pomocą:

$ read -ru ${COPROC[0]} foo
$ echo $foo
two

Zgodnie z ksh byłoby to:

$ awk '{print $2;fflush();}' |&
$ print -p "one two three"
$ read -p foo
$ echo $foo
two
jlliagre
źródło
-1

Co to jest „coproc”?

Jest skrótem od „koprocesu”, co oznacza drugi proces współpracujący z powłoką. Jest bardzo podobny do zadania w tle rozpoczynanego od „&” na końcu polecenia, z tym wyjątkiem, że zamiast współużytkować to samo standardowe wejście i wyjście co jego powłoka nadrzędna, jego standardowe wejście / wyjście jest połączone z powłoką nadrzędną za pomocą specjalnego rodzaj rury zwanej FIFO. Aby zapoznać się z informacjami, kliknij tutaj

Jeden uruchamia coproc w Zsh

coproc command

Polecenie musi być przygotowane do odczytu ze stdin i / lub zapisu na stdout, w przeciwnym razie nie jest zbyt przydatne jako coproc.

Przeczytaj ten artykuł tutaj, który zawiera studium przypadku między exec a coproc

Munai Das Udasin
źródło
Czy możesz dodać część artykułu do swojej odpowiedzi? Próbowałem omówić ten temat w U&L, ponieważ wydawało się, że jest niedostatecznie reprezentowany. Dzięki za odpowiedź! Zauważ też, że ustawiłem tag jako Bash, a nie zsh.
slm
@slm Wskazałeś już hakerów Bash. Widziałem tam wystarczające przykłady. Jeśli Twoim zamiarem było zwrócenie uwagi na to pytanie, to tak, udało ci się:>
Valentin Bajrami
Nie są to specjalne rodzaje rur, są to te same rury, które są używane z nimi |. (to znaczy używaj potoków w większości powłok i par gniazd w ksh93). rury i pary gniazd są pierwszymi, pierwszymi, wszystkie są FIFO. mkfifotworzy nazwane potoki, koprocesy nie używają nazwanych potoków.
Stéphane Chazelas,
@slm przepraszam za zsh ... właściwie pracuję na zsh. Czasami robię to z prądem. Działa też dobrze w Bash ...
Munai Das Udasin,
@ Stephane Chazelas Jestem całkiem pewien, że przeczytałem gdzieś, że to I / O jest połączone ze specjalnymi rodzajami rur o nazwie FIFO ...
Munai Das Udasin
-1

Oto kolejny dobry (i działający) przykład - prosty serwer napisany w BASH. Pamiętaj, że potrzebujesz OpenBSD netcat, klasyczny nie zadziała. Oczywiście możesz użyć gniazda inet zamiast unixowego.

server.sh:

#!/usr/bin/env bash

SOCKET=server.sock
PIDFILE=server.pid

(
    exec </dev/null
    exec >/dev/null
    exec 2>/dev/null
    coproc SERVER {
        exec nc -l -k -U $SOCKET
    }
    echo $SERVER_PID > $PIDFILE
    {
        while read ; do
            echo "pong $REPLY"
        done
    } <&${SERVER[0]} >&${SERVER[1]}
    rm -f $PIDFILE
    rm -f $SOCKET
) &
disown $!

client.sh:

#!/usr/bin/env bash

SOCKET=server.sock

coproc CLIENT {
    exec nc -U $SOCKET
}

{
    echo "$@"
    read
} <&${CLIENT[0]} >&${CLIENT[1]}

echo $REPLY

Stosowanie:

$ ./server.sh
$ ./client.sh ping
pong ping
$ ./client.sh 12345
pong 12345
$ kill $(cat server.pid)
$
Aleksiej Naidyonow
źródło