Entendendo Processos em Shell
Um processo em Unix/Linux é uma instância de um programa em execução. Quando você executa um comando no shell, o sistema operacional cria um novo processo com um identificador único chamado PID (Process ID). Compreender como os processos funcionam é fundamental para criar scripts robustos, pois você precisa saber como iniciar, monitorar e finalizar processos corretamente.
Cada processo possui um processo pai (PPID) e pode gerar processos filhos. Quando um script shell é executado, ele se torna o processo pai de todos os comandos executados dentro dele. É crucial entender essa hierarquia porque quando o processo pai é finalizado, o comportamento dos filhos depende de como foram inicializados — alguns podem se tornar órfãos, outros podem ser automaticamente encerrados.
Executando Processos em Primeiro e Segundo Plano
No shell, você pode executar um comando em primeiro plano (foreground) ou segundo plano (background). Quando um processo roda em primeiro plano, ele bloqueia a entrada do terminal até sua conclusão. Em segundo plano, o comando executa enquanto você pode continuar digitando novos comandos. Isso é controlado pelo operador &.
#!/bin/bash
# Processo em primeiro plano (bloqueante)
echo "Iniciando backup..."
tar -czf backup.tar.gz /home/usuario/dados
echo "Backup concluído"
# Processo em segundo plano (não-bloqueante)
echo "Iniciando sincronização em background..."
rsync -av /local/ /remoto/ &
SYNC_PID=$!
echo "Sincronização rodando com PID: $SYNC_PID"
# Continua executando sem esperar
echo "Script pode fazer outras tarefas..."
wait $SYNC_PID
echo "Sincronização finalizada"
O comando wait pausa a execução do script até que o processo especificado termine. Se você usar wait sem argumentos, aguarda todos os processos em background.
Monitorando e Controlando Processos
Para criar scripts robustos, você precisa monitorar o status dos processos filhos. O comando jobs lista processos em background no contexto atual, enquanto ps fornece uma visão mais detalhada de todos os processos do sistema.
#!/bin/bash
# Iniciando múltiplos processos em background
for i in {1..3}; do
sleep 30 &
PIDS[$i]=$!
echo "Processo $i iniciado com PID: ${PIDS[$i]}"
done
# Monitorando todos os processos
echo "Aguardando conclusão de todos os processos..."
for pid in "${PIDS[@]}"; do
if wait $pid; then
echo "Processo $pid finalizou com sucesso"
else
echo "Processo $pid retornou erro: $?"
fi
done
echo "Todos os processos concluídos"
Sinais: Comunicação entre Processos
Sinais são mecanismos de comunicação interprocessual (IPC) que permitem ao sistema operacional notificar um processo sobre eventos. Existem dezenas de sinais em Unix/Linux, sendo alguns dos mais importantes: SIGTERM (encerramento graciosa), SIGKILL (encerramento forçado), SIGSTOP (pausar), SIGCONT (retomar) e SIGUSR1/SIGUSR2 (sinais personalizados definidos pelo usuário).
Quando você pressiona Ctrl+C no terminal, o sistema envia um sinal SIGINT (signal interrupt) ao processo. Ctrl+Z envia SIGSTOP. Diferentemente de SIGKILL, que não pode ser capturado, a maioria dos sinais pode ser tratada por um script através de handlers personalizados, permitindo limpeza adequada de recursos.
Sinais Comuns em Scripts Shell
Os sinais mais relevantes para scripting são SIGTERM, SIGINT, SIGHUP (hangup), SIGALRM (alarme), e os sinais do usuário. Cada sinal possui um número associado — SIGTERM é 15, SIGKILL é 9, SIGINT é 2. Você pode enviar sinais usando o comando kill -SINAL PID.
#!/bin/bash
# Enviando sinais para um processo
sleep 100 &
SLEEP_PID=$!
echo "Processo iniciado com PID: $SLEEP_PID"
sleep 2
echo "Enviando SIGTERM (encerramento graciosa)..."
kill -TERM $SLEEP_PID
# Verifica se o processo ainda está rodando
sleep 1
if kill -0 $SLEEP_PID 2>/dev/null; then
echo "Processo ainda ativo, enviando SIGKILL..."
kill -KILL $SLEEP_PID
else
echo "Processo finalizou com SIGTERM"
fi
# Aguarda a conclusão
wait $SLEEP_PID 2>/dev/null
echo "Status final: $?"
A opção -0 do comando kill testa se um processo existe sem enviar sinal algum — útil para verificar se um PID ainda está ativo.
Trap: Capturando e Tratando Sinais
O comando trap é o mecanismo principal para capturar sinais e executar ações customizadas. Quando um script recebe um sinal, ao invés de ser encerrado abruptamente, ele pode executar uma função ou comando definido no trap. Isso permite limpeza de arquivos temporários, fechamento de conexões, e outras operações críticas.
A sintaxe básica é trap 'comando' SINAL. Você pode definir múltiplos traps para diferentes sinais no mesmo script. O trap é herdado por processos filhos, mas pode ser redefinido ou removido com trap - SINAL.
Implementando Limpeza Segura com Trap
Um padrão essencial em scripting robusto é definir uma função de limpeza e associá-la aos sinais SIGTERM e SIGINT. Isso garante que recursos sejam liberados mesmo se o script for interrompido.
#!/bin/bash
# Variáveis globais
TEMP_DIR=$(mktemp -d)
LOCK_FILE="/tmp/meu_script.lock"
CHILD_PIDS=()
# Função de limpeza
cleanup() {
local exit_code=$?
echo "Executando limpeza..."
# Finaliza processos filhos
for pid in "${CHILD_PIDS[@]}"; do
if kill -0 $pid 2>/dev/null; then
echo "Finalizando processo filho: $pid"
kill -TERM $pid
# Aguarda com timeout
local count=0
while kill -0 $pid 2>/dev/null && [ $count -lt 5 ]; do
sleep 1
((count++))
done
# Força se ainda estiver ativo
if kill -0 $pid 2>/dev/null; then
kill -KILL $pid
fi
fi
done
# Remove arquivos temporários
if [ -d "$TEMP_DIR" ]; then
rm -rf "$TEMP_DIR"
echo "Diretório temporário removido"
fi
# Remove lock file
rm -f "$LOCK_FILE"
echo "Limpeza concluída"
exit $exit_code
}
# Registra a função de limpeza
trap cleanup SIGTERM SIGINT EXIT
# Verifica lock file para evitar múltiplas execuções
if [ -f "$LOCK_FILE" ]; then
echo "Outro processo já está em execução"
exit 1
fi
echo $$ > "$LOCK_FILE"
# Executa trabalho principal
echo "Iniciando processamento..."
for i in {1..5}; do
(
echo "Tarefa $i em execução (PID: $$)"
sleep 10
echo "Tarefa $i concluída"
) &
CHILD_PIDS+=($!)
done
# Aguarda todos os filhos
wait
echo "Processamento finalizado"
Este exemplo demonstra um padrão robusto: define uma função cleanup que trata múltiplos aspectos (finalização de filhos com timeout, remoção de temporários, lock file), registra essa função para SIGTERM, SIGINT e EXIT. O sinal EXIT é especial — dispara ao final normal do script também, garantindo limpeza em todos os cenários.
Tratando Sinais Específicos Diferentemente
Às vezes você quer comportamentos diferentes para sinais distintos. Por exemplo, SIGTERM pode ser uma "solicitação educada", enquanto SIGINT (Ctrl+C) deveria ser imediato.
#!/bin/bash
# Estado global
GRACEFUL_SHUTDOWN=false
# Processa SIGTERM com encerramento graciosa
on_sigterm() {
echo "SIGTERM recebido - iniciando encerramento graciosa"
GRACEFUL_SHUTDOWN=true
}
# Processa SIGINT com encerramento imediata
on_sigint() {
echo "SIGINT recebido - encerrando imediatamente"
exit 130 # Código padrão para SIGINT
}
trap on_sigterm SIGTERM
trap on_sigint SIGINT
echo "Processando 100 itens..."
for i in {1..100}; do
if [ "$GRACEFUL_SHUTDOWN" = true ]; then
echo "Encerramento graciosa: parando após item $i"
break
fi
echo "Processando item $i"
sleep 1
done
echo "Script finalizado normalmente"
Scripts Robustos: Padrões e Boas Práticas
Um script robusto não é apenas um que funciona — é aquele que falha de forma previsível, fornece feedback adequado, lida com erros e se recupera quando possível. Robustez envolve tratamento de erros, validação de entrada, logging, e tratamento de sinais que já discutimos. Vamos integrar todos esses conceitos em um exemplo real.
Estrutura Recomendada para Scripts Produção
Scripts destinados a ambiente de produção devem seguir um padrão consistente. Começa com shebang e declaração de opções do shell, prossegue para configuração e funções, depois lógica principal. Cada função deve ter responsabilidade clara e o script deve sair com código apropriado.
#!/bin/bash
set -euo pipefail # Exit on error, undefined vars, pipe failures
IFS=$'\n\t' # Safer field separator
# Configuração
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "$0")"
readonly LOG_FILE="${LOG_FILE:-/var/log/${SCRIPT_NAME}.log}"
readonly LOCK_FILE="/tmp/${SCRIPT_NAME}.lock"
readonly PID_FILE="/var/run/${SCRIPT_NAME}.pid"
# Variáveis de estado
declare -a CHILD_PIDS=()
CURRENT_TASK=""
# Funções de logging
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*" | tee -a "$LOG_FILE"
}
error() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" | tee -a "$LOG_FILE" >&2
}
# Função de limpeza
cleanup() {
local exit_code=$?
if [ -n "$CURRENT_TASK" ]; then
error "Interrompido durante: $CURRENT_TASK"
fi
# Finaliza filhos
for pid in "${CHILD_PIDS[@]}"; do
if kill -0 "$pid" 2>/dev/null; then
log "Finalizando processo $pid"
if ! kill -TERM "$pid" 2>/dev/null; then
kill -KILL "$pid" 2>/dev/null || true
fi
fi
done
wait "${CHILD_PIDS[@]}" 2>/dev/null || true
# Remove lock
rm -f "$LOCK_FILE" "$PID_FILE"
log "Script finalizado com código: $exit_code"
exit "$exit_code"
}
# Handlers de sinal
on_sigterm() {
log "SIGTERM recebido"
cleanup
}
on_sigint() {
log "SIGINT recebido (Ctrl+C)"
cleanup
}
# Validação de pré-requisitos
check_requirements() {
local required_cmds=("curl" "jq" "openssl")
for cmd in "${required_cmds[@]}"; do
if ! command -v "$cmd" &> /dev/null; then
error "Comando obrigatório não encontrado: $cmd"
exit 1
fi
done
if [ ! -w "$(dirname "$LOG_FILE")" ]; then
error "Sem permissão de escrita em $(dirname "$LOG_FILE")"
exit 1
fi
}
# Verificação de lock
acquire_lock() {
if [ -f "$LOCK_FILE" ]; then
local old_pid
old_pid=$(cat "$LOCK_FILE" 2>/dev/null || echo "")
if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then
error "Outro processo rodando (PID: $old_pid)"
exit 1
fi
rm -f "$LOCK_FILE"
fi
echo $$ > "$LOCK_FILE"
echo $$ > "$PID_FILE"
}
# Funções de negócio
process_data() {
local input_file="$1"
CURRENT_TASK="processar dados de $input_file"
log "Iniciando: $CURRENT_TASK"
# Validação
if [ ! -f "$input_file" ]; then
error "Arquivo não encontrado: $input_file"
return 1
fi
if [ ! -r "$input_file" ]; then
error "Sem permissão de leitura: $input_file"
return 1
fi
# Processamento
local line_count=0
while IFS= read -r line; do
((line_count++))
# Simula processamento
sleep 0.1
if [ $((line_count % 10)) -eq 0 ]; then
log "Processadas $line_count linhas"
fi
done < "$input_file"
log "Processamento concluído: $line_count linhas"
return 0
}
fetch_remote_data() {
local url="$1"
local output_file="$2"
CURRENT_TASK="baixar dados de $url"
log "Iniciando: $CURRENT_TASK"
if ! curl -sSf -m 30 -o "$output_file" "$url"; then
error "Falha ao baixar de $url"
return 1
fi
log "Download concluído: $output_file"
return 0
}
# Programa principal
main() {
log "Iniciando $SCRIPT_NAME (PID: $$)"
check_requirements
acquire_lock
# Registra handlers
trap on_sigterm SIGTERM
trap on_sigint SIGINT
trap cleanup EXIT
# Trabalho em paralelo
log "Iniciando processamento paralelo"
fetch_remote_data "https://example.com/data.json" "/tmp/data.json" &
CHILD_PIDS+=($!)
process_data "/etc/hostname" &
CHILD_PIDS+=($!)
# Aguarda conclusão
local failed=0
for pid in "${CHILD_PIDS[@]}"; do
if ! wait "$pid"; then
((failed++))
fi
done
CURRENT_TASK=""
if [ $failed -gt 0 ]; then
error "$failed tarefas falharam"
return 1
fi
log "Todas as tarefas completadas com sucesso"
return 0
}
# Executa
main "$@"
Este exemplo consolida tudo que vimos: usa set -euo pipefail para falhar rápido e seguro, implementa logging estruturado, trata sinais adequadamente, valida pré-requisitos, gerencia lock files, processa múltiplas tarefas em paralelo, e fornece limpeza garantida.
Conclusão
Dominar shell scripting avançado exige compreensão profunda de três pilares: processos e seu ciclo de vida — você agora sabe como iniciar, monitorar e finalizar processos corretamente, utilizando &, wait e jobs para coordenação; sinais como mecanismo de comunicação — compreende que sinais como SIGTERM, SIGINT e SIGKILL são notificações que permitem ao sistema se comunicar com processos, e que a maioria pode ser capturada para ações customizadas; trap para tratamento robusto — implementa handlers que garantem limpeza de recursos mesmo em cenários de interrupção, transformando scripts frágeis em soluções confiáveis para produção.