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
Pingback: Manipulando Arquivos Descritores No Shell | Daemonio Labs
Pingback: dcalc: Calculadora Simples Com Conversão De Bases | Daemonio Labs