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

18

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

  1. usando um Pipe (colocando std-output em std-input do próximo     comando).
  2. usando um Tee (junte 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:

Como é 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 
}
    
por Abdul Al Hazred 30.03.2015 / 20:43

4 respostas

15

Circular E / S Loop implementado com tail -f

Isso implementa um loop de E / S circular:

$ 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 seno 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, bc faz 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 específico de matemática, 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...]
    
por 30.03.2015 / 20:51
11

Você pode usar um FIFO para isso, criado com mkfifo . Note, entretanto, que é muito fácil criar acidentalmente um deadlock. Deixe-me explicar isso - pegue seu exemplo hipotético de "circular". Você alimenta a saída de um comando para sua entrada. Existem pelo menos duas maneiras que isso pode travar:

  1. O comando tem um buffer de saída. Está parcialmente preenchido, mas ainda não foi liberado (na verdade escrito). Isso será feito assim que for preenchido. Então, 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 kernel é preenchido. Então fica lá, esperando que o espaço deles esteja no buffer. Isso acontecerá assim que ele ler sua entrada, ou seja, nunca, como não vai fazer 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

Note que, eventualmente, pára. Por quê? Está em deadlock, # 2 acima. Você também pode notar a chamada stdbuf lá, para desativar o buffer. Sem isso? Deadlocks sem qualquer saída.

    
por 30.03.2015 / 21:26
3

Em geral, eu usaria um Makefile (comando make) e tentaria mapeie 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 make produzirá uma iteração no momento.

    
por 30.03.2015 / 21:49
3

Você sabe, não estou convencido de que você precisa necessariamente de um loop de feedback repetitivo como seus diagramas retratam, talvez você possa usar um pipeline persistente entre coprocessos . Então, novamente, pode ser que não exista muita diferença - uma vez que você abre uma linha em um coprocess, você pode implementar loops de estilo típicos apenas escrevendo informações e lendo informações dele sem fazer nada muito fora do comum.

Em primeiro lugar, parece que bc é um candidato ideal para um coprocessamento para você. Em bc você pode define funções que podem fazer praticamente o que você pede no 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, isso não dura . Assim que o subshell encarregado do pipe printf terminar (logo após printf gravar a()\n no pipe) o pipe será demolido e a entrada bc fechará também sai. Isso não é tão útil quanto poderia ser.

@derobert já mencionou FIFO s como pode ser obtido criando um arquivo pipe nomeado com o utilitário mkfifo . Estes são essencialmente apenas pipes, exceto que o kernel do sistema liga uma entrada do sistema de arquivos para ambas as extremidades. Estes são muito úteis, mas seria melhor se você pudesse apenas ter um pipe sem correr o risco de ser bisbilhotado no sistema de arquivos.

Quando isso acontece, seu shell faz muito isso. Se você usa um shell que implementa substituição de processo , você tem um meio muito direto de obter um canal duradouro - do tipo que você pode atribuir a um processo em segundo plano com o qual 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 pipe 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 fazem o coprocess de diferentes maneiras - e que existe uma sintaxe específica em bash para configurar um (e provavelmente um para zsh também) - mas eu não sei como essas coisas funcionam. Eu só sei que você pode usar a sintaxe acima para fazer praticamente a mesma coisa sem toda a ladainha em bash e zsh - e você pode fazer uma coisa muito semelhante em dash e busybox ash para alcançar o mesma finalidade com here-documents (porque dash e busybox fazem aqui - documentos com pipes em vez de arquivos temporários como os outros dois fazem) .

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 difícil. E essa é 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"

... que apenas me pega o último valor para bc ' a ao invés de chamar a função a() para incrementá-la e imprimir ...

.29566669586771958965

Ele continuará a rodar, na verdade, até que eu o mate e derrube seus tubos IPC ...

kill "$BCPID"; exec 3>&- 4>&-
unset BCPID BCIN BCOUT
    
por 31.03.2015 / 09:39