Entendendo Coprocessos (coproc) Do Bash

Introdução

Nesse post irei abordar um tema recentemente implementado no bash, o conceito de coprocessos. Em outros shells, como ksh, esse conceito já existe há bastante tempo e no bash, ele aparece a partir da versão 4 do interpretador.

Coprocessos

Quando colocamos um processo em background, na maioria das vezes já definimos o que ele irá fazer e por isso não é preciso comunicarmos com ele. E outra, essa comunicação nem sempre é fácil e quando presente é feita através de sinais, arquivos em disco, fifos ou sockets. Um coprocesso nada mais é que um processo em background em que seus arquivos descritores padrões (nesse caso, stdin e stdout) estão disponíveis para o processo pai. Em outras palavras, um coprocesso é um job (processo em background) com a capacidade de se comunicar com o processo pai através de arquivos descritores.

Criando um coprocesso no bash

A sintaxe para se criar um coprocesso é:

$ coproc [NOME] comando [REDIRECIONAMENTO]

[NOME] deve ser definido somente se comando é um comando composto. Após a criação do coprocesso, o bash diponibiliza os arquivos descritores em um array. O nome do array vai depender se [NOME] foi definido ou não, caso seja, o nome do array será NOME e caso não seja, será COPROC. Esse array tem dois elementos, o primeiro representa a entrada do coproc e o segundo a saída.

Para entender melhor, veja um exemplo:

#!/bin/bash
coproc bc
echo '77+99' >&${COPROC[1]}
read -u ${COPROC[0]} RESULTADO
echo 'Resultado: '$RESULTADO

No script acima o bash criou o coprocesso bc. Como bc é um comando simples, então o array de arquivos descritores irá se chamar COPROC. Para enviarmos algo para o job bc precisamos redirecionar a saída do comando desejado para o descritor ${COPROC[1]}. A saída do bc está disponível para leitura através do descritor ${COPROC[0]}. Essa leitura pode ser feita, por exemplo, com o read utilizando a opção -u.

Quando é útil utilizar coprocessos

Um coprocesso é criado somente uma vez e estará disponível para ser executado tantas vezes que o processo pai desejar. A vantagem de um coprocesso é utilizá-lo quando precisamos abrir várias instâncias de um processo, por exemplo dentro de um loop. Veja o exemplo abaixo sem coprocesso:

#!/bin/bash

for A in {1..50} ; do
    echo $A'*'$A | bc
done

O processo bc será criado 50 vezes por causa do loop. Isso pode acarretar lentidão no script, pois o overhead de se fazer um fork em um programa é bastante penoso. Usando coprocessos, um programa só é carregado uma vez e por isso tende a aumentar o desempenho dos scripts. Veja o mesmo exemplo utilizando coprocessos:


#!/bin/bash

coproc bc

for A in {1..50} ; do
    echo $A'*'$A >&${COPROC[1]}
    read -u ${COPROC[0]}
    echo $REPLY
done

O segundo script é mais rápido que o primeiro, embora utilize mais comandos. É bom notar que os comandos acrescentados não interferem muito no desempenho do script, pois eles são todos built-ins do bash e não precisam de ser “forkados” para serem executados.

Alguns problemas encontrados ao se utilizar coprocessos

Há basicamente quatro tipos de problemas : Bufferização, Fechamento do descritor de saída, Herança dos descritores e Quantidade limite de coprocessos.

Alguma coisa sobre bufferização

Alguns processos, quando percebem que sua saída não está conectada em um terminal, evitam escrever seus dados assim que eles estão prontos. Esse processo se chama bufferização e é utilizado para tornar eficientes as operações de entradas e saídas. Se tais processos são usados como coprocessos, então seremos incapazes de obter os resultados de forma imediata, e o pior de tudo, o read que usamos para ler os dados irá travar pois ele estará esperando por dados que ainda estão bufferizados.

Um problema inicial seria responder quais processos bufferizam seus dados quando não escrevem em um terminal, e a resposta é: A maioria. Todos aqueles processos que utilizam stream (FILE *, lembra disso das aulas de C?) para operações de I/O estão sujeitos a bufferização. Outros comandos, como o cat “flusham” os buffers internos a cada escrita, por isso não sofrem bufferização.

Para saber quais processos utilizam bufferização basta utilizá-los em um pipe. Se a saída não estiver pronta assim que você der um enter, então ela está sendo bufferizada:

$ sed ‘s/.*/eu disse: &/’ | cat
aaaaaaaaa
bbbbbbb
cccccccc
eu disse: aaaaaaaa
eu disse: bbbbbbb
eu disse: cccccccc

 

O sed bufferiza sua saída, pois só irá retornar os resultados das substituições quando o buffer interno estiver cheio ou quando não há mais dados para ler (ctrl + d ). Outro exemplo:

$ head -n2 | cat
aaaaa
bbbbb
aaaaa
bbbb

 

O head retorna assim que pressionamos enter o que indica que ele não bufferiza a saída.

Usando Processos Bufferizados Como Coprocessos

Como vimos, ao se usar um processos bufferizado como coprocesso não podemos ler os dados em tempo real e possivelmente uma tentativa de leitura irá ser travada. Existe um comando chamado stdbuf que modifica as operações de bufferização de um processo. Com ele podemos desabilitar a bufferização de um processo.

$ stdbuf -o0 sed ‘s/.*/eu disse: &/’ | cat
aaaa
eu disse: aaaa
bbbb
eu disse: bbbb
cccc
eu disse: cccc

A opção -o indica stdout e o argumento 0 (zero) indica sem bufferização. Veja que agora, usando o stdbuf, a saída do sed não foi bufferizada.

Programas que possuem opções contra bufferização

Alguns programas possuem opções que desabilitam a bufferização. Abaixo uma tabela relacionando o nome do programa e a opção para desligar a bufferização.

+-------------------------------+----------------------------------------------+
|grep (e.g. GNU version 2.5.1)  |                --line-buffered               |
+-------------------------------+----------------------------------------------+
| sed (e.g. GNU version 4.0.6)  |                -u,--unbuffered               |
+-------------------------------+----------------------------------------------+
|   awk (some GNU versions)     | -W interactive, or use the fflush() function |
+-------------------------------+----------------------------------------------+
|      tcpdump, tethereal       |                      -l                      |
+-------------------------------+----------------------------------------------+

Fechamento do descritor de saída

Programas que não esperam nada de stdin quando finalizam suas execuções fecham imediatamente o descritor de saída. Isto é, o coprocesso é executado e após o seu término o descritor ${COPROC[0]} é fechado e com isso, não podemos ler toda a sua saída.

#!/bin/bash

coproc ls

while read -u ${COPROC[0]} FILE
do
    echo == Nome do arquivo: $FILE
done

O script acima poderá não ler toda a saída do ls, pois quando esse programa acaba sua execução, ele fecha o descritor ${COPROC[0]}. Se isso ocorrer o read retornará esse erro:

s: line 5: read: FILE: invalid file descriptor specification

Por isso tome cuidado ao usar coprocessos com programas que não esperam nada de stdin. Toda vez que esses programas são usados é provável que eles fechem o descritor de saída antes mesmo de lermos todos os dados.

Herança dos arquivos descritores

Os descritores de um coprocesso não podem ser acessados pelos processos filhos do script. Isso afeta principalmente o uso de subprocessos como na sintaxe cat | while. Veja:

#!/bin/bash

coproc sed -u 's/:.*//'

cat /etc/passwd | while read LINHA
do
    echo -n 'Nome do usuario: '
    echo $LINHA >&${COPROC[1]}
    read -u ${COPROC[0]} USUARIO
    echo $USUARIO
done

Dentro do while os descritores no array COPROC não existem porque eles não foram herdados. O script acima retorna o seguinte erro diversas vezes:

Nome do usuario: s: line 8: ${COPROC[1]}: Bad file descriptor
teste.sh: line 9: read: 63: invalid file descriptor: Bad file descriptor

Esse erro ocorre porque o ‘|’ cria subprocessos e estes não conseguem se comunicar com o coprocesso criado. O certo é:

#!/bin/bash

coproc sed -u 's/:.*//'

while read LINHA
do
    echo -n 'Nome do usuario: '
    echo $LINHA >&${COPROC[1]}
    read -u ${COPROC[0]} USUARIO
    echo $USUARIO
done < /etc/passwd

Quantidade limite de coprocessos

O bash em si não fornece garantia para coprocessos simultâneos, apesar que é possível utilizá-los (Veja o link [2]). Evite utilizar múltiplos coprocessos, pois o bash não irá garantir uma demanda grande deles. O mais seguro é que se você for usar dois coprocessos, você deverá fechar o primeiro. Para fechar um coprocesso você pode matá-lo pelo pid $[NAME]_PID. Para coprocessos de comandos simples o pid estará na variável $COPROC_PID.

Exemplos Elaborados

A característica de comunicação de coprocessos nos permite fazer muitas coisas interessantes. É possível por exemplo, abrir uma sessão telnet em um servidor e enviarmos comandos para ela e obtermos a resposta logo em seguida. Podemos também criar um debugger para algum programa sem termos que implementar o programa em si. As opções são muitas, mas infelizmente estou sem tempo para mostrar um exemplo real e útil aqui.

Referências

[1] The coproc keyword (Acessado em: Julho/2011)
http://wiki.bash-hackers.org/syntax/keywords/coproc

[2]Bash – Coprocessos, Júlio Cezar Neves (Acessado em: Julho/2011)
http://www.dicas-l.com.br/arquivo/bash_coprocessos.php

[3] What is buffering? … (Acessado em: Julho/2011)
http://mywiki.wooledge.org/BashFAQ/009

[4] coproc tutorial, zsh Mailist (Acessado em: Julho/2011)
http://www.zsh.org/mla/users/1999/msg00619.html

[5] zsh Co-process (Acessado em: Julho/2011)
http://linuxgazette.net/52/tag/7.html

2 pensamentos sobre “Entendendo Coprocessos (coproc) Do Bash

  1. Pingback: Manipulando Arquivos Descritores No Shell | Daemonio Labs

  2. Pingback: dcalc: Calculadora Simples Com Conversão De Bases | Daemonio Labs

Deixe um comentário