Come posso implementare un flusso circolare di dati tra comandi interconnessi?


19

Conosco due tipi di come i comandi possono essere collegati tra loro:

  1. usando un Pipe (inserendo std-output in std-input del comando successivo).
  2. usando un T (unisci l'output in molti output).

Non so se sia tutto ciò che è possibile, quindi disegno un ipotetico tipo di connessione:

inserisci qui la descrizione dell'immagine

Come potrebbe essere possibile implementare un flusso circolare di dati tra comandi come, ad esempio, in questo pseudo codice, dove uso variabili anziché comandi:

pseudo-code:

a = 1    # start condition 

repeat 
{
b = tripple(a)
c = sin(b) 
a = c + 1 
}

Risposte:


16

Circuito I / O circolare implementato con tail -f

Questo implementa un loop I / O circolare:

$ 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...]

Questo implementa il ciclo circolare di input / output usando l'algoritmo sinusoidale che hai citato:

$ 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...]

Qui, bcfa la matematica in virgola mobile ed s(...)è la notazione di bc per la funzione seno.

Implementazione dello stesso algoritmo utilizzando invece una variabile

Per questo particolare esempio matematico, l'approccio I / O circolare non è necessario. Si potrebbe semplicemente aggiornare una variabile:

$ 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...]

12

È possibile utilizzare un FIFO per questo, creato con mkfifo. Si noti tuttavia che è molto facile creare accidentalmente un deadlock. Lascia che te lo spieghi: prendi il tuo ipotetico esempio "circolare". Dai l'output di un comando al suo input. Esistono almeno due modi in cui questo potrebbe bloccarsi:

  1. Il comando ha un buffer di output. È parzialmente riempito, ma non è stato ancora scaricato (in realtà scritto). Lo farà una volta riempito. Quindi torna a leggere il suo input. Rimarrà lì per sempre, perché l'input che sta aspettando è in realtà nel buffer di output. E non verrà scaricato fino a quando non avrà quell'input ...

  2. Il comando ha un sacco di output da scrivere. Inizia a scriverlo, ma il buffer della pipe del kernel si riempie. Quindi sta lì, in attesa che si trovino spazio nel buffer. Ciò accadrà non appena leggerà il suo input, cioè, mai perché non lo farà fino a quando non finirà di scrivere qualsiasi cosa sul suo output.

Detto questo, ecco come lo fai. Questo esempio è con od, per creare una catena infinita di dump esadecimali:

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

Si noti che alla fine si ferma. Perché? Si è bloccato, n. 2 sopra. Puoi anche notare la stdbufchiamata lì dentro, per disabilitare il buffering. Senza esso? Deadlock senza output.


grazie, non sapevo nulla dei buffer in questo contesto, conosci alcune parole chiave per saperne di più?
Abdul Al Hazred,

1
@AbdulAlHazred Per il buffering di input / output, cercare il buffering stdio . Per il buffer del kernel in una pipe, il buffer della pipe sembra funzionare.
derobert

4

In generale, userei un Makefile (comando make) e proverei a mappare il diagramma per creare le regole del makefile.

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

Per avere comandi ripetitivi / ciclici, è necessario definire una politica di iterazione. Con:

SHELL=/bin/bash

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

accumulator:
    touch accumulator     #initial value

ognuno makeprodurrà una iterazione alla volta.


Simpatico abuso di make, ma non necessario: se si utilizza un file intermedio, perché non utilizzare un loop per gestirlo?
alexis

@alexis, makefiles è probabilmente eccessivo. Non mi sento a mio agio con i loop: mi manca la nozione di orologio, condizioni di arresto o un chiaro esempio. I diagrammi iniziali mi hanno ricordato Flussi di lavoro e firme delle funzioni. Per diagrammi complessi finiremo per aver bisogno di connessioni di dati o e regole tipizzate per makefile. (questa è solo un'intuizione abusiva)
JJoao,

@alexis, e ovviamente sono d'accordo con te.
JJoao,

Non penso che questo sia un abuso, makeriguarda le macro che è un'applicazione perfetta qui.
Mikeserv,

1
@mikeserv, Sì. E sappiamo tutti che abusare degli strumenti è la Magna Carta sotterranea di Unix :)
JJoao

4

Sai, non sono convinto che tu abbia necessariamente bisogno di un ciclo di feedback ripetitivo come mostrano i tuoi diagrammi, per quanto forse potresti usare una pipeline persistente tra i coprocessi . Inoltre, potrebbe non esserci troppa differenza: una volta aperta una riga su un coprocesso è possibile implementare cicli di stile tipici semplicemente scrivendo informazioni e leggendo informazioni da essa senza fare nulla di straordinario.

In primo luogo, sembrerebbe che bcsia il candidato principale per un coprocesso per te. In bcte puoi definefunzioni che possono fare praticamente ciò che chiedi nel tuo pseudocodice. Ad esempio, alcune funzioni molto semplici per farlo potrebbero apparire come:

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

... che stamperebbe ...

b=3
c=.14112000805986722210
a=1.14112000805986722210

Ma ovviamente non dura . Non appena la sottostruttura responsabile della printfpipe si chiude (subito dopo printfscrive a()\nsulla pipe) la pipe viene abbattuta e bcl'input si chiude e si chiude anche. Non è così utile come potrebbe essere.

@derobert ha già menzionato FIFO come si può creare creando un file pipe con l' mkfifoutilità. Questi sono essenzialmente solo pipe, tranne per il fatto che il kernel di sistema collega una voce del filesystem ad entrambe le estremità. Questi sono molto utili, ma sarebbe meglio se si potesse semplicemente avere una pipe senza rischiare che venisse curiosata nel filesystem.

In effetti, la tua shell lo fa molto. Se usi una shell che implementa la sostituzione dei processi, allora hai un mezzo molto semplice per ottenere una pipe duratura - del tipo che potresti assegnare a un processo in background con cui puoi comunicare.

Ad bashesempio, puoi vedere come funziona la sostituzione del processo:

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

Vedi che è davvero una sostituzione . La shell sostituisce un valore durante l'espansione che corrisponde al percorso di un collegamento a una pipe . Puoi trarne vantaggio - non devi essere costretto a usare quella pipe solo per comunicare con qualunque processo venga eseguito all'interno della ()sostituzione stessa ...

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

... che stampa ...

hey cat here

Ora so che diverse shell eseguono il processo di coprocesso in modi diversi - e che esiste una sintassi specifica bashper configurarne uno (e probabilmente anche uno zsh) - ma non so come funzionano queste cose. So solo che puoi usare la sintassi di cui sopra per fare praticamente la stessa cosa senza tutto il rigmarole in entrambi bashe zsh- e puoi fare una cosa molto simile dashe busybox ashraggiungere lo stesso scopo con i documenti qui (perché dashe busyboxfare qui- documenti con pipe anziché file temporanei come fanno gli altri due) .

Quindi, quando applicato a 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="$!"

... questa è la parte difficile. E questa è la parte divertente ...

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

... che stampa ...

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

... e funziona ancora ...

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

... che mi dà solo l'ultimo valore per bc" apiuttosto che chiamare la a()funzione per incrementarla e stampare ...

.29566669586771958965

Continuerà a funzionare, infatti, fino a quando non lo ucciderò e abbatterò i suoi tubi IPC ...

kill "$BCPID"; exec 3>&- 4>&-
unset BCPID BCIN BCOUT

1
Molto interessante. Nota con bash e zsh recenti non è necessario specificare il descrittore di file, ad es. eval "exec {BCOUT}<>"<(:) "{BCIN}<>"<(:)Funziona anche
Thor
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.