Daemonization: criando processos em background

1. O que é um Daemon? Conceitos Fundamentais

Um daemon é um processo de computador executado em background, sem associação a um terminal interativo. Diferente de um programa comum que termina quando o usuário fecha o terminal, um daemon permanece ativo por longos períodos, muitas vezes até o desligamento do sistema.

A principal diferença entre um processo normal, um job em background e um daemon é o controle do terminal. Um job em background ainda pertence à sessão do terminal e pode receber sinais dele. Um daemon, por outro lado, não possui terminal controlador e opera de forma independente.

Casos de uso típicos incluem servidores web (httpd), serviços de banco de dados (mysqld), monitores de sistema, agentes de backup e serviços de rede. Em sistemas Unix-like, os daemons tradicionalmente têm nomes terminando com a letra "d".

2. Passos Clássicos da Daemonização

O processo de daemonização segue uma sequência bem definida de chamadas de sistema. Abaixo está a implementação passo a passo:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <syslog.h>

void daemonizar() {
    pid_t pid;

    /* Primeiro fork: cria processo filho */
    pid = fork();
    if (pid < 0) {
        exit(EXIT_FAILURE);
    }
    /* Processo pai encerra */
    if (pid > 0) {
        exit(EXIT_SUCCESS);
    }

    /* Cria nova sessão e grupo de processos */
    if (setsid() < 0) {
        exit(EXIT_FAILURE);
    }

    /* Segundo fork opcional: impede aquisição de terminal */
    pid = fork();
    if (pid < 0) {
        exit(EXIT_FAILURE);
    }
    if (pid > 0) {
        exit(EXIT_SUCCESS);
    }

    /* Redireciona descritores de arquivo padrão */
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    /* Abre /dev/null para substituir os descritores */
    open("/dev/null", O_RDWR);  /* stdin */
    dup(0);                     /* stdout */
    dup(0);                     /* stderr */
}

O primeiro fork() garante que o processo não seja líder de sessão, permitindo que setsid() funcione corretamente. O setsid() cria uma nova sessão, desassociando o processo do terminal controlador. O segundo fork() é opcional, mas previne que o processo readquira um terminal.

3. Gerenciamento de PID e Arquivos de Lock

Para controlar múltiplas instâncias e permitir gerenciamento externo, escrevemos o PID em um arquivo:

#include <string.h>

#define PID_FILE "/var/run/meu_daemon.pid"

void escrever_pid_file() {
    FILE *fp = fopen(PID_FILE, "w");
    if (fp == NULL) {
        syslog(LOG_ERR, "Não foi possível criar %s", PID_FILE);
        exit(EXIT_FAILURE);
    }
    fprintf(fp, "%d\n", getpid());
    fclose(fp);
}

int verificar_instancia_unica() {
    int fd = open(PID_FILE, O_CREAT | O_RDWR, 0644);
    if (fd < 0) {
        return -1;
    }

    /* Tenta travar o arquivo */
    if (lockf(fd, F_TLOCK, 0) < 0) {
        close(fd);
        return -1;  /* Já existe outra instância */
    }

    /* Escreve o PID atual */
    char pid_str[16];
    snprintf(pid_str, sizeof(pid_str), "%d\n", getpid());
    write(fd, pid_str, strlen(pid_str));

    return fd;  /* Retorna o file descriptor para manter lock */
}

O uso de lockf() ou flock() impede que duas instâncias do daemon sejam executadas simultaneamente, prevenindo corrupção de dados e conflitos de recursos.

4. Mudança de Diretório e Umask

Após a daemonização, é essencial ajustar o diretório de trabalho e a máscara de permissões:

/* Muda para diretório raiz para liberar ponto de montagem */
if (chdir("/") < 0) {
    syslog(LOG_ERR, "Falha ao mudar para /");
    exit(EXIT_FAILURE);
}

/* Reseta umask para controle explícito de permissões */
umask(0);

Mudar para / evita que o daemon mantenha ocupado um diretório em um sistema de arquivos montado que poderia ser desmontado. O umask(0) garante que as permissões dos arquivos criados pelo daemon sejam exatamente as especificadas nas chamadas open() ou creat().

5. Tratamento de Sinais em Daemons

Daemons precisam tratar sinais adequadamente para funcionar de forma confiável:

#include <string.h>

volatile sig_atomic_t fim_execucao = 0;
volatile sig_atomic_t recarregar_config = 0;

void handler_sinal(int sig) {
    switch (sig) {
        case SIGTERM:
            fim_execucao = 1;
            break;
        case SIGHUP:
            recarregar_config = 1;
            break;
        case SIGPIPE:
            /* Ignorar silenciosamente */
            break;
    }
}

void configurar_manipuladores_sinal() {
    struct sigaction sa;

    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = handler_sinal;
    sigemptyset(&sa.sa_mask);

    /* Usar sigaction() em vez de signal() para portabilidade */
    if (sigaction(SIGTERM, &sa, NULL) < 0) {
        syslog(LOG_ERR, "Falha ao configurar handler SIGTERM");
        exit(EXIT_FAILURE);
    }
    if (sigaction(SIGHUP, &sa, NULL) < 0) {
        syslog(LOG_ERR, "Falha ao configurar handler SIGHUP");
        exit(EXIT_FAILURE);
    }
    if (sigaction(SIGPIPE, &sa, NULL) < 0) {
        syslog(LOG_ERR, "Falha ao configurar handler SIGPIPE");
        exit(EXIT_FAILURE);
    }
}

O sigaction() é preferível a signal() por oferecer comportamento mais previsível entre diferentes sistemas Unix. O tratamento de SIGHUP permite recarregar configurações sem reiniciar o daemon, enquanto SIGTERM possibilita um encerramento gracioso.

6. Logging e Redirecionamento de Saída

Após redirecionar os descritores padrão para /dev/null, devemos usar syslog() para registro de eventos:

void inicializar_logging() {
    openlog("meu_daemon", LOG_PID | LOG_CONS, LOG_DAEMON);
    syslog(LOG_INFO, "Daemon iniciado com PID %d", getpid());
}

void registrar_mensagem(const char *msg) {
    syslog(LOG_INFO, "%s", msg);
}

Alternativamente, podemos redirecionar a saída para um arquivo específico:

void redirecionar_para_arquivo(const char *caminho) {
    FILE *log_file = freopen(caminho, "a", stdout);
    if (log_file == NULL) {
        syslog(LOG_ERR, "Falha ao abrir arquivo de log %s", caminho);
        exit(EXIT_FAILURE);
    }
    setbuf(log_file, NULL);  /* Desabilita bufferização */
}

7. Exemplo Completo de Daemon em C

Abaixo está um daemon funcional completo que incorpora todos os conceitos discutidos:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <syslog.h>
#include <string.h>

#define PID_FILE "/var/run/meu_daemon.pid"
#define INTERVALO_SLEEP 5

volatile sig_atomic_t fim_execucao = 0;

void handler_sinal(int sig) {
    if (sig == SIGTERM) {
        fim_execucao = 1;
    }
}

int main(int argc, char *argv[]) {
    /* === DAEMONIZAÇÃO === */
    pid_t pid = fork();
    if (pid < 0) exit(EXIT_FAILURE);
    if (pid > 0) exit(EXIT_SUCCESS);

    if (setsid() < 0) exit(EXIT_FAILURE);

    pid = fork();
    if (pid < 0) exit(EXIT_FAILURE);
    if (pid > 0) exit(EXIT_SUCCESS);

    /* === CONFIGURAÇÕES INICIAIS === */
    chdir("/");
    umask(0);

    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);
    open("/dev/null", O_RDWR);
    dup(0);
    dup(0);

    /* === LOGGING === */
    openlog("meu_daemon", LOG_PID | LOG_CONS, LOG_DAEMON);
    syslog(LOG_INFO, "Daemon iniciado");

    /* === PID FILE === */
    int pid_fd = open(PID_FILE, O_CREAT | O_RDWR, 0644);
    if (pid_fd < 0 || lockf(pid_fd, F_TLOCK, 0) < 0) {
        syslog(LOG_ERR, "Falha ao criar lock file");
        exit(EXIT_FAILURE);
    }
    dprintf(pid_fd, "%d\n", getpid());

    /* === SINAIS === */
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = handler_sinal;
    sigaction(SIGTERM, &sa, NULL);
    sigaction(SIGINT, &sa, NULL);

    /* === LOOP PRINCIPAL === */
    while (!fim_execucao) {
        syslog(LOG_INFO, "Daemon executando...");
        sleep(INTERVALO_SLEEP);
    }

    /* === LIMPEZA NA SAÍDA === */
    syslog(LOG_INFO, "Daemon finalizando");
    close(pid_fd);
    unlink(PID_FILE);
    closelog();

    return EXIT_SUCCESS;
}

8. Boas Práticas e Armadilhas Comuns

Ao implementar um daemon, algumas práticas são essenciais:

Verificação de retorno: Todas as chamadas de sistema devem ter seus retornos verificados. Ignorar falhas pode levar a comportamentos imprevisíveis.

Evitar printf(): Após redirecionar os descritores, printf() não terá efeito visível. Use syslog() ou escreva diretamente em arquivos de log.

Race conditions: A criação do arquivo PID deve ser atômica. O uso de lockf() com F_TLOCK garante que apenas uma instância obtenha o lock.

Compatibilidade com systemd: Sistemas modernos usam systemd, que gerencia daemons de forma diferente. Para compatibilidade, evite o segundo fork() e use sd_notify() quando disponível.

Inicialização de sinais: Configure os handlers de sinal antes de qualquer operação que possa gerar sinais, especialmente durante a inicialização.

Buffers: Desabilite a bufferização de saída (setbuf(stdout, NULL)) ou use fsync() após escritas importantes para garantir que os dados sejam persistidos em disco.

Referências