Wie benutzt man den Befehl coproc in verschiedenen Shells?


Antworten:


118

Co-Prozesse sind ein kshFeature (bereits in ksh88). zshhat das Feature von Anfang an (Anfang der 90er Jahre), während es erst bashin 4.0(2009) hinzugefügt wurde .

Das Verhalten und die Benutzeroberfläche unterscheiden sich jedoch erheblich zwischen den drei Schalen.

Die Idee ist jedoch dieselbe: Sie können einen Job im Hintergrund starten und ihn als Eingabe senden und auslesen, ohne auf Named Pipes zurückgreifen zu müssen.

Dies geschieht mit unbenannten Pipes mit den meisten Shells und Socket-Paaren mit den neuesten Versionen von ksh93 auf einigen Systemen.

In a | cmd | b, aspeist Daten cmdund bliest seinen Ausgang. Das Ausführen cmdals Co-Prozess ermöglicht, dass die Shell sowohl aals auch ist b.

ksh co-prozesse

In kshstarten Sie einen Coprozess als:

cmd |&

Sie geben Daten ein, cmdindem Sie Folgendes tun:

echo test >&p

oder

print -p test

Und lies cmd's Ausgabe mit Dingen wie:

read var <&p

oder

read -p var

cmdwie jeder Hintergrundjob gestartet wird, können Sie verwenden fg, bg, killauf sie und beziehen es von %job-numberoder über $!.

Um das schreibende Ende der Pipe zu schließen, aus dem cmdgelesen wird, können Sie Folgendes tun:

exec 3>&p 3>&-

Und um das Leseende der anderen Pipe zu schließen (in die geschrieben cmdwird):

exec 3<&p 3<&-

Sie können einen zweiten Co-Prozess erst starten, wenn Sie die Pipe-Dateideskriptoren auf einem anderen FDS gespeichert haben. Zum Beispiel:

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

zsh co-prozesse

In zshsind Co-Prozesse fast identisch mit denen in ksh. Der einzige wirkliche Unterschied besteht darin, dass zshCo-Prozesse mit dem coprocSchlüsselwort gestartet werden .

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

Tun:

exec 3>&p

Hinweis: Dadurch wird der coprocDateideskriptor nicht nach fd verschoben 3(wie in ksh), sondern dupliziert. Es gibt also keine explizite Möglichkeit, die Zufuhr- oder Leseleitung zu schließen, sondern eine andere coproc .

So schließen Sie zum Beispiel das Einzugsende:

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

Neben Pipe-basierten Co-Prozessen zsh(seit 3.1.6-dev19, veröffentlicht im Jahr 2000) gibt es auch Pseudotty-basierte Konstrukte wie expect. Um mit den meisten Programmen zu interagieren, funktionieren Co-Prozesse im ksh-Stil nicht, da Programme mit der Pufferung beginnen, wenn ihre Ausgabe eine Pipe ist.

Hier sind einige Beispiele.

Starten Sie den Co-Prozess x:

zmodload zsh/zpty
zpty x cmd

(Hier cmdist ein einfacher Befehl. Aber Sie können mit evaloder Funktionen ausgefallenere Dinge tun .)

Feed ein Co-Prozess-Daten:

zpty -w x some data

Co-Process-Daten lesen (im einfachsten Fall):

zpty -r x var

So expectkann es auf eine Ausgabe des Co-Prozesses warten, die mit einem bestimmten Muster übereinstimmt.

Bash Co-Prozesse

Die Bash-Syntax ist viel neuer und baut auf einer neuen Funktion auf, die kürzlich zu ksh93, bash und zsh hinzugefügt wurde. Es bietet eine Syntax, mit der dynamisch zugewiesene Dateideskriptoren über 10 verarbeitet werden können.

bashBietet eine grundlegende coproc und eine erweiterte Syntax .

Grundlegende Syntax

Die grundlegende Syntax zum Starten eines Co-Prozesses sieht wie zshfolgt aus:

coproc cmd

In kshoder zshwerden die Rohre zu und von dem Co-Prozess zugegriffen mit >&pund <&p.

In bashwerden jedoch die Dateideskriptoren der Pipe vom Co-Prozess und der anderen Pipe zum Co-Prozess im $COPROCArray (bzw. ${COPROC[0]}und) zurückgegeben ${COPROC[1]}.

Daten in den Co-Prozess einspeisen:

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

Daten aus dem Co-Prozess lesen:

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

Mit der Basissyntax können Sie jeweils nur einen Co-Prozess starten.

Erweiterte Syntax

In der erweiterten Syntax können Sie nennen Ihre Co-Prozesse (wie in zshzpty Co-proccesses):

coproc mycoproc { cmd; }

Der Befehl muss ein zusammengesetzter Befehl sein. (Beachten Sie, wie das obige Beispiel erinnert function f { ...; }.)

Diesmal befinden sich die Dateideskriptoren in ${mycoproc[0]}und ${mycoproc[1]}.

Sie können bei einer mehr als ein Co-Prozess Startzeit, aber Sie tun eine Warnung erhalten , wenn Sie einen Co-Prozess starten , während man noch (auch in nicht-interaktiven Modus) ausgeführt wird .

Sie können die Dateideskriptoren schließen, wenn Sie die erweiterte Syntax verwenden.

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

exec {tr[1]}>&-

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

Beachten Sie, dass das Schließen auf diese Weise in Bash-Versionen vor 4.3 nicht funktioniert, in denen Sie es stattdessen schreiben müssen:

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

Wie in kshund zshwerden diese Pipe-Dateideskriptoren als ausführbar markiert.

Aber bashder einzige Weg , um diejenigen zu ausgeführten Befehle zu übergeben ist , sie zu fds duplizieren 0, 1oder 2. Dies begrenzt die Anzahl der Co-Prozesse, mit denen Sie für einen einzelnen Befehl interagieren können. (Ein Beispiel finden Sie unten.)

Yash-Prozess und Pipeline-Umleitung

yashEs gibt an sich keine Co-Process-Funktion, aber dasselbe Konzept kann mit den Pipeline- und Prozessumleitungsfunktionen implementiert werden . yashhat eine Schnittstelle zum pipe()Systemaufruf, so dass dies dort relativ einfach von Hand erledigt werden kann.

Sie würden einen Co-Prozess starten mit:

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

Was zuerst ein pipe(4,5)(5 das Schreibende, 4 das Leseende) erstellt, dann fd 3 zu einer Pipe umleitet, zu einem Prozess, der mit seinem stdin am anderen Ende ausgeführt wird, und stdout zu der zuvor erstellten Pipe übergeht. Dann schließen wir das schreibende Ende dieser Pipe im übergeordneten Element, das wir nicht benötigen. Also haben wir jetzt in der Shell fd 3 mit dem stdin des cmd verbunden und fd 4 mit Pipes mit dem stdout des cmd verbunden.

Beachten Sie, dass das Close-On-Exec-Flag in diesen Dateideskriptoren nicht gesetzt ist.

So füttern Sie Daten:

echo data >&3 4<&-

So lesen Sie Daten:

read var <&4 3>&-

Und Sie können fds wie gewohnt schließen:

exec 3>&- 4<&-

Nun, warum sind sie nicht so beliebt

kaum ein Vorteil gegenüber der Verwendung von Named Pipes

Co-Prozesse können einfach mit Standard Named Pipes implementiert werden. Ich weiß nicht, wann genau Named Pipes eingeführt wurden, aber es ist möglich, dass kshCo-Prozesse entwickelt wurden (wahrscheinlich Mitte der 80er Jahre, ksh88 wurde 88 "veröffentlicht", aber ich glaube, es kshwurde einige Jahre zuvor intern bei AT & T verwendet das) was erklären würde warum.

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

Kann geschrieben werden mit:

mkfifo in out

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

Die Interaktion mit diesen ist unkomplizierter, insbesondere wenn Sie mehr als einen Co-Prozess ausführen müssen. (Siehe Beispiele unten.)

Der einzige Vorteil bei der Verwendung coprocbesteht darin, dass Sie diese benannten Rohre nach der Verwendung nicht entfernen müssen.

Deadlock-anfällig

Muscheln verwenden Rohre in einigen Konstrukten:

  • Schalenrohre: cmd1 | cmd2 ,
  • Befehl Substitution: $(cmd) ,
  • und Prozesssubstitution: <(cmd) , >(cmd).

In diesen fließen die Daten zwischen verschiedenen Prozessen nur in eine Richtung.

Bei Co-Prozessen und Named Pipes kann es jedoch leicht zu Deadlocks kommen. Sie müssen nachverfolgen, bei welchem ​​Befehl welcher Dateideskriptor geöffnet ist, um zu verhindern, dass einer geöffnet bleibt und einen Prozess am Leben hält. Deadlocks können schwierig zu untersuchen sein, da sie nicht deterministisch auftreten können. Zum Beispiel nur, wenn so viele Daten gesendet werden, dass nur eine Pipe voll ist.

funktioniert schlechter als expectfür das, wofür es entwickelt wurde

Der Hauptzweck von Co-Prozessen bestand darin, der Shell die Möglichkeit zu geben, mit Befehlen zu interagieren. Es funktioniert jedoch nicht so gut.

Die einfachste Form des oben erwähnten Deadlocks ist:

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

Da seine Ausgabe nicht an ein Terminal geht, trpuffert er seine Ausgabe. Es wird also nichts ausgegeben, bis entweder das Dateiende angezeigt stdinwird oder ein Puffer voller auszugebender Daten vorhanden ist. Nach der Ausgabe der Shell a\n(nur 2 Bytes) readblockiert die also auf unbestimmte Zeit, da sie darauf trwartet, dass die Shell weitere Daten sendet.

Kurz gesagt, Pipes eignen sich nicht für die Interaktion mit Befehlen. Co-Prozesse können nur verwendet werden, um mit Befehlen zu interagieren, die ihre Ausgabe nicht puffern, oder mit Befehlen, die angewiesen werden, ihre Ausgabe nicht zu puffern. Zum Beispiel stdbufmit einigen Befehlen auf neueren GNU- oder FreeBSD-Systemen.

Deshalb expectoder zptyVerwendung pseudo-Terminals statt. expectist ein Tool, das für die Interaktion mit Befehlen entwickelt wurde und das auch gut funktioniert.

Die Handhabung von Dateideskriptoren ist umständlich und schwierig

Co-Prozesse können verwendet werden, um komplexere Rohrleitungen zu erstellen, als dies mit einfachen Mantelrohren möglich ist.

Diese andere Unix.SE-Antwort enthält ein Beispiel für eine Coproc-Verwendung.

Hier ist ein vereinfachtes Beispiel: Stellen Sie sich vor, Sie möchten eine Funktion, die eine Kopie der Ausgabe eines Befehls an drei andere Befehle weiterleitet und dann die Ausgabe dieser drei Befehle verkettet.

Alles mit Rohren.

Zum Beispiel: füttern die Ausgabe printf '%s\n' foo barzu tr a b, sed 's/./&&/g'und cut -b2-wie etwas zu erhalten:

foo
bbr
ffoooo
bbaarr
oo
ar

Erstens ist es nicht unbedingt offensichtlich, aber es besteht die Möglichkeit eines Deadlocks, der bereits nach wenigen Kilobyte Daten auftritt.

Abhängig von Ihrer Shell treten dann verschiedene Probleme auf, die unterschiedlich gelöst werden müssen.

Zum Beispiel zshwürden Sie mit:

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

Oben ist für die Co-Process-FDS das Flag "Close-On-Exec" gesetzt, jedoch nicht die, die von ihnen dupliziert wurden (wie in {o1}<&p). Um Deadlocks zu vermeiden, müssen Sie sicherstellen, dass sie in Prozessen geschlossen sind, die sie nicht benötigen.

In ähnlicher Weise müssen wir eine Unterschale verwenden und exec catam Ende verwenden, um sicherzustellen, dass kein Schalenprozess über das Offenhalten eines Rohrs liegt.

Mit ksh(hier ksh93) müsste das sein:

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

( Hinweis: Dies funktioniert nicht auf Systemen, kshdie socketpairsanstelle von verwendet werden pipes, und auf Systemen , die /dev/fd/nwie unter Linux funktionieren.)

In kshwerden fds oben 2mit dem Flag close-on-exec markiert, sofern sie nicht explizit in der Befehlszeile übergeben werden. Aus diesem Grund müssen wir die nicht verwendeten Dateideskriptoren nicht schließen wie mit zsh- aber es ist auch der Grund, warum wir diesen neuen Wert von tun {i1}>&$i1und verwenden müssen , um ihn weiterzugeben und ...eval$i1teecat

In bashdieser kann nicht getan werden, da Sie die close-on-exec nicht vermeiden Flagge.

Oben ist es relativ einfach, weil wir nur einfache externe Befehle verwenden. Es wird komplizierter, wenn Sie stattdessen Shell-Konstrukte verwenden möchten, und Sie beginnen, auf Shell-Fehler zu stoßen.

Vergleichen Sie das oben Genannte mit Named Pipes:

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

Fazit

Wenn Sie möchten , mit einem Befehl, den Einsatz zu interagieren expect, oder zsh‚s zptyoder Named Pipes.

Wenn Sie ausgefallene Rohrleitungen verwenden möchten, verwenden Sie Named Pipes.

Co-Prozesse können einige der oben genannten Aufgaben ausführen, sollten jedoch darauf vorbereitet sein, bei nicht-trivialen Aufgaben ernsthaft am Kopf zu kratzen.


Tolle Antwort. Ich weiß nicht , wenn dies ausdrücklich darauf fixiert war, sondern als zumindest bash 4.3.11, Sie können jetzt schließen coproc Filedeskriptoren direkt, ohne die Notwendigkeit für einen Aux. Variable; in Bezug auf das Beispiel in Ihrer Antwort exec {tr[1]}<&- jetzt funktionieren würde (um die coproc enge stdin, beachten Sie, dass Ihr Code (indirekt) zu schließen versucht {tr[1]}mit >&-, aber {tr[1]}ist das die coproc stdin und muss mit geschlossen werden <&-). Der Fix muss irgendwo dazwischen gekommen sein 4.2.25, was immer noch das Problem darstellt, und 4.3.11was nicht.
mklement0

1
@ mklement0, danke. exec {tr[1]}>&-scheint in der Tat mit neueren Versionen zu funktionieren und wird in einem CWRU / Changelog-Eintrag referenziert ( lassen Sie Wörter wie {array [ind]} als gültige Umleitung zu ... 2012-09-01). exec {tr[1]}<&-(oder das korrektere >&-Äquivalent, obwohl das keinen Unterschied macht, da es nur close()beides erfordert) schließt nicht die Standardeingabe des Coprocs, sondern das schreibende Ende der Pipe zu diesem Coproc.
Stéphane Chazelas

1
@ mklement0, guter Punkt, ich habe es aktualisiert und hinzugefügt yash.
Stéphane Chazelas

1
Ein Vorteil gegenüber mkfifoist, dass Sie sich keine Sorgen um die Rennbedingungen und die Sicherheit für den Pipezugang machen müssen. Sie müssen sich immer noch um die Deadlocks bei den Fifos sorgen.
Otheus

1
Über Deadlocks: Der stdbufBefehl kann dazu beitragen, zumindest einige von ihnen zu verhindern. Ich habe es unter Linux und bash benutzt. Jedenfalls glaube ich, dass @ StéphaneChazelas im Fazit richtig ist: Die "Head Scratching" -Phase endete für mich erst, als ich wieder auf Named Pipes umstieg.
Shub

7

Co-Prozesse wurden zuerst in einer Shell-Skriptsprache mit der ksh88Shell (1988) und später zshvor 1993 eingeführt.

Die Syntax zum Starten eines Co-Prozesses unter ksh lautet command |&. Von dort aus können Sie mit in die commandStandardeingabe schreiben und mit print -pderen Standardausgabe lesen read -p.

Mehr als ein paar Jahrzehnte später führte bash, dem diese Funktion fehlte, sie schließlich in der Version 4.0 ein. Leider wurde eine inkompatible und komplexere Syntax gewählt.

Unter Bash 4.0 und höher können Sie mit dem coprocBefehl einen Co-Prozess starten , z.

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

Sie können dann etwas an das Kommando stdin übergeben:

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

und lese awk Ausgabe mit:

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

Unter ksh wäre das gewesen:

$ awk '{print $2;fflush();}' |&
$ print -p "one two three"
$ read -p foo
$ echo $foo
two

-1

Was ist ein Coproc?

Es ist die Abkürzung für "Co-Process", was bedeutet, dass ein zweiter Prozess mit der Shell zusammenarbeitet. Es ist einem Hintergrundjob sehr ähnlich, der mit einem "&" am Ende des Befehls gestartet wird, mit der Ausnahme, dass anstelle der gleichen Standardeingabe und -ausgabe wie die übergeordnete Shell die Standard-E / A über eine spezielle Verbindung mit der übergeordneten Shell verbunden ist Rohrart FIFO genannt. Für Referenz hier klicken

Man startet einen coproc in zsh mit

coproc command

Der Befehl muss zum Lesen von stdin und / oder zum Schreiben von stdout vorbereitet sein, oder er ist als Coproc wenig nützlich.

Lesen Sie diesen Artikel hier. Er enthält eine Fallstudie zwischen exec und coproc


Können Sie Ihrer Antwort einen Teil des Artikels hinzufügen? Ich habe versucht, dieses Thema in U & L zu behandeln, da es unterrepräsentiert zu sein schien. Danke für deine Antwort! Beachten Sie auch, dass ich das Tag als Bash festgelegt habe, nicht als zsh.
SLM

@slm Du hast schon auf Bash-Hacker hingewiesen. Ich habe dort genügend Beispiele gesehen. Wenn Ihre Absicht war, diese Frage zur Sprache zu bringen, dann haben Sie es geschafft:
Valentin Bajrami

Es sind keine besonderen Arten von Pfeifen, es sind die gleichen Pfeifen, mit denen sie verwendet werden |. (Das heißt, verwenden Sie Pipes in den meisten Shells und Socket-Paare in ksh93). Pipes und Socket-Paare sind first-in, first-out, sie sind alle FIFO. mkfifoErstellt Named Pipes, Coprozesse verwenden keine Named Pipes.
Stéphane Chazelas

@slm sorry für zsh ... eigentlich arbeite ich an zsh. Ich neige dazu, es manchmal mit dem Fluss zu tun. Es funktioniert auch gut in Bash ...
Munai Das Udasin

@ Stephane Chazelas Ich bin mir ziemlich sicher, dass ich irgendwo gelesen habe, dass es sich um I / O handelt, das mit speziellen Arten von Pipes namens FIFO verbunden ist ...
Munai Das Udasin

-1

Hier ist ein weiteres gutes (und funktionierendes) Beispiel - ein einfacher Server, der in BASH geschrieben ist. Bitte beachte, dass du OpenBSD's brauchst netcat, das klassische wird nicht funktionieren. Natürlich können Sie inet socket anstelle von unix one verwenden.

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

Verwendungszweck:

$ ./server.sh
$ ./client.sh ping
pong ping
$ ./client.sh 12345
pong 12345
$ kill $(cat server.pid)
$
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.