DevOps Admin

Como Usar Shell Scripting Avançado: Processos, Sinais, Trap e Scripts Robustos em Produção Já leu

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

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.

Referências


Artigos relacionados