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