Signals e tratamento de sinais do sistema operacional
1. Introdução aos Signals
Sinais são interrupções assíncronas enviadas pelo sistema operacional a um processo para notificar sobre eventos específicos. Quando um sinal é recebido, o processo pode executar uma ação padrão ou um manipulador personalizado definido pelo programador.
Os sinais mais comuns incluem:
- SIGINT (2): Interrupção do teclado (Ctrl+C) — comportamento padrão: terminar o processo
- SIGTERM (15): Solicitação de término — comportamento padrão: terminar o processo
- SIGKILL (9): Terminação forçada — não pode ser capturado ou ignorado
- SIGSEGV (11): Violação de segmentação (acesso inválido à memória)
- SIGUSR1 (10) e SIGUSR2 (12): Sinais definidos pelo usuário
- SIGCHLD (17): Notifica processo pai sobre término de processo filho
Cada sinal possui um comportamento padrão: terminar o processo, ignorar o sinal, parar a execução ou continuar a execução.
2. Manipulando Sinais com signal()
A função signal() permite registrar um manipulador para um sinal específico:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler_sigint(int sig) {
write(STDOUT_FILENO, "\nSinal SIGINT recebido. Finalizando...\n", 38);
_exit(0);
}
int main() {
signal(SIGINT, handler_sigint);
printf("Pressione Ctrl+C para testar o sinal\n");
while(1) {
pause();
}
return 0;
}
Também é possível usar constantes especiais:
signal(SIGINT, SIG_IGN); // Ignora o sinal
signal(SIGINT, SIG_DFL); // Restaura comportamento padrão
Limitações da API signal():
- Comportamento não confiável em sistemas UNIX System V (handler é resetado após receber o sinal)
- Portabilidade limitada entre diferentes sistemas Unix
- Falta de controle sobre bloqueio de sinais durante execução do handler
3. Tratamento Avançado com sigaction()
A função sigaction() oferece controle mais granular sobre o tratamento de sinais:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
volatile sig_atomic_t flag = 0;
void handler(int sig) {
flag = 1;
}
int main() {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART; // Reinicia chamadas de sistema interrompidas
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
printf("Aguardando sinal SIGINT...\n");
while(!flag) {
pause();
}
printf("Sinal recebido! Flag alterada.\n");
return 0;
}
Campos importantes da struct sigaction:
- sa_handler: Ponteiro para função manipuladora
- sa_mask: Conjunto de sinais bloqueados durante execução do handler
- sa_flags: Opções como SA_RESTART (reiniciar syscalls), SA_NODEFER (não bloquear o próprio sinal), SA_SIGINFO (obter informações detalhadas)
4. Bloqueio e Desbloqueio de Sinais
A máscara de sinais do processo controla quais sinais estão bloqueados:
#include <stdio.h>
#include <signal.h>
int main() {
sigset_t set, oldset;
// Inicializa conjunto vazio
sigemptyset(&set);
// Adiciona SIGINT ao conjunto
sigaddset(&set, SIGINT);
// Bloqueia SIGINT
if (sigprocmask(SIG_BLOCK, &set, &oldset) == -1) {
perror("sigprocmask");
return 1;
}
printf("SIGINT bloqueado. Pressione Ctrl+C...\n");
sleep(5);
// Verifica sinais pendentes
sigset_t pending;
sigpending(&pending);
if (sigismember(&pending, SIGINT)) {
printf("SIGINT pendente!\n");
}
// Desbloqueia e entrega o sinal pendente
sigprocmask(SIG_SETMASK, &oldset, NULL);
printf("Sinais desbloqueados.\n");
return 0;
}
Funções principais:
- sigemptyset(): Limpa conjunto
- sigfillset(): Preenche com todos os sinais
- sigaddset(): Adiciona sinal específico
- sigdelset(): Remove sinal específico
- sigprocmask(): Modifica máscara de sinais
- sigpending(): Verifica sinais pendentes
5. Sinais Assíncronos e Reentrância
Handlers de sinais executam de forma assíncrona, criando problemas de reentrância. Apenas funções async-signal-safe devem ser usadas dentro de handlers.
Funções seguras (async-signal-safe):
- write(), read(), open(), close()
- _exit(), _Exit()
- sigprocmask(), sigaction()
- kill(), raise()
- wait(), waitpid()
Funções NÃO seguras:
- printf(), fprintf(), sprintf()
- malloc(), free()
- pthread_*()
Boas práticas:
- Manter handlers mínimos (apenas alterar uma flag)
- Usar volatile sig_atomic_t para variáveis compartilhadas
- Evitar operações complexas dentro do handler
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
volatile sig_atomic_t sigint_count = 0;
void handler(int sig) {
sigint_count++; // Operação atômica segura
}
int main() {
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
printf("Pressione Ctrl+C algumas vezes...\n");
while(sigint_count < 3) {
pause();
}
printf("\nSinal SIGINT recebido %d vezes. Saindo...\n", sigint_count);
return 0;
}
6. Envio de Sinais Entre Processos
A função kill() permite enviar sinais para outros processos:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// Processo filho
printf("Filho (PID %d) aguardando sinal...\n", getpid());
pause();
printf("Filho recebeu sinal!\n");
exit(0);
} else if (pid > 0) {
// Processo pai
sleep(2);
printf("Pai enviando SIGTERM para filho (PID %d)\n", pid);
kill(pid, SIGTERM);
// Envia sinal para grupo de processos
// kill(-pgid, SIGUSR1);
wait(NULL);
printf("Filho terminou.\n");
}
return 0;
}
Outras funções de envio:
- raise(int sig): Envia sinal para o próprio processo
- alarm(unsigned int seconds): Agenda SIGALRM para o futuro
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler_alarm(int sig) {
write(STDOUT_FILENO, "Tempo esgotado!\n", 16);
_exit(1);
}
int main() {
signal(SIGALRM, handler_alarm);
alarm(5); // SIGALRM em 5 segundos
printf("Operação com timeout de 5 segundos...\n");
// Simula operação bloqueante
while(1) {
pause();
}
return 0;
}
7. Exemplos Práticos e Casos de Uso
Tratamento de SIGINT para limpeza de recursos
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
volatile sig_atomic_t cleanup_flag = 0;
void cleanup_handler(int sig) {
cleanup_flag = 1;
}
int main() {
struct sigaction sa;
sa.sa_handler = cleanup_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
printf("Executando... Pressione Ctrl+C para finalizar\n");
while(!cleanup_flag) {
// Trabalho normal do programa
sleep(1);
printf(".");
fflush(stdout);
}
printf("\nRealizando limpeza...\n");
// Fechar arquivos, liberar memória, etc.
printf("Recursos liberados. Saindo.\n");
return 0;
}
Sincronização pai-filho via SIGCHLD
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
volatile sig_atomic_t child_exit = 0;
void child_handler(int sig) {
child_exit = 1;
}
int main() {
struct sigaction sa;
sa.sa_handler = child_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa, NULL);
pid_t pid = fork();
if (pid == 0) {
sleep(3);
printf("Filho terminando...\n");
exit(42);
}
printf("Pai aguardando filho (PID %d)...\n", pid);
while(!child_exit) {
pause();
}
int status;
wait(&status);
printf("Filho terminou com status %d\n", WEXITSTATUS(status));
return 0;
}
Referências
- The GNU C Library: Signal Handling — Documentação oficial da glibc sobre tratamento de sinais, incluindo funções signal() e sigaction()
- Linux man page: signal(7) — Visão completa dos sinais no Linux, comportamentos e lista de funções async-signal-safe
- Linux man page: sigaction(2) — Documentação detalhada da chamada de sistema sigaction() com exemplos
- Beej's Guide to Unix IPC: Signals — Tutorial prático sobre comunicação entre processos usando sinais em C
- Advanced Programming in the UNIX Environment: Signal Handling — Capítulo 10 do livro clássico de Stevens, abordando tratamento avançado de sinais