Risposte:
i co-processi sono una ksh
caratteristica (già presente ksh88
). zsh
ha avuto la funzionalità dall'inizio (primi anni '90), mentre è stata aggiunta solo bash
in 4.0
(2009).
Tuttavia, il comportamento e l'interfaccia sono significativamente diversi tra le 3 shell.
L'idea è la stessa, però: consente di avviare un lavoro in background e di essere in grado di inviarlo in input e leggere il suo output senza dover ricorrere a named pipe.
Questo viene fatto con pipe senza nome con la maggior parte delle shell e socketpairs con versioni recenti di ksh93 su alcuni sistemi.
In a | cmd | b
, a
alimenta i dati cmd
e ne b
legge l'output. L'esecuzione cmd
come coprocesso consente alla shell di essere sia a
e che b
.
In ksh
, si avvia un coprocesso come:
cmd |&
Feed dati a cmd
facendo cose come:
echo test >&p
o
print -p test
E leggi cmd
l'output con cose come:
read var <&p
o
read -p var
cmd
viene avviato come qualsiasi processo in background, è possibile utilizzare fg
, bg
, kill
su di esso e si riferiscono facendo %job-number
o via $!
.
Per chiudere l'estremità di scrittura della pipe cmd
dalla quale stai leggendo, puoi fare:
exec 3>&p 3>&-
E per chiudere l'estremità di lettura dell'altra pipe (quella su cui cmd
sta scrivendo):
exec 3<&p 3<&-
Non è possibile avviare un secondo co-processo a meno che non si salvino prima i descrittori del file di pipe in altri fds. Per esempio:
tr a b |&
exec 3>&p 4<&p
tr b c |&
echo aaa >&3
echo bbb >&p
In zsh
, i co-processi sono quasi identici a quelli in ksh
. L'unica vera differenza è che i zsh
co-processi vengono avviati con la coproc
parola chiave.
coproc cmd
echo test >&p
read var <&p
print -p test
read -p var
fare:
exec 3>&p
Nota: questo non sposta il coproc
descrittore di file su fd 3
(come in ksh
), ma lo duplica. Quindi, non esiste un modo esplicito per chiudere la pipa di alimentazione o lettura, altro iniziando un altro coproc
.
Ad esempio, per chiudere l'estremità di alimentazione:
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
Oltre ai zsh
coprocessi basati su pipe, (dal 3.1.6-dev19, rilasciato nel 2000) ha costrutti basati su pseudo-tty come expect
. Per interagire con la maggior parte dei programmi, i co-processi in stile ksh non funzioneranno, poiché i programmi iniziano a bufferizzare quando il loro output è una pipe.
Ecco alcuni esempi.
Inizia il co-processo x
:
zmodload zsh/zpty
zpty x cmd
(Qui, cmd
è un comando semplice. Ma puoi fare cose più fantasiose con eval
o funzioni.)
Inserisci un dato di co-processo:
zpty -w x some data
Leggi i dati di co-process (nel caso più semplice):
zpty -r x var
Ad esempio expect
, può attendere un po 'di output dal co-processo corrispondente a un determinato modello.
La sintassi bash è molto più recente e si basa su una nuova funzionalità recentemente aggiunta a ksh93, bash e zsh. Fornisce una sintassi per consentire la gestione di descrittori di file allocati dinamicamente sopra 10.
bash
offre una sintassi di base coproc
e una estesa .
La sintassi di base per l'avvio di un coprocesso è simile zsh
alla seguente:
coproc cmd
In ksh
o zsh
, si accede ai tubi da e verso il co-processo con >&p
e <&p
.
Ma in bash
, i descrittori di file della pipe dal co-process e l'altra pipe al co-process sono restituiti nella $COPROC
matrice (rispettivamente ${COPROC[0]}
e ${COPROC[1]}
. Quindi ...
Invia i dati al co-processo:
echo xxx >&"${COPROC[1]}"
Leggi i dati dal co-processo:
read var <&"${COPROC[0]}"
Con la sintassi di base, è possibile avviare solo un coprocesso alla volta.
Nella sintassi estesa, puoi nominare i tuoi co-processi (come in co-processi zsh
zpty):
coproc mycoproc { cmd; }
Il comando deve essere un comando composto. (Nota come l'esempio sopra ricorda function f { ...; }
.)
Questa volta, i descrittori di file sono in ${mycoproc[0]}
e ${mycoproc[1]}
.
È possibile avviare più di un co-processo alla volta, ma si fare ottenere un avviso quando si avvia un processo di co-mentre uno è ancora in esecuzione (anche in modalità non interattiva).
È possibile chiudere i descrittori di file quando si utilizza la sintassi estesa.
coproc tr { tr a b; }
echo aaa >&"${tr[1]}"
exec {tr[1]}>&-
cat <&"${tr[0]}"
Nota che chiudendo in questo modo non funziona nelle versioni bash precedenti alla 4.3 dove invece devi scriverlo:
fd=${tr[1]}
exec {fd}>&-
Come in ksh
e zsh
, questi descrittori di file di pipe sono contrassegnati come close-on-exec.
Ma in bash
, l'unico modo per passare coloro ai comandi eseguiti è di duplicarli a fds 0
, 1
, o 2
. Ciò limita il numero di coprocessi con cui è possibile interagire per un singolo comando. (Vedi sotto per un esempio.)
yash
non ha di per sé una funzione di coprocesso, ma lo stesso concetto può essere implementato con le sue pipeline e le funzioni di reindirizzamento dei processi . yash
ha un'interfaccia per la pipe()
chiamata di sistema, quindi questo tipo di cose può essere fatto relativamente facilmente a mano lì.
Inizieresti un co-processo con:
exec 5>>|4 3>(cmd >&5 4<&- 5>&-) 5>&-
Che prima crea un pipe(4,5)
(5 alla fine della scrittura, 4 alla fine della lettura), quindi reindirizza fd 3 a una pipe verso un processo che viene eseguito con il suo stdin all'altra estremità e stdout che va alla pipe creata in precedenza. Quindi chiudiamo l'estremità di scrittura di quella pipe nel genitore di cui non avremo bisogno. Quindi ora nella shell abbiamo fd 3 collegato allo stdin di cmd e fd 4 collegato allo stdout di cmd con pipe.
Si noti che il flag close-on-exec non è impostato su quei descrittori di file.
Per alimentare i dati:
echo data >&3 4<&-
Per leggere i dati:
read var <&4 3>&-
E puoi chiudere fds come al solito:
exec 3>&- 4<&-
I coprocessi possono essere facilmente implementati con pipe denominate standard. Non so quando sono state introdotte le pipe con nome esatto, ma è possibile che sia ksh
venuto fuori con dei co-processi (probabilmente a metà degli anni '80, ksh88 è stato "rilasciato" nell'88, ma credo che sia ksh
stato usato internamente in AT&T qualche anno prima quello) che spiegherebbe il perché.
cmd |&
echo data >&p
read var <&p
Può essere scritto con:
mkfifo in out
cmd <in >out &
exec 3> in 4< out
echo data >&3
read var <&4
Interagire con questi è più semplice, specialmente se è necessario eseguire più di un co-processo. (Vedi gli esempi di seguito.)
L'unico vantaggio dell'utilizzo coproc
è che non è necessario ripulire le tubature indicate dopo l'uso.
Le conchiglie usano tubi in alcuni costrutti:
cmd1 | cmd2
,$(cmd)
,<(cmd)
, >(cmd)
.In questi, i dati scorrono in una sola direzione tra processi diversi.
Con i co-processi e le pipe denominate, tuttavia, è facile imbattersi in deadlock. Devi tenere traccia di quale comando ha quale descrittore di file aperto, per evitare che uno rimanga aperto e mantenga attivo un processo. I deadlock possono essere difficili da investigare, perché possono verificarsi in modo non deterministico; ad esempio, solo quando vengono inviati tutti i dati necessari per riempire una pipe.
expect
quello per cui è stato progettatoLo scopo principale dei coprocessi era fornire alla shell un modo per interagire con i comandi. Tuttavia, non funziona così bene.
La forma più semplice di deadlock sopra menzionata è:
tr a b |&
echo a >&p
read var<&p
Poiché il suo output non va su un terminale, ne tr
bufferizza l'output. Quindi non produrrà nulla fino a quando non vedrà la fine del file sul suo stdin
, o non avrà accumulato un buffer pieno di dati per l'output. Quindi, sopra, dopo che la shell ha emesso a\n
(solo 2 byte), il read
blocco si bloccherà indefinitamente perché tr
è in attesa che la shell invii più dati.
In breve, le pipe non sono buone per interagire con i comandi. I coprocessi possono essere usati solo per interagire con comandi che non bufferizzano il loro output, o comandi a cui è possibile dire di non bufferizzare il loro output; per esempio, usando stdbuf
alcuni comandi sui recenti sistemi GNU o FreeBSD.
Ecco perché expect
o zpty
utilizzare invece pseudo-terminali. expect
è uno strumento progettato per interagire con i comandi e lo fa bene.
I coprocessi possono essere utilizzati per eseguire impianti idraulici più complessi di quelli consentiti dai semplici tubi a guscio.
quell'altra risposta Unix.SE ha un esempio di utilizzo coproc.
Ecco un esempio semplificato: Immagina di volere una funzione che alimenta una copia dell'output di un comando ad altri 3 comandi, e quindi concatenare l'output di quei 3 comandi.
Tutti usando tubi.
Per esempio: alimentare l'uscita di printf '%s\n' foo bar
a tr a b
, sed 's/./&&/g'
e cut -b2-
di ottenere qualcosa di simile a:
foo
bbr
ffoooo
bbaarr
oo
ar
Innanzitutto, non è necessariamente ovvio, ma c'è una possibilità di deadlock lì, e inizierà a verificarsi dopo solo pochi kilobyte di dati.
Quindi, a seconda della shell, si verificheranno numerosi problemi diversi che devono essere risolti in modo diverso.
Ad esempio, con zsh
, lo faresti con:
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
Sopra, i co-process fds hanno il flag close-on-exec impostato, ma non quelli che sono duplicati da loro (come in {o1}<&p
). Quindi, per evitare deadlock, dovrai assicurarti che siano chiusi in tutti i processi che non ne hanno bisogno.
Allo stesso modo, dobbiamo usare una subshell e usarla exec cat
alla fine, per assicurarci che non ci sia alcun processo di shell nel tenere aperta una pipe.
Con ksh
(qui ksh93
), dovrebbe essere:
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
( Nota: non funzionerà su sistemi in cui ksh
utilizza socketpairs
invece di pipes
e in cui /dev/fd/n
funziona come su Linux.)
In ksh
, i file fds sopra 2
sono contrassegnati con il flag close-on-exec, a meno che non siano passati esplicitamente sulla riga di comando. Ecco perché non dobbiamo chiudere i descrittori di file inutilizzati come con zsh
— ma è anche il motivo per cui dobbiamo fare {i1}>&$i1
e usare eval
per quel nuovo valore di $i1
, da passare a tee
e cat
...
In bash
questo non si può fare, perché non è possibile evitare il flag close-on-exec.
Sopra, è relativamente semplice, perché usiamo solo semplici comandi esterni. Diventa più complicato quando vuoi usare i costrutti di shell lì dentro e inizi a imbatterti in bug di shell.
Confronta quanto sopra con lo stesso usando named pipe:
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
Se si desidera interagire con un comando, utilizzare expect
, o zsh
's zpty
, o named pipe.
Se vuoi fare un impianto idraulico di fantasia con i tubi, usa i tubi denominati.
I coprocessi possono fare alcune delle cose precedenti, ma essere pronti a fare qualche serio graffio alla testa per qualcosa di non banale.
exec {tr[1]}>&-
sembra davvero funzionare con le versioni più recenti ed è referenziato in una voce CWRU / changelog ( consenti a parole come {array [ind]} di essere reindirizzamento valido ... 2012-09-01). exec {tr[1]}<&-
(o l' >&-
equivalente più corretto, sebbene ciò non faccia alcuna differenza in quanto richiede solo close()
entrambi) non chiude lo stdin del coproc, ma la fine della scrittura della pipe su quel coproc.
yash
.
mkfifo
è che non devi preoccuparti delle condizioni di gara e della sicurezza per l'accesso alle condotte. Devi ancora preoccuparti dello stallo con i Fifo.
stdbuf
comando può aiutare a prevenirne almeno alcuni. L'ho usato sotto Linux e bash. Ad ogni modo, credo che @ StéphaneChazelas abbia ragione nella conclusione: la fase di "graffiare la testa" è finita per me solo quando sono tornato alle pipe nominate.
I coprocessi furono inizialmente introdotti in un linguaggio di scripting di shell con la ksh88
shell (1988), e in seguito zsh
ad un certo punto prima del 1993.
La sintassi per avviare un coprocesso con ksh è command |&
. A partire da lì, è possibile scrivere command
sull'input standard con print -p
e leggere l'output standard con read -p
.
Più di un paio di decenni dopo, Bash, che mancava di questa funzionalità, finalmente lo introdusse nella sua versione 4.0. Sfortunatamente, è stata selezionata una sintassi incompatibile e più complessa.
In bash 4.0 e versioni successive, è possibile avviare un co-processo con il coproc
comando, ad esempio:
$ coproc awk '{print $2;fflush();}'
È quindi possibile passare qualcosa al comando stdin in questo modo:
$ echo one two three >&${COPROC[1]}
e leggi l'output di awk con:
$ read -ru ${COPROC[0]} foo
$ echo $foo
two
Sotto ksh, sarebbe stato:
$ awk '{print $2;fflush();}' |&
$ print -p "one two three"
$ read -p foo
$ echo $foo
two
Che cos'è un "coproc"?
È l'abbreviazione di "co-process", che significa un secondo processo che coopera con la shell. È molto simile a un processo in background iniziato con un "&" alla fine del comando, tranne per il fatto che invece di condividere lo stesso input e output standard della sua shell padre, il suo I / O standard è collegato alla shell padre da uno speciale tipo di pipe chiamato FIFO.Per riferimento clicca qui
Si avvia un coprocessore in zsh con
coproc command
Il comando deve essere preparato per leggere da stdin e / o scrivere su stdout, oppure non è molto utile come coproc.
Leggi questo articolo qui fornisce un case study tra exec e coproc
|
. (cioè usa pipe nella maggior parte delle shell e socket in ksh93). pipe e socket sono first-in, first-out, sono tutti FIFO. mkfifo
rende le named pipe, i coprocessi non usano le named pipe.
Ecco un altro buon esempio (e funzionante): un semplice server scritto in BASH. Nota che avresti bisogno di OpenBSD netcat
, quello classico non funzionerà. Ovviamente potresti usare il socket inet invece di unix.
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
Uso:
$ ./server.sh
$ ./client.sh ping
pong ping
$ ./client.sh 12345
pong 12345
$ kill $(cat server.pid)
$
bash 4.3.11
, voi can ora vicino descrittori di file coproc direttamente, senza la necessità di un aux. variabile; in termini dell'esempio nella tua rispostaexec {tr[1]}<&-
ora funzionerebbe (per chiudere lo stdin del coproc; nota che il tuo codice (indirettamente) tenta di chiudere{tr[1]}
usando>&-
, ma{tr[1]}
è lo stdin del coproc e deve essere chiuso con<&-
). La correzione deve essere arrivata da qualche parte4.2.25
, che mostra ancora il problema e4.3.11
, cosa che non accade.