¿Cómo puedo implementar un flujo circular de datos entre comandos interconectados?

19

Sé de dos tipos cómo se pueden conectar los comandos entre sí:

  1. mediante el uso de una tubería (poner salida estándar en entrada estándar del siguiente comando).
  2. mediante el uso de una T (empalme la salida en muchas salidas).

No sé si eso es todo lo que es posible, así que dibujo un tipo de conexión hipotética:

ingrese la descripción de la imagen aquí

¿Cómo podría ser posible implementar un flujo circular de datos entre comandos como, por ejemplo, en este pseudocódigo, donde uso variables en lugar de comandos .:

pseudo-code:

a = 1    # start condition 

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

Respuestas:

16

Bucle circular de E / S implementado con tail -f

Esto implementa un ciclo circular de E / S:

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

Esto implementa el ciclo circular de entrada / salida utilizando el algoritmo seno que mencionaste:

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

Aquí, bchace los cálculos de coma flotante y s(...)es la notación de bc para la función seno.

Implementación del mismo algoritmo usando una variable en su lugar

Para este ejemplo matemático en particular, no se necesita el enfoque circular de E / S. Uno podría simplemente actualizar una variable:

$ 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
fuente
12

Puede usar un FIFO para esto, creado con mkfifo. Sin embargo, tenga en cuenta que es muy fácil crear accidentalmente un punto muerto. Déjame explicarte eso: toma tu ejemplo hipotético "circular". Alimenta la salida de un comando a su entrada. Hay al menos dos formas en que esto podría llegar a un punto muerto:

  1. El comando tiene un búfer de salida. Está parcialmente lleno, pero aún no se ha lavado (escrito). Lo hará una vez que se llene. Entonces vuelve a leer su entrada. Se quedará allí para siempre, porque la entrada que está esperando está realmente en el búfer de salida. Y no se vaciará hasta que reciba esa entrada ...

  2. El comando tiene un montón de resultados para escribir. Comienza a escribirlo, pero el búfer de la tubería del núcleo se llena. Entonces se sienta allí, esperando que haya espacio en el búfer. Eso sucederá tan pronto como lea su entrada, es decir, nunca, ya que no va a hacer eso hasta que termine de escribir lo que sea en su salida.

Dicho esto, así es como lo haces. Este ejemplo es con od, para crear una cadena interminable de volcados hexadecimales:

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

Tenga en cuenta que finalmente se detiene. ¿Por qué? Se estancó, # 2 arriba. También puede notar la stdbufllamada allí, para deshabilitar el almacenamiento en búfer. ¿Sin ello? Puntos muertos sin salida.

derobert
fuente
gracias, no sabía nada sobre buffers en este contexto, ¿conoces algunas palabras clave para leer más al respecto?
Abdul Al Hazred
1
@AbdulAlHazred Para el almacenamiento en búfer de entrada / salida, busque el almacenamiento en búfer estándar . Para el búfer del núcleo en una tubería, el búfer de tubería parece funcionar.
derobert
4

En general, usaría un Makefile (comando make) y trataría de mapear su diagrama a las reglas de makefile.

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

Para tener comandos repetitivos / cíclicos, necesitamos definir una política de iteración. Con:

SHELL=/bin/bash

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

accumulator:
    touch accumulator     #initial value

cada makeuno producirá una iteración a la vez.

JJoao
fuente
Abuso lindo make, pero innecesario: si usa un archivo intermedio, ¿por qué no usar un bucle para administrarlo?
alexis
@alexis, los makefiles probablemente sean excesivos. No me siento muy cómodo con los bucles: extraño la noción de reloj, la condición de detención o un ejemplo claro. Los diagramas iniciales me recordaron los flujos de trabajo y las firmas de funciones. Para diagramas complejos, terminaremos necesitando conexiones de datos o reglas de tipo makefile. (esto es solo una intuición abusiva)
JJoao
@alexis, y por supuesto, estoy de acuerdo contigo.
JJoao
No creo que esto sea abuso, makese trata de macros, que es una aplicación perfecta aquí.
mikeserv
1
@mikeserv, sí. Y todos sabemos que abusar de las herramientas es la Carta Magna subterránea de Unix :)
JJoao
4

Sabes, no estoy convencido de que necesariamente necesites un ciclo de retroalimentación repetitiva como lo muestran tus diagramas, por mucho que tal vez puedas usar una tubería persistente entre coprocesos . Por otra parte, puede ser que no haya demasiada diferencia: una vez que abre una línea en un coproceso, puede implementar bucles de estilo típicos simplemente escribiendo información y leyendo información de ella sin hacer nada fuera de lo común.

En primer lugar, parecería que bces un candidato principal para un coproceso para usted. En bcusted puede definefunciones que pueden hacer más o menos lo que pide en su pseudocódigo. Por ejemplo, algunas funciones muy simples para hacer esto podrían ser:

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

... que imprimiría ...

b=3
c=.14112000805986722210
a=1.14112000805986722210

Pero, por supuesto, no dura . Tan pronto como el subshell a cargo de printfla tubería se cierra (justo después de printfescribir a()\nen la tubería), la tubería se rompe y bcla entrada se cierra y también se cierra. Eso no es tan útil como podría ser.

@derobert ya ha mencionado FIFO como se puede obtener al crear un archivo de canalización con nombre con la mkfifoutilidad. Estos son esencialmente solo tuberías, excepto que el núcleo del sistema vincula una entrada del sistema de archivos a ambos extremos. Estos son muy útiles, pero sería mejor si pudieras tener una tubería sin correr el riesgo de que se espíe en el sistema de archivos.

Resulta que tu caparazón lo hace mucho. Si usa un shell que implementa la sustitución de procesos, entonces tiene un medio muy sencillo de obtener una tubería duradera , del tipo que podría asignar a un proceso en segundo plano con el que puede comunicarse.

En bash, por ejemplo, puede ver cómo funciona la sustitución del proceso:

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

Ves que realmente es una sustitución . El shell sustituye un valor durante la expansión que corresponde a la ruta a un enlace a una tubería . Puede aprovechar eso: no necesita limitarse a usar esa tubería solo para comunicarse con cualquier proceso que se ejecute dentro de la ()sustitución misma ...

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

... que imprime ...

hey cat here

Ahora sé que diferentes shells hacen el coprocesamiento de diferentes maneras, y que hay una sintaxis específica bashpara configurar uno (y probablemente también para uno zsh) , pero no sé cómo funcionan esas cosas. Solo sé que puede usar la sintaxis anterior para hacer prácticamente lo mismo sin todo el rigmarole en ambos bashy zsh, y puede hacer algo muy similar dashy busybox ashlograr el mismo propósito con documentos aquí (porque dashy busyboxhacer aquí) documentos con tuberías en lugar de archivos temporales como lo hacen los otros dos) .

Entonces, cuando se aplica 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="$!"

... esa es la parte difícil. Y esta es la parte divertida ...

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

... que imprime ...

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

... y sigue funcionando ...

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

... lo que me da el último valor para bc's en alugar de llamar a la a()función para incrementarlo e imprime ...

.29566669586771958965

Continuará funcionando, de hecho, hasta que lo mate y derribe sus tuberías IPC ...

kill "$BCPID"; exec 3>&- 4>&-
unset BCPID BCIN BCOUT
mikeserv
fuente
1
Muy interesante. Tenga en cuenta que con bash y zsh recientes no tiene que especificar el descriptor de archivo, por ejemplo eval "exec {BCOUT}<>"<(:) "{BCIN}<>"<(:), también funciona
Thor