Wie kann ich einen zirkulären Datenfluss zwischen verschalteten Befehlen implementieren?

19

Ich kenne zwei Arten, wie Befehle miteinander verbunden werden können:

  1. mit einer Pipe (std-output in std-input des nächsten Befehls setzen).
  2. durch Verwenden eines T-Stücks (Spleißen Sie den Ausgang in viele Ausgänge).

Ich weiß nicht, ob das alles ist, was möglich ist, also zeichne ich einen hypothetischen Verbindungstyp:

Bildbeschreibung hier eingeben

Wie könnte es möglich sein, einen zirkulären Datenfluss zwischen Befehlen zu implementieren, wie zum Beispiel in diesem Pseudocode, in dem ich Variablen anstelle von Befehlen verwende:

pseudo-code:

a = 1    # start condition 

repeat 
{
b = tripple(a)
c = sin(b) 
a = c + 1 
}
Abdul Al Hazred
quelle

Antworten:

16

Circular I / O Loop Implementiert mit tail -f

Dies implementiert eine kreisförmige E / A-Schleife:

$ echo 1 >file
$ tail -f file | while read n; do echo $((n+1)); sleep 1; done | tee -a file
2
3
4
5
6
7
[..snip...]

Dies implementiert die zirkuläre Eingabe- / Ausgabeschleife unter Verwendung des von Ihnen erwähnten Sinusalgorithmus:

$ echo 1 >file
$ tail -f file | while read n; do echo "1+s(3*$n)" | bc -l; sleep 1; done | tee -a file
1.14112000805986722210
.72194624281527439351
1.82812473159858353270
.28347272185896349481
1.75155632167982146959
[..snip...]

Hier bcwird die Gleitkommamathematik ausgeführt und s(...)ist die bc-Notation für die Sinusfunktion.

Implementierung des gleichen Algorithmus unter Verwendung einer Variablen

Für dieses spezielle mathematische Beispiel wird der zirkuläre E / A-Ansatz nicht benötigt. Man könnte einfach eine Variable aktualisieren:

$ n=1; while true; do n=$(echo "1+s(3*$n)" | bc -l); echo $n; sleep 1; done
1.14112000805986722210
.72194624281527439351
1.82812473159858353270
.28347272185896349481
[..snip...]
John1024
quelle
12

Hierfür können Sie ein FIFO verwenden, das mit erstellt wurde mkfifo. Beachten Sie jedoch, dass es sehr leicht ist, versehentlich einen Deadlock zu erstellen. Lassen Sie mich das erklären - nehmen Sie Ihr hypothetisches "kreisförmiges" Beispiel. Sie führen die Ausgabe eines Befehls seiner Eingabe zu. Es gibt mindestens zwei Möglichkeiten, wie dies zum Stillstand kommen kann:

  1. Der Befehl hat einen Ausgabepuffer. Es ist teilweise gefüllt, wurde aber noch nicht gelöscht (tatsächlich geschrieben). Sobald es voll ist, wird es es tun. Es wird also wieder die Eingabe gelesen. Es bleibt für immer dort, denn die Eingabe, auf die es wartet, befindet sich tatsächlich im Ausgabepuffer. Und es wird nicht geleert, bis es diese Eingabe bekommt ...

  2. Der Befehl hat eine Reihe von Ausgaben zu schreiben. Es beginnt mit dem Schreiben, aber der Kernel-Pipe-Puffer füllt sich. Also sitzt es da und wartet darauf, dass sie Platz im Puffer haben. Das wird passieren, sobald es seine Eingabe liest, dh niemals, weil es das nicht tun wird, bis es fertig ist, was auch immer an seine Ausgabe geschrieben wird.

Das heißt, hier ist, wie Sie es tun. Dieses Beispiel ist mit od, um eine endlose Kette von Hex-Dumps zu erstellen:

mkfifo fifo
( echo "we need enough to make it actually write a line out"; cat fifo ) \ 
    | stdbuf -i0 -o0 -- od -t x1 | tee fifo

Beachten Sie, dass schließlich aufhört. Warum? Es kam zum Stillstand, Nr. 2 oben. Möglicherweise bemerken Sie dort auch den stdbufAufruf, um die Pufferung zu deaktivieren. Ohne es? Deadlocks ohne Ausgabe.

derobert
quelle
Danke, ich wusste in diesem Zusammenhang nichts über Puffer. Kennen Sie einige Schlüsselwörter, um mehr darüber zu erfahren?
Abdul Al Hazred
1
@AbdulAlHazred Suchen Sie zum Puffern der Ein- / Ausgabe nach stdio buffering . Für den Kernel-Puffer in einer Pipe scheint der Pipe-Puffer zu funktionieren.
Derobert
4

Im Allgemeinen würde ich ein Makefile (Befehl make) verwenden und versuchen, Ihr Diagramm Makefile-Regeln zuzuordnen.

f1 f2 : f0
      command < f0 > f1 2>f2

Um sich wiederholende / zyklische Befehle zu haben, müssen wir eine Iterationsrichtlinie definieren. Mit:

SHELL=/bin/bash

a.out : accumulator
    cat accumulator <(date) > a.out
    cp a.out accumulator

accumulator:
    touch accumulator     #initial value

Jedes makewird jeweils eine Iteration erzeugen.

Joao
quelle
Netter Missbrauch von make, aber unnötig: Wenn Sie eine Zwischendatei verwenden, warum nicht einfach eine Schleife verwenden, um sie zu verwalten?
Alexis
@alexis, makefiles ist wahrscheinlich übertrieben. Schleifen gefallen mir nicht besonders gut: Ich vermisse den Begriff der Uhr, der Haltebedingung oder ein klares Beispiel. Die ersten Diagramme erinnerten mich an Workflows und Funktionssignaturen. Für komplexe Diagramme benötigen wir Datenverbindungen oder makefile-typisierte Regeln. (Dies ist nur eine missbräuchliche Intuition)
JJoao
@alexis, und natürlich stimme ich Ihnen zu.
JJoao
Ich denke nicht, dass dies Missbrauch ist - es makegeht um Makros, die hier eine perfekte Anwendung sind.
mikeserv
1
@mikeserv, ja. Und wir alle wissen, dass der Missbrauch von Werkzeugen die unterirdische Magna Carta von Unix ist :)
JJoao
4

Wissen Sie, ich bin nicht davon überzeugt, dass Sie unbedingt eine sich wiederholende Rückkopplungsschleife benötigen, wenn Ihre Diagramme dargestellt werden. Vielleicht können Sie sogar eine dauerhafte Pipeline zwischen Koprozessen verwenden . Andererseits kann es sein, dass es keinen allzu großen Unterschied gibt - sobald Sie eine Zeile in einem Coprozess öffnen, können Sie typische Stilschleifen implementieren, in die nur Informationen geschrieben und daraus gelesen werden, ohne dass etwas ganz Besonderes getan wird.

Zunächst scheint es, dass dies bcfür Sie ein Hauptkandidat für einen Koprozess ist. In können bcSie defineFunktionen, die so ziemlich das tun können, was Sie in Ihrem Pseudocode verlangen. Zum Beispiel könnten einige sehr einfache Funktionen dazu so aussehen:

printf '%s()\n' b c a |
3<&0 <&- bc -l <<\IN <&3
a=1; b=0; c=0;
define a(){ "a="; return (a = c+1); }
define b(){ "b="; return (b = 3*a); }
define c(){ "c="; return (c = s(b)); }
IN

... was würde drucken ...

b=3
c=.14112000805986722210
a=1.14112000805986722210

Aber natürlich tut es nicht zuletzt . Sobald die für die Pipe zuständige Unterschale printfbeendet wird (direkt nach dem printfSchreiben a()\nin die Pipe), wird die Pipe abgerissen und bcdie Eingabe wird geschlossen und sie wird ebenfalls beendet. Das ist bei weitem nicht so nützlich, wie es sein könnte.

@derobert hat bereits erwähnten FIFO s , wie sie durch die Schaffung eines gehabt werden Named Pipe mit der Datei - mkfifoDienstprogramm. Dies sind im Wesentlichen auch nur Pipes, mit der Ausnahme, dass der Systemkern einen Dateisystemeintrag mit beiden Enden verbindet. Diese sind sehr nützlich, aber es wäre schöner, wenn Sie nur eine Pipe hätten, ohne zu riskieren, dass sie im Dateisystem beschnüffelt wird.

Wie es passiert, macht Ihre Shell dies oft. Wenn Sie eine Shell verwenden, die die Prozessersetzung implementiert , haben Sie ein sehr einfaches Mittel, um eine dauerhafte Pipe zu erhalten - die Art, die Sie möglicherweise einem Hintergrundprozess zuweisen, mit dem Sie kommunizieren können.

In bashzum Beispiel können Sie sehen , wie die Prozess Substitution Werke:

bash -cx ': <(:)'
+ : /dev/fd/63

Sie sehen, es ist wirklich eine Substitution . Die Shell ersetzt während der Expansion einen Wert, der dem Pfad zu einer Verbindung zu einer Pipe entspricht . Sie können das ausnutzen - Sie müssen nicht gezwungen sein, diese Pipe nur zu verwenden, um mit dem Prozess zu kommunizieren, der in der ()Substitution selbst abläuft ...

bash -c '
    eval "exec 3<>"<(:) "4<>"<(:)
    cat  <&4 >&3  &
    echo hey cat >&4
    read hiback  <&3
    echo "$hiback" here'

... was druckt ...

hey cat here

Jetzt weiß ich, dass verschiedene Shells das Coprozess- Ding auf unterschiedliche Weise ausführen - und dass es eine bestimmte Syntax bashfür das Einrichten eines (und wahrscheinlich auch einer zsh) gibt -, aber ich weiß nicht, wie diese Dinge funktionieren. Ich weiß nur, dass Sie die obige Syntax verwenden können, um praktisch dasselbe zu tun, ohne das Rigmarole in beiden bashund zsh- und Sie können eine sehr ähnliche Sache in hier-Dokumenten tundash und busybox ashden gleichen Zweck mit hier-Dokumenten erreichen (weil dashund busyboxhier- Dokumente mit Pipes anstelle von temporären Dateien (wie die beiden anderen) .

Also, wenn angewendet auf bc...

eval "exec 3<>"<(:) "4<>"<(:)
bc -l <<\INIT <&4 >&3 &
a=1; b=0; c=0;
define a(){ "a="; return (a = c+1); }
define b(){ "b="; return (b = 3*a); }
define c(){ "c="; return (c = s(b)); }
INIT
export BCOUT=3 BCIN=4 BCPID="$!"

... das ist der schwierige Teil. Und das ist der lustige Teil ...

set --
until [ "$#" -eq 10 ]
do    printf '%s()\n' b c a >&"$BCIN"
      set "$@" "$(head -n 3 <&"$BCOUT")"
done; printf %s\\n "$@"

... was druckt ...

b=3
c=.14112000805986722210
a=1.14112000805986722210
#...24 more lines...
b=3.92307618030433853649
c=-.70433330413228041035
a=.29566669586771958965

... und es läuft noch ...

echo a >&"$BCIN"
read a <&"$BCOUT"
echo "$a"

... was mir nur den letzten Wert für bc's gibt, aanstatt die a()Funktion aufzurufen , um ihn zu erhöhen und zu drucken ...

.29566669586771958965

Es wird in der Tat so lange weiterlaufen, bis ich es töte und seine IPC-Rohre abreiße ...

kill "$BCPID"; exec 3>&- 4>&-
unset BCPID BCIN BCOUT
mikeserv
quelle
1
Sehr interessant. Beachten Sie, dass Sie bei der letzten Bash und Zsh nicht den Dateideskriptor angeben müssen, z. B. eval "exec {BCOUT}<>"<(:) "{BCIN}<>"<(:)funktioniert dies auch
Thor,