Come si usa il comando coproc in varie shell?


Risposte:


118

i co-processi sono una kshcaratteristica (già presente ksh88). zshha avuto la funzionalità dall'inizio (primi anni '90), mentre è stata aggiunta solo bashin 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, aalimenta i dati cmde ne blegge l'output. L'esecuzione cmdcome coprocesso consente alla shell di essere sia ae che b.

coprocessi di ksh

In ksh, si avvia un coprocesso come:

cmd |&

Feed dati a cmdfacendo cose come:

echo test >&p

o

print -p test

E leggi cmdl'output con cose come:

read var <&p

o

read -p var

cmdviene avviato come qualsiasi processo in background, è possibile utilizzare fg, bg, killsu di esso e si riferiscono facendo %job-numbero via $!.

Per chiudere l'estremità di scrittura della pipe cmddalla quale stai leggendo, puoi fare:

exec 3>&p 3>&-

E per chiudere l'estremità di lettura dell'altra pipe (quella su cui cmdsta 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

co-processi zsh

In zsh, i co-processi sono quasi identici a quelli in ksh. L'unica vera differenza è che i zshco-processi vengono avviati con la coprocparola chiave.

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

fare:

exec 3>&p

Nota: questo non sposta il coprocdescrittore 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 zshcoprocessi 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 evalo 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.

co-processi di bash

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.

bashoffre una sintassi di base coproc e una estesa .

Sintassi di base

La sintassi di base per l'avvio di un coprocesso è simile zshalla seguente:

coproc cmd

In ksho zsh, si accede ai tubi da e verso il co-processo con >&pe <&p.

Ma in bash, i descrittori di file della pipe dal co-process e l'altra pipe al co-process sono restituiti nella $COPROCmatrice (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.

Sintassi estesa

Nella sintassi estesa, puoi nominare i tuoi co-processi (come in co-processi zshzpty):

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

processo di yash e reindirizzamento della pipeline

yashnon 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 . yashha 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<&-

Ora, perché non sono così popolari

quasi nessun vantaggio sull'uso di named pipe

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 kshvenuto fuori con dei co-processi (probabilmente a metà degli anni '80, ksh88 è stato "rilasciato" nell'88, ma credo che sia kshstato 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.

deadlock-prone

Le conchiglie usano tubi in alcuni costrutti:

  • tubi shell: cmd1 | cmd2 ,
  • sostituzione di comando: $(cmd) ,
  • e sostituzione del processo: <(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.

funziona peggio di expectquello per cui è stato progettato

Lo 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 trbufferizza 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 readblocco 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 stdbufalcuni comandi sui recenti sistemi GNU o FreeBSD.

Ecco perché expecto zptyutilizzare invece pseudo-terminali. expectè uno strumento progettato per interagire con i comandi e lo fa bene.

La gestione dei descrittori di file è complicata e difficile da ottenere

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 bara 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 catalla 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 kshutilizza socketpairsinvece di pipese in cui /dev/fd/nfunziona come su Linux.)

In ksh, i file fds sopra 2sono 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}>&$i1e usare evalper quel nuovo valore di $i1, da passare a teee cat...

In bashquesto 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

Conclusione

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.


Ottima risposta davvero. Non so quando specificamente è stato risolto, ma come di almeno 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 risposta exec {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 parte 4.2.25, che mostra ancora il problema e 4.3.11, cosa che non accade.
mklement0

1
@ mklement0, grazie. 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.
Stéphane Chazelas,

1
@ mklement0, buon punto, l'ho aggiornato e aggiunto yash.
Stéphane Chazelas,

1
Uno dei vantaggi 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.
Otheus,

1
Informazioni sui deadlock: il stdbufcomando 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.
shub

7

I coprocessi furono inizialmente introdotti in un linguaggio di scripting di shell con la ksh88shell (1988), e in seguito zshad un certo punto prima del 1993.

La sintassi per avviare un coprocesso con ksh è command |&. A partire da lì, è possibile scrivere commandsull'input standard con print -pe 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 coproccomando, 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

-1

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


Puoi aggiungere un po 'di articolo alla tua risposta? Stavo cercando di trattare questo argomento in U&L poiché sembrava sottorappresentato. Grazie per la tua risposta! Nota anche che ho impostato il tag come Bash, non zsh.
slm

@slm Hai già indicato gli hacker di Bash. Ho visto esempi sufficienti. Se la tua intenzione era quella di portare questa domanda all'attenzione, allora sì, ci sei riuscito:>
Valentin Bajrami

Non sono tipi speciali di tubi, sono gli stessi tubi usati con |. (cioè usa pipe nella maggior parte delle shell e socket in ksh93). pipe e socket sono first-in, first-out, sono tutti FIFO. mkfiforende le named pipe, i coprocessi non usano le named pipe.
Stéphane Chazelas il

@slm scusa per zsh ... attualmente lavoro su zsh. Tendo a farlo a volte con il flusso. Funziona bene anche a Bash ...
Munai Das Udasin il

@ Stephane Chazelas Sono abbastanza sicuro di averlo letto da qualche parte che è I / O collegato a tipi speciali di pipe chiamati FIFO ...
Munai Das Udasin

-1

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)
$
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.