Signals: SIGTERM, SIGKILL, SIGHUP na prática

1. Introdução aos Signals no Bash

Signals são mecanismos de comunicação assíncrona entre processos no Linux/Unix. Eles permitem que o sistema operacional ou um processo notifique outro sobre eventos específicos, como solicitações de encerramento, interrupções ou mudanças de estado. No Bash, entender signals é essencial para controlar processos de forma eficiente, especialmente em scripts que precisam lidar com terminação limpa, recarga de configurações ou recuperação de falhas.

Os três signals mais comuns na prática com Bash são:

  • SIGTERM (15): Solicitação educada de encerramento
  • SIGKILL (9): Terminação forçada e inegável
  • SIGHUP (1): Notificação de desconexão do terminal

No Bash interativo, signals como SIGINT (Ctrl+C) são tratados automaticamente. Já em scripts, você pode capturar e responder a esses sinais usando o comando trap, permitindo controle granular sobre o comportamento dos seus processos.

2. SIGTERM: O Pedido de Encerramento Padrão

SIGTERM é o signal padrão enviado pelo comando kill. Seu comportamento esperado é que o processo realize uma terminação limpa: feche arquivos abertos, libere recursos e finalize operações pendentes.

# Enviando SIGTERM para um processo
kill 1234           # Equivalente a kill -15 1234
kill -15 1234       # Forma explícita
kill -TERM 1234     # Usando nome do signal

Em scripts, você pode capturar SIGTERM para executar ações de limpeza antes de encerrar:

#!/bin/bash
# Script que faz limpeza ao receber SIGTERM

cleanup() {
    echo "[$(date)] Recebido SIGTERM. Limpando..."
    rm -f /tmp/meu_arquivo_temporario
    echo "Arquivos temporários removidos. Encerrando."
    exit 0
}

trap cleanup SIGTERM

echo "Processo rodando. PID: $$"
while true; do
    echo "Trabalhando..."
    sleep 2
done

Execute este script e envie SIGTERM de outro terminal:

kill -TERM <PID_DO_SCRIPT>

3. SIGKILL: A Força Bruta

SIGKILL é o signal mais agressivo. Diferente de SIGTERM, ele não pode ser ignorado, capturado ou tratado pelo processo. O kernel do sistema operacional remove o processo imediatamente, sem qualquer chance de limpeza.

# Enviando SIGKILL
kill -9 1234
kill -KILL 1234

Quando usar SIGKILL: Apenas como último recurso, quando um processo não responde a SIGTERM ou está congelado. Os riscos incluem:

  • Perda de dados não salvos
  • Arquivos temporários órfãos
  • Corrupção de sistemas de arquivos (especialmente em bancos de dados)
  • Recursos do sistema (sockets, semáforos) não liberados
# Exemplo: tentar SIGTERM primeiro, depois SIGKILL
for signal in TERM KILL; do
    if kill -"$signal" 1234 2>/dev/null; then
        echo "Signal $signal enviado com sucesso"
        sleep 2
    fi
done

4. SIGHUP: Recarregar ou Desconectar

Originalmente, SIGHUP era enviado quando a linha telefônica (hangup) era desconectada. Hoje, indica que o terminal de controle foi fechado. Seu comportamento padrão é encerrar o processo, mas muitos daemons o interpretam como "recarregue suas configurações".

# Enviando SIGHUP para recarregar configurações
kill -HUP $(pgrep nginx)    # Recarrega configurações do Nginx
kill -1 $(pgrep sshd)       # Recarrega configurações do SSH

Para proteger processos de SIGHUP ao sair do terminal, use nohup ou disown:

# Usando nohup para ignorar SIGHUP
nohup ./meu_script.sh &
nohup python3 servidor.py > log.txt 2>&1 &

# Usando disown em processos já em execução
./meu_script.sh &
disown %1          # Remove job da tabela de jobs do shell

O comando disown remove o processo da lista de jobs do shell, impedindo que SIGHUP seja enviado quando o terminal for fechado.

5. Manipulando Signals com trap

O comando trap é a ferramenta central para manipular signals em scripts Bash. Sua sintaxe básica é:

trap 'comando' SIGNAL
trap 'comando' SIGNAL1 SIGNAL2   # Múltiplos signals
trap '' SIGNAL                    # Ignorar signal (com limitações)
trap - SIGNAL                     # Restaurar comportamento padrão

Exemplo prático com múltiplos signals:

#!/bin/bash
# Script robusto com tratamento de signals

arquivo_temp="/tmp/dados_${$}.tmp"

cleanup() {
    local signal=$1
    echo "[$(date)] Signal $signal recebido. Executando limpeza..."
    rm -f "$arquivo_temp"
    echo "Arquivo $arquivo_temp removido."
    exit 0
}

trap 'cleanup SIGTERM' SIGTERM
trap 'cleanup SIGINT' SIGINT
trap 'cleanup SIGHUP' SIGHUP

echo "Iniciando processamento. PID: $$"
echo "Dados importantes" > "$arquivo_temp"

# Simula trabalho longo
for i in {1..30}; do
    echo "Processando etapa $i..."
    sleep 1
done

cleanup "normal"

Ignorando signals: Você pode ignorar signals com trap '' SIGNAL, mas isso não funciona para SIGKILL e SIGSTOP:

trap '' SIGTERM   # Ignora SIGTERM (mas não funciona para SIGKILL)

6. Signals e Gerenciamento de Processos no Terminal

Signals são fundamentais para gerenciar jobs em background. Além de SIGTERM, SIGKILL e SIGHUP, outros signals importantes incluem SIGSTOP (pausa) e SIGCONT (continua):

# Iniciar processo em background
sleep 100 &
# Pausar o processo
kill -STOP %1
# Verificar status (deve mostrar "Stopped")
jobs
# Continuar o processo
kill -CONT %1
# Encerrar o processo
kill %1

Exemplo prático de pausa e retomada:

#!/bin/bash
# Demonstração de pausa/continuação

echo "Iniciando processo de backup..."
dd if=/dev/zero of=/tmp/teste.img bs=1M count=1000 &
pid=$!
echo "Backup rodando. PID: $pid"

sleep 2
echo "Pausando backup..."
kill -STOP $pid
sleep 3
echo "Retomando backup..."
kill -CONT $pid

wait $pid
echo "Backup concluído."

7. Boas Práticas e Casos Reais

Prefira SIGTERM sobre SIGKILL: Em scripts de shutdown, sempre tente SIGTERM primeiro com um timeout:

#!/bin/bash
# Script de shutdown seguro

graceful_shutdown() {
    local pid=$1
    local timeout=10

    kill -TERM "$pid" 2>/dev/null

    # Aguarda o processo encerrar
    for ((i=0; i<timeout; i++)); do
        if ! kill -0 "$pid" 2>/dev/null; then
            echo "Processo $pid encerrado com SIGTERM"
            return 0
        fi
        sleep 1
    done

    # Timeout: usa SIGKILL
    echo "Timeout. Forçando SIGKILL no processo $pid"
    kill -KILL "$pid" 2>/dev/null
}

# Uso
graceful_shutdown 1234

Combinando signals com find e xargs:

# Encerrar todos os processos python de um usuário específico
pgrep -u usuario python3 | xargs -r kill -TERM

# Enviar SIGHUP para todos os processos nginx
pgrep nginx | xargs -r kill -HUP

Debugging com logging de signals:

#!/bin/bash
# Script com logging de signals para debugging

log_signal() {
    local signal=$1
    echo "$(date '+%Y-%m-%d %H:%M:%S') - Signal $signal recebido pelo PID $$" >> /tmp/signal_log.txt
}

trap 'log_signal SIGTERM; exit 0' SIGTERM
trap 'log_signal SIGINT; exit 0' SIGINT
trap 'log_signal SIGHUP' SIGHUP

echo "Processo em execução. PID: $$"
while true; do
    sleep 5
done

Para verificar signals recebidos em tempo real, use strace:

strace -e trace=signal -p <PID>

Dominar signals no Bash é essencial para criar scripts robustos e confiáveis. Lembre-se: SIGTERM é para encerramento limpo, SIGHUP para recarga de configurações, e SIGKILL apenas quando não houver alternativa.

Referências