Wie erstelle ich eine bidirektionale Pipe zwischen zwei Programmen?

63

Jeder weiß , wie unidirektionale Leitung zwischen zwei Programmen zu machen (bind stdoutder ersten und stdinder zweiten): first | second.

Aber wie macht man bidirektionale Pipe, also Cross-Bind stdinund stdoutvon zwei Programmen? Gibt es eine einfache Möglichkeit, dies in einer Shell zu tun?


quelle

Antworten:

30

Wenn die Pipes in Ihrem System bidirektional sind (wie in Solaris 11 und mindestens einigen BSDs, jedoch nicht in Linux):

cmd1 <&1 | cmd2 >&0

Vorsicht vor Deadlocks.

Beachten Sie auch, dass einige Versionen von ksh93 auf einigen Systemen Pipes ( |) mit einem Socket-Paar implementieren . Socket-Paare sind bidirektional, aber ksh93 fährt die umgekehrte Richtung explizit herunter, sodass der obige Befehl nicht mit diesen ksh93s funktioniert, selbst auf Systemen, bei denen Pipes (wie sie durch den pipe(2)Systemaufruf erstellt wurden ) bidirektional sind.

Stéphane Chazelas
quelle
1
Weiß jemand, ob dies auch unter Linux funktioniert? (unter Verwendung von archlinux hier)
heinrich5991
41

Nun, es ist ziemlich "einfach" mit Named Pipes ( mkfifo). Ich habe es einfach in Anführungszeichen gesetzt, denn wenn die Programme nicht dafür ausgelegt sind, ist ein Deadlock wahrscheinlich.

mkfifo fifo0 fifo1
( prog1 > fifo0 < fifo1 ) &
( prog2 > fifo1 < fifo0 ) &
( exec 30<fifo0 31<fifo1 )      # write can't open until there is a reader
                                # and vice versa if we did it the other way

Normalerweise ist beim Schreiben von stdout eine Pufferung erforderlich. Wenn also zum Beispiel beide Programme:

#!/usr/bin/perl
use 5.010;
say 1;
print while (<>);

Sie würden eine Endlosschleife erwarten. Aber stattdessen würden beide blockieren; Sie müssten hinzufügen $| = 1(oder eine Entsprechung), um die Ausgabepufferung zu deaktivieren. Der Deadlock wird verursacht, weil beide Programme auf etwas auf stdin warten, es aber nicht sehen, weil es sich im stdout-Puffer des anderen Programms befindet und noch nicht in die Pipe geschrieben wurde.

Update : Vorschläge von Stéphane Charzelas und Joost einfließen lassen:

mkfifo fifo0 fifo1
prog1 > fifo0 < fifo1 &
prog2 < fifo0 > fifo1

tut das gleiche, ist kürzer und portabler.

derobert
quelle
23
Eine Named Pipe ist ausreichend: prog1 < fifo | prog2 > fifo.
Andrey Vihrov
2
@AndreyVihrov das ist wahr, Sie können eine anonyme Pipe für eine der genannten ersetzen. Aber ich mag die Symmetrie :-P
derobert
3
@ user14284: Unter Linux kannst du das wahrscheinlich mit so etwas machen prog1 < fifo | tee /dev/stderr | prog2 | tee /dev/stderr > fifo.
Andrey Vihrov
3
Wenn Sie es schaffen prog2 < fifo0 > fifo1, können Sie Ihren kleinen Tanz vermeiden exec 30< ...(was übrigens nur mit bashoder yashfür FDS über 10 so funktioniert ).
Stéphane Chazelas
1
@ Joost Hmmm, es scheint so, als hättest du Recht, dass es nicht nötig ist, zumindest nicht in der Bash. Ich war wahrscheinlich besorgt, dass die Shell, da sie die Weiterleitungen ausführt (einschließlich des Öffnens der Rohre), möglicherweise blockiert - aber zumindest Gabeln schlägt, bevor die Fifos geöffnet werden. dashscheint auch in Ordnung zu sein (verhält sich aber etwas anders)
derobert
13

Ich bin mir nicht sicher, ob Sie dies versuchen:

nc -l -p 8096 -c second &
nc -c first 127.0.0.1 8096 &

Dies beginnt mit dem Öffnen eines Listening-Sockets an Port 8096, und sobald eine Verbindung hergestellt ist, wird ein Programm secondmit stdindem Stream-Ausgang und stdoutdem Stream-Eingang erzeugt.

Dann wird ein zweiter ncgestartet, der dem Listening - Port verbindet und laicht Programm firstmit seinen stdoutals Stromeingang und dessen stdinals Strom ausgegeben.

Dies geschieht nicht genau mit einer Pipe, aber es scheint zu tun, was Sie brauchen.

Da dies das Netzwerk verwendet, kann dies auf 2 Remotecomputern durchgeführt werden. Dies ist fast die Art secondund Weise, wie ein Webserver ( ) und ein Webbrowser ( first) funktionieren.

jfg956
quelle
1
Und nc -Ufür UNIX-Domain-Sockets, die nur den Adressraum des Dateisystems belegen.
Ciro Santilli
Die Option -c ist unter Linux nicht verfügbar. Mein Glück war von kurzer Dauer :-(
Pablochacin
9

Sie können pipexec verwenden :

$ pipexec -- '[A' cmd1 ] '[B' cmd2 ] '{A:1>B:0}' '{B:1>A:0}'
Andreas Florath
quelle
6

bashVersion 4 verfügt über einen coprocBefehl, der dies in reinen bashPipes ohne Named Pipes ermöglicht:

coproc cmd1
eval "exec cmd2 <&${COPROC[0]} >&${COPROC[1]}"

Einige andere Muscheln können das coprocauch.

Nachfolgend finden Sie eine ausführlichere Antwort, die jedoch drei anstelle von zwei Befehlen verkettet, was ein wenig interessanter macht.

Wenn Sie auch gerne verwenden catund stdbufdann konstruieren, kann das leichter verständlich gemacht werden.

Version bashmit catund stdbuf, leicht verständlich:

# start pipeline
coproc {
    cmd1 | cmd2 | cmd3
}
# create command to reconnect STDOUT `cmd3` to STDIN of `cmd1`
endcmd="exec stdbuf -i0 -o0 /bin/cat <&${COPROC[0]} >&${COPROC[1]}"
# eval the command.
eval "${endcmd}"

Beachten Sie, dass Sie eval verwenden müssen, da die Variablenerweiterung in <& $ var in meiner Version von Bash 4.2.25 illegal ist.

Version using pure bash: In zwei Teile aufteilen, erste Pipeline unter Coproc starten, dann den zweiten Teil zu Mittag essen (entweder ein einzelner Befehl oder eine Pipeline) und wieder mit der ersten verbinden:

coproc {
    cmd 1 | cmd2
}
endcmd="exec cmd3 <&${COPROC[0]} >&${COPROC[1]}"
eval "${endcmd}"

Konzeptioneller Beweiß:

Datei ./prog, nur ein Dummy-Programm zum Verzehren, Markieren und erneuten Drucken von Zeilen. Die Verwendung von Subshells, um Pufferprobleme zu vermeiden, kann zu einem Übermaß führen. Hier geht es nicht darum.

#!/bin/bash
let c=0
sleep 2

[ "$1" == "1" ] && ( echo start )

while : ; do
  line=$( head -1 )
  echo "$1:${c} ${line}" 1>&2
  sleep 2
  ( echo "$1:${c} ${line}" )
  let c++
  [ $c -eq 3 ] && exit
done

Datei ./start_cat Dies ist eine Version mit bash, catundstdbuf

#!/bin/bash

echo starting first cmd>&2

coproc {
  stdbuf -i0 -o0 ./prog 1 \
    | stdbuf -i0 -o0 ./prog 2 \
    | stdbuf -i0 -o0 ./prog 3
}

echo "Delaying remainer" 1>&2
sleep 5
cmd="exec stdbuf -i0 -o0 /bin/cat <&${COPROC[0]} >&${COPROC[1]}"

echo "Running: ${cmd}" >&2
eval "${cmd}"

oder Datei ./start_part. Dies ist eine Version, die bashnur pure verwendet . Für Demozwecke verwende ich immer noch, stdbufweil Ihr echter Prog sich sowieso intern mit dem Puffern befassen müsste, um ein Blockieren aufgrund von Puffern zu vermeiden.

#!/bin/bash

echo starting first cmd>&2

coproc {
  stdbuf -i0 -o0 ./prog 1 \
    | stdbuf -i0 -o0 ./prog 2
}

echo "Delaying remainer" 1>&2
sleep 5
cmd="exec stdbuf -i0 -o0 ./prog 3 <&${COPROC[0]} >&${COPROC[1]}"

echo "Running: ${cmd}" >&2
eval "${cmd}"

Ausgabe:

> ~/iolooptest$ ./start_part
starting first cmd
Delaying remainer
2:0 start
Running: exec stdbuf -i0 -o0 ./prog 3 <&63 >&60
3:0 2:0 start
1:0 3:0 2:0 start
2:1 1:0 3:0 2:0 start
3:1 2:1 1:0 3:0 2:0 start
1:1 3:1 2:1 1:0 3:0 2:0 start
2:2 1:1 3:1 2:1 1:0 3:0 2:0 start
3:2 2:2 1:1 3:1 2:1 1:0 3:0 2:0 start
1:2 3:2 2:2 1:1 3:1 2:1 1:0 3:0 2:0 start

Das tut es.

AnyDev
quelle
5

Ein praktischer Baustein zum Schreiben solcher bidirektionaler Pipes ist etwas, das die Standardausgabe und die Standardausgabe des aktuellen Prozesses miteinander verbindet. Nennen wir es Ioloop. Nach dem Aufruf dieser Funktion müssen Sie nur noch eine reguläre Pipe starten:

ioloop &&     # stdout -> stdin 
cmd1 | cmd2   # stdin -> cmd1 -> cmd2 -> stdout (-> back to stdin)

Wenn Sie die Deskriptoren der obersten Shell nicht ändern möchten, führen Sie diese in einer Subshell aus:

( ioloop && cmd1 | cmd2 )

Hier ist eine portable Implementierung von ioloop mit einer Named Pipe:

ioloop() {
    FIFO=$(mktemp -u /tmp/ioloop_$$_XXXXXX ) &&
    trap "rm -f $FIFO" EXIT &&
    mkfifo $FIFO &&
    ( : <$FIFO & ) &&    # avoid deadlock on opening pipe
    exec >$FIFO <$FIFO
}

Die Named Pipe ist im Dateisystem nur für kurze Zeit während des ioloop-Setups vorhanden. Diese Funktion ist nicht ganz POSIX, da mktemp veraltet ist (und möglicherweise für einen Rennangriff anfällig ist).

Eine linuxspezifische Implementierung mit / proc / ist möglich, für die keine Named Pipe erforderlich ist, aber ich denke, diese ist mehr als gut genug.

user873942
quelle
Interessante Funktion, +1. Könnte wahrscheinlich einen Satz oder 2 verwenden, der hinzugefügt wurde, ( : <$FIFO & )um das genauer zu erklären . Vielen Dank für die Veröffentlichung.
Alex Stragies
Ich sah mich ein wenig um und kam ausdruckslos; wo finde ich informationen über die abwertung von mktemp? Ich benutze es ausgiebig, und wenn ein neueres Tool seinen Platz eingenommen hat, möchte ich damit beginnen.
DopeGhoti
Alex: Der offene (2) Syscall auf einem Nanorohr blockiert. Wenn Sie versuchen, "exec <$ PIPE> $ PIPE" auszuführen, bleibt es hängen und wartet darauf, dass ein anderer Prozess die andere Seite öffnet. Der Befehl ": <$ FIFO &" wird in einer Subshell im Hintergrund ausgeführt und ermöglicht den erfolgreichen Abschluss der bidirektionalen Umleitung.
User873942
DopeGhoti: Die Bibliotheksfunktion mktemp (3) C ist veraltet. Das Dienstprogramm mktemp (1) ist nicht.
User873942
4

Es gibt auch

Wie @ StéphaneChazelas in den Kommentaren richtig bemerkt, sind die obigen Beispiele die "Grundform", er hat schöne Beispiele mit Optionen für seine Antwort auf eine ähnliche Frage .

Alex Stragies
quelle
Beachten Sie, dass standardmäßig socatSockets anstelle von Pipes verwendet werden (Sie können dies mit ändern commtype=pipes). Möglicherweise möchten Sie die noforkOption hinzufügen , um zu vermeiden, dass ein zusätzlicher Socat-Prozess Daten zwischen den Pipes / Sockets verschiebt. (danke für die Bearbeitung meiner Antwort übrigens)
Stéphane Chazelas
0

Hier gibt es viele gute Antworten. Also möchte ich einfach etwas hinzufügen, um einfach mit ihnen herumzuspielen. Ich nehme an, stderrwird nirgendwo umgeleitet. Erstellen Sie zwei Skripte (sagen wir a.sh und b.sh):

#!/bin/bash
echo "foo" # change to 'bar' in second file

for i in {1..10}; do
  read input
  echo ${input}
  echo ${i} ${0} got: ${input} >&2
done

Dann, wenn Sie sie auf eine gute Weise verbinden, sollten Sie auf der Konsole sehen:

1 ./a.sh got: bar
1 ./b.sh got: foo
2 ./a.sh got: foo
2 ./b.sh got: bar
3 ./a.sh got: bar
3 ./b.sh got: foo
4 ./a.sh got: foo
4 ./b.sh got: bar
...
pawel7318
quelle