UNIX Pipes

Para entendermos UNIX pipes, devemos primeiro recapitular o seguinte exemplo:

$ echo 'my precious' > rawcontent.txt
$ base64 < rawcontent.txt
bXkgcHJlY2lvdXMK
  • O programa echo redireciona a o output para o arquivo rawcontent.txt

  • O conteúdo do arquivo é enviado como input para o comando base64

Note o padrão aqui: temos uma pipeline de transformação de dados, onde o output de um programa é utilizado como input para o próximo programa.

Esta técnica de pipeline é utilizada em sistemas UNIX-like com o operador pipe |.

UNIX pipelines

Ao invés de escrevemos nossa pipeline em múltiplas linhas, dificultando a legibilidade caso a complexidade aumente, podemos utilizar o operador | para montar uma sentença única de comandos encadeados:

$ echo 'my precious' | base64
bXkgcHJlY2lvdXMK

Muito melhor, não? E muito provável você já viu isto em algum lugar, por exemplo:

$ ps ax | grep ruby
88327 s002  S+     0:00.94 docker run -it ruby irb
88330 s002  S+     0:00.92 /usr/local/bin/com.docker.cli run -it ruby irb
91074 s003  S+     0:00.00 grep ruby

O output do comando ps é enviado como input para o comando grep. Cool, uh? Este pipe é chamado de pipe anônimo, ou anonymous pipe!

Anonymous pipe

Este pipe é chamado de anônimo justamente por não ter nome e ser temporário, pois o fd é criado durante a pipeline e depois é liberado.

Este tipo de IPC utiliza uma comunicação tem as seguintes características:

  • One-way, ou seja, a informação trafega apenas e uma única direção

  • FIFO (first-in, first-out), ou seja, o output é redirecionado para um pipe e enfileirado como input em outro pipe do próximo comando

$ ps ax | grep docker | tail -n 3

62374 s039  S+     0:05.31 /usr/local/bin/com.docker.cli run -it ubuntu bash
65442 s040  S+     0:02.93 docker run -it ubuntu bash
65445 s040  S+     0:02.86 /usr/local/bin/com.docker.cli run -it ubuntu bash

Quando um | é criado, abre-se um par de file descriptors, uma para escrita e outro para leitura, tal como fizemos na seção anterior com custom fd.

Como a pipe é anônima, ambos file descriptors abertos são utilizados apenas no contexto da pipeline e são automaticamente liberados/fechados quando a pipeline termina.

Apesar de pipes anônimas | serem utilizadas praticamente em quase tudo, é possível criamos pipes com nomes?

Named pipes

Como o próprio nome diz, named pipes são pipes com nomes. São similares a pipes anônimas; empregam FIFO e são uma forma de IPC de via única (one-way).

A única diferença é que um named pipe é criado de forma explícita via comando mkfifo, onde um arquivo é criado no filesystem e aberto para escrita e leitura.

$ mkfifo myqueue

Um arquivo chamado myqueue é criado.

Vamos enviar uma mensagem para o pipe com o comando echo, utilizando redirecionamento de stream > que vimos na última seção:

$ echo 'my precious' > myqueue 

Note que o processo fica bloqueado, à espera de algo.

IPC one-way

Por ser uma estrutura de fila FIFO simples e ser utilizado como one-way IPC, o sistema operacional precisa garantir que a mensagem será recebida por outro processo. Por isso o processo escritor, ou writer, fica bloqueado, pois é preciso que outro processo outro processo leitor (reader) para "consumir" a mensagem do pipe.

Em outra sessão do bash, vamos consumir a informação do pipe utilizando o comando cat:

$ cat myqueue
my precious

Yay!

O mesmo acontece se iniciarmos com o leitor: este fica bloqueado à espera que alguma mensagem chegue no pipe, no caso através de outro processo escritor.

Implementando um simples Background Job com UNIX pipes

Utilizando anonymous pipes e named pipes, podemos explorar a funcionalidade primitiva de um sistema de processamento assíncrono (background job).

Começamos por definir os componentes:

  1. Um processo leitor, ou consumer, fica infinitamente à espera de mensagens no pipe (fila)

  2. Diferentes escritores, ou publishers, colocam mensagens no pipe de forma assíncrona

  3. O processo leitor (consumer) recebe cada mensagem no pipe e faz o devido processamento da mensagem

👉 Nosso background job irá fazer a simples tarefa de receber uma mensagem codificada em base64, decodificá-la, e então mostrar no screen (STDOUT).

Consumer

Primeiro, criamos o pipe que representará a "fila" do nosso background job:

mkfifo myqueue

Agora, o consumer fica em loop infinito à espera de mensagens na fila.

Dentro do loop, consome a mensagem, decodifica-a e então mostra no screen:

while true
do
  ## Fica bloqueado à espera da próxima mensagem na fila
  ENCODED=`cat myqueue` 
  
  ## Quando a mensagem chega na fila, decodifica-a utilizando o comando
  ##  echo e base64 com anonymous pipes
  DECODED=`echo $ENCODED | base64 -d`
  
  echo "Mensagem decodificada: $DECODED"
done

E o consumer está pronto. Código final do arquivo consumer.sh:

#!/bin/bash

## Cria o named pipe
mkfifo myqueue

echo 'Aguardando jobs na fila...'

while true
do
  ## Fica bloqueado à espera da próxima mensagem na fila
  ENCODED=`cat myqueue` 
  
  ## Quando a mensagem chega na fila, decodifica-a utilizando o comando
  ##  echo e base64 com anonymous pipes
  DECODED=`echo $ENCODED | base64 -d`
  
  echo "Mensagem decodificada: $DECODED"
done

Em uma sessão do bash:

$ bash consumer.sh
Aguardando jobs na fila...

E em outra sessão do bash, podemos utilizar vários producers para enviar diversas mensagens codificadas para a fila:

$ echo 'my precious' | base64 > myqueue
$ echo 'pipes are awesome' | base64 > myqueue

Consultando o output na sessão do consumer:

Mensagem decodificada: my precious
Mensagem decodificada: pipes are awesome

Resumo

Que jornada! Nesta seção vimos a utilização de UNIX pipes para IPC, vimos a semelhança e diferença entre anonymous pipes | e named pipes, bem como a implementação de um sistema de background jobs com pipes.

Agora, é hora de explorar uma forma ainda mais sofisticada de IPC: UNIX Sockets.

Last updated