Signal handling seguro: sigaction e signalfd

1. Introdução aos Sinais em Unix/Linux

Sinais são interrupções assíncronas enviadas a um processo para notificar eventos específicos. Eles podem ser gerados pelo kernel (como SIGSEGV para falha de segmentação), pelo usuário (Ctrl+C envia SIGINT), ou por outros processos via kill(). Quando um sinal é recebido, o fluxo normal do programa é pausado e um manipulador (handler) é executado.

Sinais comuns incluem:
- SIGINT (2): Interrupção do terminal (Ctrl+C)
- SIGTERM (15): Solicitação de término
- SIGSEGV (11): Violação de memória
- SIGCHLD (17): Filho terminou ou parou
- SIGHUP (1): Hangup do terminal

O principal problema com sinais é a reentrância: manipuladores podem interromper funções não reentrantes como malloc() ou printf(), causando deadlocks ou corrupção de dados. Além disso, a função signal() tradicional tem comportamento inconsistente entre plataformas.

2. A Função sigaction: Controle Avançado sobre Manipuladores

sigaction() é a alternativa moderna e portável a signal(). Ela oferece controle fino sobre o comportamento do manipulador através da estrutura struct sigaction:

struct sigaction {
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int      sa_flags;
    void     (*sa_restorer)(void);
};

Diferenças entre signal() e sigaction():
- signal() pode redefinir o manipulador para SIG_DFL após o primeiro sinal
- sigaction() permite bloquear outros sinais durante a execução do handler
- sigaction() é POSIX e portável; signal() tem comportamento diferente no System V vs BSD

Exemplo prático: registrando um manipulador para SIGINT:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>

volatile sig_atomic_t keep_running = 1;

void handle_sigint(int sig) {
    keep_running = 0;
}

int main() {
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = handle_sigint;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction");
        return 1;
    }

    while (keep_running) {
        printf("Trabalhando... PID: %d\n", getpid());
        sleep(1);
    }
    printf("\nSinal recebido. Encerrando.\n");
    return 0;
}

3. Configurações de Flags e Máscaras de Sinais

Flags importantes:
- SA_RESTART: Reinicia chamadas de sistema interrompidas (como read())
- SA_SIGINFO: Usa sa_sigaction em vez de sa_handler, fornecendo info estendida
- SA_NOCLDSTOP: Não gera SIGCHLD quando filhos param
- SA_RESETHAND: Reseta o manipulador para SIG_DFL após execução

Uso de sigprocmask() para bloquear sinais durante regiões críticas:

sigset_t block_set;
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT);
sigaddset(&block_set, SIGTERM);

// Bloqueia SIGINT e SIGTERM
sigprocmask(SIG_BLOCK, &block_set, NULL);

// Região crítica - sinais bloqueados
// ... operações seguras ...

// Desbloqueia
sigprocmask(SIG_UNBLOCK, &block_set, NULL);

A máscara em sigaction (sa_mask) define sinais automaticamente bloqueados durante a execução do manipulador, prevenindo interrupções aninhadas:

struct sigaction sa;
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGTERM); // Bloqueia SIGTERM durante handler
sigaddset(&sa.sa_mask, SIGHUP);  // Bloqueia SIGHUP também

4. Manipuladores com Informação Estendida (SA_SIGINFO)

Com SA_SIGINFO, o handler recebe uma estrutura siginfo_t com detalhes do sinal:

#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <stdlib.h>

void handle_sigsegv(int sig, siginfo_t *info, void *context) {
    fprintf(stderr, "Sinal %d recebido\n", sig);
    fprintf(stderr, "Endereço da falha: %p\n", info->si_addr);
    fprintf(stderr, "PID do emissor: %d\n", info->si_pid);
    _exit(1);
}

int main() {
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_sigaction = handle_sigsegv;
    sa.sa_flags = SA_SIGINFO;
    sigemptyset(&sa.sa_mask);

    if (sigaction(SIGSEGV, &sa, NULL) == -1) {
        perror("sigaction");
        return 1;
    }

    // Causa falha proposital
    int *p = NULL;
    *p = 42;

    return 0;
}

A estrutura siginfo_t fornece:
- si_signo: Número do sinal
- si_errno: Código de erro
- si_code: Código de origem do sinal
- si_pid: PID do processo emissor
- si_uid: UID do processo emissor
- si_addr: Endereço da falha (para SIGSEGV, SIGBUS, etc.)
- si_value: Valor inteiro ou ponteiro (para sinais com sigqueue())

5. Introdução ao signalfd: Tratamento de Sinais como Eventos de Arquivo

signalfd() converte sinais em descritores de arquivo, permitindo tratamento síncrono via read():

#include <sys/signalfd.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGTERM);

    // Bloqueia os sinais para que não sejam tratados normalmente
    if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1) {
        perror("sigprocmask");
        return 1;
    }

    // Cria o descritor signalfd
    int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
    if (sfd == -1) {
        perror("signalfd");
        return 1;
    }

    struct signalfd_siginfo fdsi;
    printf("Aguardando sinais... PID: %d\n", getpid());

    while (1) {
        ssize_t s = read(sfd, &fdsi, sizeof(fdsi));
        if (s != sizeof(fdsi)) {
            perror("read");
            break;
        }

        if (fdsi.ssi_signo == SIGINT) {
            printf("\nSIGINT recebido! Encerrando.\n");
            break;
        } else if (fdsi.ssi_signo == SIGTERM) {
            printf("\nSIGTERM recebido! Encerrando.\n");
            break;
        }
    }

    close(sfd);
    return 0;
}

A estrutura signalfd_siginfo contém:
- ssi_signo: Número do sinal
- ssi_errno: Código de erro
- ssi_code: Código de origem
- ssi_pid: PID do emissor
- ssi_uid: UID do emissor
- ssi_fd: Descritor de arquivo associado
- ssi_int / ssi_ptr: Valor ou ponteiro enviado

6. Integração com Loops de Eventos (I/O Multiplexing)

A grande vantagem do signalfd é integrar sinais com loops de I/O multiplexado como epoll:

#include <sys/epoll.h>
#include <sys/signalfd.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    // Configurar signalfd
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGTERM);
    sigprocmask(SIG_BLOCK, &mask, NULL);

    int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
    if (sfd == -1) {
        perror("signalfd");
        return 1;
    }

    // Criar epoll
    int epfd = epoll_create1(0);
    if (epfd == -1) {
        perror("epoll_create1");
        return 1;
    }

    // Adicionar signalfd ao epoll
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = sfd;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev) == -1) {
        perror("epoll_ctl");
        return 1;
    }

    printf("Servidor rodando. PID: %d\n", getpid());
    printf("Aguardando eventos (incluindo sinais)...\n");

    struct epoll_event events[10];
    struct signalfd_siginfo fdsi;

    while (1) {
        int nfds = epoll_wait(epfd, events, 10, -1);
        if (nfds == -1) {
            perror("epoll_wait");
            break;
        }

        for (int i = 0; i < nfds; i++) {
            if (events[i].data.fd == sfd) {
                // Sinal recebido
                ssize_t s = read(sfd, &fdsi, sizeof(fdsi));
                if (s != sizeof(fdsi)) continue;

                if (fdsi.ssi_signo == SIGINT || fdsi.ssi_signo == SIGTERM) {
                    printf("Sinal de término recebido. Saindo...\n");
                    close(sfd);
                    close(epfd);
                    return 0;
                }
                printf("Sinal %d recebido (ignorado)\n", fdsi.ssi_signo);
            } else {
                // Outros descritores (ex: sockets)
                printf("Evento no fd %d\n", events[i].data.fd);
            }
        }
    }

    return 0;
}

7. Boas Práticas e Armadilhas Comuns

Nunca usar funções não reentrantes em handlers tradicionais:
- Evite: printf(), malloc(), free(), fprintf(), strtok()
- Use apenas chamadas seguras: write(), sig_atomic_t, _exit()

Variáveis voláteis e sig_atomic_t:

volatile sig_atomic_t flag = 0;

void handler(int sig) {
    flag = 1; // Seguro: atribuição atômica
}

Sinais síncronos vs assíncronos:
- Sinais síncronos (SIGSEGV, SIGFPE) são gerados pelo próprio processo e podem ser tratados com mais segurança
- Sinais assíncronos (SIGINT, SIGTERM) podem chegar a qualquer momento, exigindo cuidado redobrado

Preferir signalfd para código moderno: Elimina handlers assíncronos completamente, permitindo tratamento síncrono e seguro dentro do loop principal.

Sempre verificar retornos: sigaction(), signalfd(), sigprocmask() podem falhar; verifique sempre.

Referências