Como posso implementar um fluxo circular de dados entre comandos interconectados?

19

Conheço dois tipos de como os comandos podem ser conectados:

  1. usando um Pipe (colocando std-output na std-input do próximo comando).
  2. usando um Tee (divida a saída em várias saídas).

Eu não sei se isso é tudo o que é possível, então eu desenho um tipo de conexão hipotética:

insira a descrição da imagem aqui

Como seria possível implementar um fluxo circular de dados entre comandos como, por exemplo, neste pseudo-código, onde eu uso variáveis ​​em vez de comandos .:

pseudo-code:

a = 1    # start condition 

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

Respostas:

16

Loop de E / S circular implementado com tail -f

Isso implementa um loop 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...]

Isso implementa o loop circular de entrada / saída usando o algoritmo senoidal que você mencionou:

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

Aqui, bcfaz a matemática do ponto flutuante e s(...)é a notação de bc para a função seno.

Implementação do mesmo algoritmo usando uma variável

Para este exemplo matemático específico, a abordagem de E / S circular não é necessária. Pode-se simplesmente atualizar uma variável:

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

Você pode usar um FIFO para isso, criado com mkfifo. Observe, no entanto, que é muito fácil criar acidentalmente um impasse. Deixe-me explicar isso - tome seu exemplo hipotético "circular". Você alimenta a saída de um comando para sua entrada. Há pelo menos duas maneiras pelas quais isso pode gerar um conflito:

  1. O comando possui um buffer de saída. Está parcialmente preenchido, mas ainda não foi liberado (realmente escrito). Isso será feito assim que for preenchido. Então, ele volta a ler sua entrada. Ele ficará lá para sempre, porque a entrada que está esperando está realmente no buffer de saída. E não será liberado até receber essa entrada ...

  2. O comando tem um monte de saída para escrever. Ele começa a escrevê-lo, mas o buffer do pipe do kernel é preenchido. Por isso, fica lá, esperando que haja espaço no buffer. Isso acontecerá assim que ele ler sua entrada, ou seja, nunca fará isso até terminar de escrever o que quer que seja para sua saída.

Dito isto, aqui está como você faz isso. Este exemplo é com od, para criar uma cadeia interminável de dumps hexadecimais:

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

Observe que eventualmente pára. Por quê? É um impasse, nº 2 acima. Você também pode perceber a stdbufchamada, para desativar o buffer. Sem ele? Deadlocks sem saída.

derobert
fonte
obrigado, eu não sabia nada sobre buffers nesse contexto, você conhece algumas palavras-chave para ler mais sobre isso?
Abdul Al Hazred
1
@AbdulAlHazred Para armazenar buffer de entrada / saída, consulte o buffer do stdio . Para o buffer do kernel em um pipe, o buffer do pipe parece funcionar.
30515 derobert
4

Em geral, eu usaria um Makefile (comando make) e tentaria mapear seu diagrama para regras de makefile.

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

Para ter comandos repetitivos / cíclicos, precisamos definir uma política de iteração. Com:

SHELL=/bin/bash

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

accumulator:
    touch accumulator     #initial value

cada makeum produzirá uma iteração por vez.

JJoao
fonte
Abuso bonito de make, mas desnecessário: se você usa um arquivo intermediário, por que não usar apenas um loop para gerenciá-lo?
314 alexis
@alexis, makefiles provavelmente é um exagero. Não me sinto muito à vontade com loops: sinto falta da noção de relógio, condição de parada ou um exemplo claro. Os diagramas iniciais lembram-me Fluxos de trabalho e assinaturas de funções. Para diagramas complexos, acabaremos precisando de conexões de dados ou regras de tipo makefile. (isto é apenas uma intuição abusivo)
JJoao
@alexis e, claro, concordo com você.
31415
Eu não acho que isso seja abuso - maketrata-se de macros, que é uma aplicação perfeita aqui.
mikeserv
1
@mikeserv, Sim. E todos nós sabemos que abusar de ferramentas é o metro Magna Carta de Unix :)
JJoao
4

Você sabe, não estou convencido de que você precise necessariamente de um ciclo de feedback repetitivo, como retratam seus diagramas, tanto quanto talvez você possa usar um pipeline persistente entre os coprocessos . Por outro lado, pode ser que não exista muita diferença - uma vez que você abre uma linha em um coprocessador, pode implementar loops típicos de estilo, escrevendo e lendo informações e lendo informações sem fazer nada fora do comum.

Em primeiro lugar, parece que você bcé o principal candidato a um co-processo. Em bcvocê pode definefunções que podem fazer praticamente o que você pede em seu pseudocódigo. Por exemplo, algumas funções muito simples para fazer isso podem parecer:

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

... o que imprimiria ...

b=3
c=.14112000805986722210
a=1.14112000805986722210

Mas é claro que não dura . Assim que o subconjunto responsável printfpelo tubo do encerra (logo após a printfgravação a()\nno tubo), o tubo é desmontado e bca entrada é fechada e também é encerrada. Isso não é tão útil quanto poderia ser.

O @derobert já mencionou FIFO s, como pode ser obtido criando um arquivo de pipe nomeado com o mkfifoutilitário. Também são essencialmente apenas canais, exceto que o kernel do sistema vincula uma entrada do sistema de arquivos às duas extremidades. Isso é muito útil, mas seria melhor se você pudesse ter um canal sem correr o risco de ser espionado no sistema de arquivos.

Por acaso, seu shell faz muito isso. Se você usa um shell que implementa a substituição do processo , você tem um meio muito simples de obter um canal duradouro - do tipo que você pode atribuir a um processo em segundo plano com o qual você pode se comunicar.

Em bash, por exemplo, você pode ver como a substituição do processo funciona:

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

Você vê que é realmente uma substituição . O shell substitui um valor durante a expansão que corresponde ao caminho para um link para um pipe . Você pode tirar proveito disso - você não precisa ser obrigado a usar esse canal apenas para se comunicar com qualquer processo executado dentro da ()própria substituição ...

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

... que imprime ...

hey cat here

Agora eu sei que diferentes shells executam o processo de coprocessamento de maneiras diferentes - e que existe uma sintaxe específica bashpara configurar uma (e provavelmente uma zshtambém) - mas não sei como essas coisas funcionam. Eu apenas sei que você pode usar a sintaxe acima para fazer praticamente a mesma coisa sem todo o rigmarole em ambos bashe zsh- e você pode fazer uma coisa muito semelhante dashe busybox ashalcançar o mesmo objetivo com os documentos aqui (porque dashe busyboxfaça aqui- documentos com tubos em vez de arquivos temporários, como os outros dois) .

Então, quando aplicado 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="$!"

... essa é a parte mais difícil. E esta é a 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

... e ainda está em execução ...

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

... o que me dá o último valor de bc, em avez de chamar a a()função para incrementá-lo e imprimi-lo ...

.29566669586771958965

De fato, ele continuará funcionando até que eu o mate e destrua os canos do IPC ...

kill "$BCPID"; exec 3>&- 4>&-
unset BCPID BCIN BCOUT
mikeserv
fonte
1
Muito interessante. Observe que com o bash e o zsh recentes você não precisa especificar o descritor de arquivo, por exemplo, eval "exec {BCOUT}<>"<(:) "{BCIN}<>"<(:)funciona também
Thor