Coprocessos
LidoNesta aula, você aprenderá sobre coprocessos no Bash, uma poderosa ferramenta que permite iniciar um processo em segundo plano e comunicar-se com ele bidireccionalmente. Exploraremos a sintaxe da palavra-chave `coproc`, como usar os file descriptors para enviar e receber dados, casos de uso práticos, e as limitações dessa abordagem. Ao final, você terá exercícios para consolidar o conhecimento.
Os coprocessos no Bash são uma funcionalidade avançada que permite iniciar um comando em segundo plano e estabelecer uma comunicação bidirecional com ele. Enquanto um pipeline tradicional conecta a saída de um comando à entrada de outro, um coprocesso mantém ambos os canais abertos, permitindo que o script envie dados para o processo e receba respostas simultaneamente. Isso é extremamente útil em cenários onde você precisa de interação contínua com um programa, como um servidor, um interpretador interativo ou um filtro que processa dados sob demanda.
Nesta aula, vamos mergulhar na sintaxe e no funcionamento dos coprocessos, explorar exemplos práticos e discutir quando utilizá-los. Você verá como criar um coprocesso com a palavra-chave `coproc`, como acessar os file descriptors de entrada e saída, e como gerenciar a comunicação de forma eficiente. Além disso, abordaremos as limitações dessa técnica, como o suporte a apenas um coprocesso por vez e a falta de sincronização automática.
coproc
A palavra-chave coproc é usada para iniciar um comando como um coprocesso. A sintaxe básica é:
coproc [NOME] comando [argumentos...]Se você fornecer um NOME, o Bash criará um array associativo com esse nome contendo os file descriptors. Se não fornecer, o nome padrão será COPROC. O coprocesso é executado em segundo plano, e o script pode se comunicar com ele através dos file descriptors ${NOME[0]} (para leitura da saída do coprocesso) e ${NOME[1]} (para escrita na entrada do coprocesso).
Vamos a um exemplo simples. Suponha que você queira executar o comando bc (uma calculadora de precisão arbitrária) como um coprocesso para fazer cálculos interativamente:
#!/bin/bash
# Inicia o coprocesso 'calc' com o comando bc
coproc CALC { bc; }
# Envia um comando para o bc
echo "3 + 4" >&${CALC[1]}
# Lê a resposta
read -u ${CALC[0]} resultado
# Exibe o resultado
echo "Resultado: $resultado"
# Encerra o coprocesso
echo "quit" >&${CALC[1]}
wait $CALC_PIDNeste exemplo, coproc CALC { bc; } inicia o comando bc como um coprocesso. O array CALC conterá os file descriptors: ${CALC[0]} é o descritor de leitura (saída do bc) e ${CALC[1]} é o descritor de escrita (entrada do bc). A variável CALC_PID é automaticamente definida com o PID do processo. Enviamos a expressão 3 + 4 para o bc, lemos a resposta com read -u ${CALC[0]}, e depois enviamos quit para encerrar o coprocesso.
Observe que precisamos usar >&${CALC[1]} para redirecionar a saída do nosso script para o descritor de escrita. Isso é semelhante ao redirecionamento para um arquivo, mas para um descritor de arquivo específico. Da mesma forma, read -u ${CALC[0]} lê de um descritor de arquivo específico.
Comunicação bidirecional
A principal vantagem dos coprocessos é a comunicação bidirecional. Diferente de um pipe simples, onde os dados fluem em uma direção, o coprocesso permite tanto enviar quanto receber dados. Isso é possível porque o Bash cria dois pipes: um para a entrada do coprocesso e outro para a saída. O script mantém os descritores de ambos os pipes abertos.
Vamos explorar um exemplo mais elaborado: um servidor de eco que recebe linhas e as devolve em maiúsculas. Podemos usar o comando tr como coprocesso, mas tr não é interativo, então não é adequado. Em vez disso, podemos usar um script simples ou um comando como cat que repete o que recebe. Para um exemplo mais real, vamos usar o comando python3 com um script que lê linhas e as imprime em maiúsculas:
#!/bin/bash
# Inicia um coprocesso com Python
coproc PY { python3 -c "
import sys
for line in sys.stdin:
sys.stdout.write(line.upper())
sys.stdout.flush()
" ; }
# Envia algumas linhas
for i in 1 2 3; do
echo "linha $i" >&${PY[1]}
read -u ${PY[0]} resposta
echo "Resposta: $resposta"
done
# Encerra o coprocesso
echo "" >&${PY[1]}
wait $PY_PIDNeste exemplo, o coprocesso Python lê do stdin, converte para maiúsculas e escreve no stdout. O script envia três linhas e lê a resposta de cada uma. Note que usamos flush() no Python para garantir que a saída seja enviada imediatamente, caso contrário, o buffer pode atrasar a comunicação.
É importante notar que a comunicação é baseada em fluxos de bytes. Não há delimitação automática de mensagens; o script e o coprocesso devem concordar sobre o formato (por exemplo, linhas terminadas por \n). No exemplo acima, usamos linhas, mas em outros casos pode ser necessário usar protocolos mais complexos, como JSON ou campos de comprimento fixo.
Casos de uso
Coprocessos são úteis em situações onde você precisa de interação contínua com um programa. Alguns casos comuns incluem:
- Calculadoras e interpretadores interativos: como
bc,python,node, etc., onde você pode enviar expressões e obter resultados sem iniciar o processo a cada vez. - Servidores locais: se você tiver um servidor que aceita comandos via stdin/stdout, pode usar um coprocesso para manter uma conexão persistente.
- Filtros com estado: programas que mantêm estado entre chamadas, como um contador ou um acumulador, podem ser executados como coprocessos para evitar reinicialização.
- Automação de ferramentas interativas: ferramentas como
ftp,telnet, ousshpodem ser controladas via coprocessos, embora seja recomendável usar expect para casos complexos.
Vamos a um exemplo prático: usar um coprocesso com python3 para realizar cálculos científicos que exigem estado, como uma série de operações em uma matriz. Mas para simplificar, vamos implementar um contador em Python:
#!/bin/bash
# Coprocesso que mantém um contador
coproc COUNTER { python3 -c "
count = 0
while True:
line = sys.stdin.readline()
if not line:
break
if line.strip() == 'inc':
count += 1
print(count, flush=True)
elif line.strip() == 'reset':
count = 0
print(0, flush=True)
else:
print('Comando desconhecido', flush=True)
"; }
# Incrementa algumas vezes
for i in 1 2 3; do
echo "inc" >&${COUNTER[1]}
read -u ${COUNTER[0]} valor
echo "Contador: $valor"
done
# Reseta
echo "reset" >&${COUNTER[1]}
read -u ${COUNTER[0]} valor
echo "Após reset: $valor"
# Encerra
echo "" >&${COUNTER[1]}
wait $COUNTER_PIDNeste exemplo, o coprocesso mantém um contador em memória e responde a comandos inc e reset. Isso demonstra como evitar o custo de iniciar um novo processo a cada interação.
Limitações
Apesar de poderosos, os coprocessos têm limitações que você deve conhecer:
- Suporte a apenas um coprocesso por vez: O Bash só permite um coprocesso ativo por vez. Se você tentar iniciar outro
coprocenquanto um já está em execução, o Bash emitirá um erro e o novo coprocesso não será iniciado. Isso limita a complexidade de scripts que precisam de múltiplas comunicações simultâneas. - Sem sincronização automática: O script e o coprocesso são processos separados, e a comunicação é assíncrona. Se o script tentar ler uma resposta antes de enviar um comando, pode ficar bloqueado. É necessário gerenciar o fluxo de dados cuidadosamente para evitar deadlocks.
- Buffer de saída: Muitos programas bufferizam sua saída quando não estão conectados a um terminal. Isso pode causar atrasos na comunicação. É recomendável usar opções como
stdbufou chamarflushno programa para forçar a saída imediata. - Mensagens delimitadas: Como a comunicação é baseada em fluxo, não há delimitação automática de mensagens. O script deve definir um protocolo, como linhas ou tamanho fixo, e garantir que o coprocesso o siga.
- Encerramento: Para encerrar um coprocesso, você precisa enviar um sinal ou fechar o descritor de escrita. Fechar o descritor de escrita fará com que o coprocesso receba EOF e possa terminar. Depois, use
waitpara aguardar o processo terminar.
Vamos testar a limitação de um único coprocesso:
#!/bin/bash
coproc A { cat; }
# Tentativa de iniciar outro coprocesso
coproc B { cat; } 2>&1
# O erro será exibido, e B não será iniciado
echo "B está definido? ${B[@]:-não}"Ao executar, você verá uma mensagem de erro como bash: coproc: B: coproc: cannot use: only one coproc allowed. Isso mostra que apenas um coprocesso pode existir por vez.
Boas práticas
- Use nomes significativos: Dê nomes descritivos aos coprocessos para facilitar a leitura do código.
- Feche os descritores quando não precisar mais: Se você não vai enviar mais dados, feche o descritor de escrita com
exec {FD}>&-para que o coprocesso receba EOF. - Implemente timeouts: Para evitar deadlocks, use
read -tpara ler com timeout e tratar a ausência de resposta. - Use ferramentas como
expectpara interações complexas: Se você precisa de controle de terminal ou sincronização avançada, considere usarexpectem vez de coprocessos.
Exercícios
Escreva um script que use um coprocesso com
bcpara calcular a soma de dois números fornecidos pelo usuário. O script deve ler dois números, enviar a expressão para o coprocesso e exibir o resultado.✓ Resposta:#!/bin/bash coproc BC { bc; } read -p "Digite o primeiro número: " a read -p "Digite o segundo número: " b echo "$a + $b" >&${BC[1]} read -u ${BC[0]} resultado echo "A soma é: $resultado" echo "quit" >&${BC[1]} wait $BC_PIDCrie um coprocesso que execute
python3e responda com o quadrado de um número enviado. O script deve enviar vários números e imprimir os resultados.✓ Resposta:#!/bin/bash coproc PY { python3 -c " import sys for line in sys.stdin: n = int(line.strip()) print(n*n, flush=True) "; } for num in 1 2 3 4 5; do echo "$num" >&${PY[1]} read -u ${PY[0]} quadrado echo "$num^2 = $quadrado" done echo "" >&${PY[1]} wait $PY_PIDExplique o que acontece se você tentar iniciar um segundo coprocesso enquanto outro está ativo. Ilustre com um exemplo.
✓ Resposta: O Bash exibirá um erro indicando que apenas um coprocesso é permitido. O novo coprocesso não será iniciado. Exemplo:#!/bin/bash coproc A { cat; } # Tentativa de iniciar outro coprocesso coproc B { cat; } 2>&1 # O erro será exibido, e B não será iniciado echo "B está definido? ${B[@]:-não}"Escreva um script que use um coprocesso para manter uma lista de itens em memória (por exemplo, em Python) e permita adicionar e listar itens. O script deve aceitar comandos do usuário:
add <item>elist.✓ Resposta:#!/bin/bash coproc LIST { python3 -c " import sys items = [] for line in sys.stdin: line = line.strip() if line.startswith('add '): items.append(line[4:]) print('Item adicionado', flush=True) elif line == 'list': for item in items: print(item, flush=True) else: print('Comando desconhecido', flush=True) "; } while true; do read -p "> " cmd if [[ $cmd == "sair" ]]; then echo "" >&${LIST[1]} break fi echo "$cmd" >&${LIST[1]} while read -u ${LIST[0]} resp; do if [[ $cmd == list* ]]; then # Para list, continuamos lendo até linha vazia if [[ -z $resp ]]; then break fi else break fi echo "$resp" done done wait $LIST_PIDDiscuta as limitações dos coprocessos e sugira uma alternativa para cenários mais complexos.
✓ Resposta: As limitações incluem: apenas um coprocesso por vez, falta de sincronização, problemas de buffer, e comunicação baseada em fluxo sem delimitação automática. Para cenários complexos, alternativas incluem usar FIFOs (named pipes) para criar múltiplos canais de comunicação, usarexpectpara interações com programas que exigem terminal, ou usar sockets com ferramentas comosocatounetcat. Em aplicações mais robustas, considerar usar uma linguagem de script mais poderosa como Python ou Perl, que oferecem melhores abstrações para comunicação entre processos.