O módulo signal permite reagir a eventos assíncronos enviados ao processo, como interrupção por Ctrl+C, pedido de encerramento do sistema, timers e notificações relacionadas a processos filhos. Em aplicações de linha de comando, servidores e workers, sinais são essenciais para liberar recursos e encerrar sem corromper dados.
O comportamento do Python possui regras importantes: handlers Python são executados na thread principal do interpretador principal, não dentro do handler nativo de baixo nível. Além disso, somente a thread principal pode registrar novos handlers. Isso influencia como serviços multithread devem coordenar o shutdown.
Sinais mais usados
SIGINT normalmente vem de Ctrl+C e, por padrão, gera KeyboardInterrupt. SIGTERM é o pedido comum de encerramento usado por systemd, Docker, Kubernetes e comandos como kill. Em Unix, SIGHUP pode indicar fechamento do terminal ou pedido de reload, dependendo da aplicação.
SIGKILL e SIGSTOP não podem ser capturados, bloqueados ou ignorados. Por isso, um programa não consegue executar limpeza quando recebe SIGKILL.
Registrar um handler
import signal
encerrar = False
def ao_encerrar(signum, frame):
global encerrar
encerrar = True
signal.signal(signal.SIGINT, ao_encerrar)
if hasattr(signal, "SIGTERM"):
signal.signal(signal.SIGTERM, ao_encerrar)
O handler recebe o número do sinal e o frame atual. Para shutdown cooperativo, mantenha-o curto: altere uma flag, escreva em um descritor não bloqueante ou sinalize uma estrutura preparada para isso.
Não faça trabalho pesado no handler
Evite salvar arquivos, fechar centenas de conexões, adquirir locks ou executar chamadas lentas diretamente no handler. A documentação alerta que primitivas como threading.Lock podem causar deadlock nesse contexto.
O handler deve notificar o fluxo normal da aplicação. A limpeza ocorre depois, em código comum, onde exceções e ordem das etapas podem ser controladas.
Shutdown cooperativo em um loop
import signal
import time
parar = False
def pedir_parada(signum, frame):
global parar
parar = True
signal.signal(signal.SIGINT, pedir_parada)
if hasattr(signal, "SIGTERM"):
signal.signal(signal.SIGTERM, pedir_parada)
while not parar:
executar_proxima_tarefa()
time.sleep(0.2)
fechar_recursos()
O loop verifica a flag entre unidades de trabalho. Tarefas longas devem oferecer pontos de cancelamento. Se uma função C roda por muito tempo sem devolver controle ao interpretador, o handler Python pode atrasar até a função terminar.
Threads e sinais
Mesmo quando um sinal chega a outra thread, o handler Python executa na thread principal. Sinais não são um mecanismo apropriado de comunicação entre threads. Use threading.Event, filas ou outras primitivas.
import signal
import threading
parada = threading.Event()
def handler(signum, frame):
parada.set()
signal.signal(signal.SIGTERM, handler)
Embora Event.set() seja frequentemente usado nesse padrão, mantenha handlers mínimos e teste a aplicação real. Não tente registrar signal.signal() dentro de um worker; isso gera ValueError.
SIGINT sem depender de KeyboardInterrupt
Aplicações complexas podem preferir um handler explícito em vez de capturar KeyboardInterrupt. Uma exceção gerada por sinal pode aparecer entre instruções e interromper código em um estado inesperado.
Um handler que apenas sinaliza o loop permite concluir a operação atual, impedir novas tarefas e executar o shutdown em ordem previsível.
Encerramento de servidores
Em servidores, a sequência pode ser: parar de aceitar conexões, aguardar requisições em andamento dentro de um prazo, cancelar tarefas restantes, fechar pools, flush de logs e sair.
O guia de socketserver no Python explica que o método shutdown() deve ser chamado por outra thread quando serve_forever() está ativo. O handler pode notificar essa thread, mas não deve bloquear esperando o servidor.
Containers e SIGTERM
Orquestradores normalmente enviam SIGTERM e aguardam um período antes de usar SIGKILL. O processo Python deve registrar SIGTERM, interromper novas entradas e concluir dentro do grace period.
Use o formato exec no comando do container para que o processo Python receba o sinal diretamente. Wrappers de shell mal configurados podem reter ou não encaminhar o sinal.
SIGPIPE e BrokenPipeError
Python ignora SIGPIPE por padrão para transformar escrita em pipe fechado em BrokenPipeError. Não restaure SIGPIPE para o comportamento padrão apenas para esconder a exceção, pois conexões de rede interrompidas poderiam encerrar o processo inesperadamente.
O artigo de errno no Python explica EPIPE e outros códigos do sistema.
Timers com alarm()
Em Unix, signal.alarm() agenda SIGALRM em segundos inteiros. Existe apenas um alarme por processo; um novo substitui o anterior.
import signal
class TempoEsgotado(TimeoutError):
pass
def timeout(signum, frame):
raise TempoEsgotado("Operação excedeu o prazo")
signal.signal(signal.SIGALRM, timeout)
signal.alarm(5)
try:
operacao_bloqueante()
finally:
signal.alarm(0)
Esse padrão é específico de Unix e só funciona na thread principal. Ele também usa exceção assíncrona, o que pode interromper código em pontos delicados. Prefira timeouts nativos da API quando existirem.
Timers de maior precisão
setitimer() aceita segundos fracionários e pode ser periódico. ITIMER_REAL gera SIGALRM; ITIMER_VIRTUAL mede tempo de CPU do processo; ITIMER_PROF combina tempo do processo e do kernel.
Timers por sinal são globais ao processo e podem conflitar com bibliotecas. Documente seu uso e restaure o estado anterior quando possível.
Consultar sinais disponíveis
import signal
for numero in sorted(signal.valid_signals(), key=int):
try:
nome = signal.Signals(numero).name
descricao = signal.strsignal(numero)
print(numero, nome, descricao)
except ValueError:
pass
Nem todos os sinais existem em todas as plataformas. Use hasattr(signal, "SIGTERM") e valid_signals() em código portátil.
Restaurar handlers
signal.signal() retorna o handler anterior. Bibliotecas devem restaurá-lo quando terminarem sua responsabilidade.
anterior = signal.signal(signal.SIGINT, handler)
try:
executar()
finally:
signal.signal(signal.SIGINT, anterior)
Não sobrescreva silenciosamente um handler instalado pela aplicação principal.
set_wakeup_fd()
Loops baseados em select ou poll podem ficar bloqueados enquanto uma flag de shutdown foi definida. set_wakeup_fd() escreve um byte em um descritor não bloqueante quando um sinal com handler registrado chega, acordando o loop.
import os
import signal
leitura, escrita = os.pipe()
os.set_blocking(leitura, False)
os.set_blocking(escrita, False)
signal.set_wakeup_fd(escrita)
signal.signal(signal.SIGTERM, lambda signum, frame: None)
O loop deve drenar os bytes. O descritor possui buffer limitado; escolha corretamente warn_on_full_buffer. O próximo conjunto sobre select mostrará como integrar esse padrão.
Bloquear e esperar sinais em Unix
pthread_sigmask() pode bloquear sinais em uma thread. sigwait(), sigwaitinfo() e sigtimedwait() permitem que uma thread espere sinais de um conjunto bloqueado de forma síncrona.
Essa arquitetura pode simplificar aplicações Unix complexas: bloqueie determinados sinais antes de iniciar workers e dedique uma thread a esperá-los. Ainda assim, confirme o comportamento de bibliotecas e da plataforma.
Não capture SIGSEGV para recuperar
Erros síncronos como SIGSEGV, SIGFPE ou SIGBUS causados por código C não podem ser corrigidos com um handler Python comum. Ao retornar, a instrução inválida provavelmente executará novamente.
Use faulthandler para obter diagnóstico de crashes nativos. O conjunto sobre ctypes no Python recomenda isolar bibliotecas instáveis em subprocessos.
Subprocessos e grupos
Ao controlar subprocessos, decida se o sinal deve atingir apenas o processo filho ou todo o grupo. terminate() e kill() possuem diferenças entre Windows e Unix. Evite deixar netos órfãos.
Não envie sinais usando PID reutilizado sem confirmar a identidade do processo. Em Linux moderno, pidfds podem reduzir esse risco.
Tratamento idempotente
O handler pode ser chamado mais de uma vez. A primeira chamada inicia o shutdown; uma segunda pode apenas registrar urgência ou reduzir o prazo.
encerrando = False
def handler(signum, frame):
global encerrando
if encerrando:
return
encerrando = True
A limpeza também deve tolerar etapas já concluídas.
Logs dentro do handler
Evite logging complexo dentro do handler, pois ele pode adquirir locks. Registre a decisão no loop principal. Quando um diagnóstico mínimo é indispensável, use mecanismos desenhados para esse contexto e teste em cada plataforma.
Integração com eventos
Uma arquitetura robusta converte o sinal em um evento normal do sistema. O handler escreve no wakeup FD ou define uma flag, o seletor acorda, e o loop executa a máquina de estados de shutdown.
Para entender falhas de syscalls interrompidas, consulte novamente errno no Python. Para configuração de fontes e loops assíncronos orientados a descritores, o conjunto seguinte abordará select.
Testes recomendados
Teste SIGINT e SIGTERM durante espera, processamento, escrita e shutdown; dois sinais em sequência; tarefa C longa; handler registrado fora da thread principal; ausência do sinal em Windows; wakeup FD cheio; timer cancelado; subprocessos e grace period do container.
Execute testes de sinais em subprocesso para não interferir no runner principal. Verifique código de saída, recursos fechados e tempo de encerramento.
Erros comuns
Os erros mais frequentes são fazer limpeza pesada no handler, usar locks, capturar SIGSEGV como recuperação, depender apenas de KeyboardInterrupt, esquecer SIGTERM em containers, presumir sinais Unix no Windows, não restaurar handlers e deixar o loop bloqueado sem wakeup FD.
Conclusão
signal permite transformar eventos do sistema em shutdown cooperativo, timers e notificações controladas. O handler deve ser mínimo e transferir a decisão para o fluxo normal da aplicação.
Respeite a thread principal, trate SIGTERM e SIGINT, mantenha a limpeza idempotente e teste o comportamento real em subprocessos. Consulte a documentação oficial de signal e o manual signal(7) do Linux.







