Fork e processos filhos

1. Introdução ao Conceito de Processos no Linux

Um processo é uma instância de um programa em execução. Cada processo possui um identificador único chamado PID (Process ID), um espaço de endereçamento próprio (memória, pilha, heap) e um contexto de execução (registradores, contador de programa). No Linux, os processos organizam-se em uma hierarquia: todo processo (exceto o init, PID 1) tem um processo pai. Essa estrutura forma uma árvore de processos.

As chamadas de sistema (system calls) são funções fornecidas pelo kernel que permitem que programas em user mode interajam com o hardware e o sistema operacional. fork(), exec(), wait() e pipe() são exemplos de chamadas de sistema. Diferem das funções comuns da biblioteca C (como printf()) por envolverem uma transição para o kernel.

2. A Chamada de Sistema fork()

A chamada de sistema fork() é a principal forma de criar um novo processo no Linux. Seu protótipo é:

#include <unistd.h>
pid_t fork(void);

O comportamento do fork() é notável: ele duplica o processo atual, criando um novo processo chamado filho. O filho é uma cópia exata do pai, incluindo o código, dados, pilha, heap e descritores de arquivo. A diferença crucial está no valor de retorno:

  • No pai, fork() retorna o PID do filho (um número positivo).
  • No filho, fork() retorna 0.
  • Em caso de erro, fork() retorna -1.

Exemplo mínimo:

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

int main() {
    pid_t pid = fork();

    if (pid == -1) {
        perror("fork falhou");
        return 1;
    }

    if (pid == 0) {
        printf("Sou o filho. PID = %d\n", getpid());
    } else {
        printf("Sou o pai. PID = %d, filho = %d\n", getpid(), pid);
    }

    return 0;
}

3. Identificação de Processos: getpid() e getppid()

Para obter o PID do processo atual, usamos getpid(). Para saber o PID do processo pai, usamos getppid(). Ambas estão declaradas em <unistd.h>.

Exemplo prático:

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

int main() {
    pid_t pid = fork();

    if (pid == -1) {
        perror("fork falhou");
        return 1;
    }

    if (pid == 0) {
        printf("Filho: meu PID = %d, PID do pai = %d\n", getpid(), getppid());
    } else {
        printf("Pai: meu PID = %d, PID do filho = %d\n", getpid(), pid);
    }

    return 0;
}

A saída pode variar, mas a estrutura mostra claramente a relação pai-filho.

4. Sincronização com wait() e waitpid()

Quando um filho termina, ele se torna um "zumbi" até que o pai colete seu status de saída. Se o pai não chamar wait() ou waitpid(), o zumbi permanece na tabela de processos. Se o pai termina antes do filho, o filho se torna "órfão" e é adotado pelo init (PID 1).

wait() bloqueia até que qualquer filho termine:

#include <sys/wait.h>
pid_t wait(int *status);

waitpid() oferece mais controle:

pid_t waitpid(pid_t pid, int *status, int options);

Macros para interpretar o status:

  • WIFEXITED(status): verdadeiro se o filho terminou normalmente.
  • WEXITSTATUS(status): retorna o código de saída.
  • WIFSIGNALED(status): verdadeiro se o filho foi morto por um sinal.

Exemplo:

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

    if (pid == -1) {
        perror("fork");
        return 1;
    }

    if (pid == 0) {
        printf("Filho: vou dormir 2 segundos...\n");
        sleep(2);
        return 42;
    } else {
        int status;
        wait(&status);
        if (WIFEXITED(status)) {
            printf("Pai: filho terminou com código %d\n", WEXITSTATUS(status));
        }
    }

    return 0;
}

5. Execução de Novos Programas com exec()

Enquanto fork() duplica o processo, exec() substitui a imagem do processo atual por um novo programa. A família exec inclui:

  • execl(const char *path, const char *arg, ...)
  • execlp(const char *file, const char *arg, ...) (busca no PATH)
  • execv(const char *path, char *const argv[])
  • execvp(const char *file, char *const argv[]) (busca no PATH)

O padrão comum é fork() + exec(): o pai cria um filho com fork(), e o filho imediatamente chama exec() para rodar um novo programa.

Exemplo: executar ls -l em um filho:

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

    if (pid == -1) {
        perror("fork");
        return 1;
    }

    if (pid == 0) {
        // Filho executa ls
        execlp("ls", "ls", "-l", NULL);
        perror("execlp"); // Só chega aqui se execlp falhar
        return 1;
    } else {
        wait(NULL); // Pai espera o filho
        printf("Pai: filho terminou.\n");
    }

    return 0;
}

6. Tratamento de Sinais em Processos Filhos

Após fork(), o filho herda a tabela de sinais do pai, incluindo handlers configurados com signal() ou sigaction(). No entanto, o filho pode redefinir handlers sem afetar o pai.

Exemplo: ignorar SIGINT (Ctrl+C) apenas no filho:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

    if (pid == -1) {
        perror("fork");
        return 1;
    }

    if (pid == 0) {
        signal(SIGINT, SIG_IGN); // Filho ignora Ctrl+C
        printf("Filho: SIGINT ignorado. PID = %d\n", getpid());
        while (1) pause(); // Fica em loop
    } else {
        printf("Pai: PID = %d. Pressione Ctrl+C para testar.\n", getpid());
        sleep(3);
        kill(pid, SIGKILL); // Mata o filho
        wait(NULL);
    }

    return 0;
}

Cuidado: handlers compartilhados podem causar comportamento inesperado. É boa prática redefinir handlers no filho imediatamente após fork().

7. Comunicação Simples entre Pai e Filho com pipe()

Pipes anônimos permitem comunicação unidirecional entre processos relacionados. A função pipe(int fd[2]) cria um pipe: fd[0] para leitura, fd[1] para escrita.

Exemplo: filho envia uma mensagem para o pai:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main() {
    int fd[2];
    if (pipe(fd) == -1) {
        perror("pipe");
        return 1;
    }

    pid_t pid = fork();

    if (pid == -1) {
        perror("fork");
        return 1;
    }

    if (pid == 0) {
        // Filho escreve
        close(fd[0]); // Fecha leitura
        const char *msg = "Olá, pai!";
        write(fd[1], msg, strlen(msg) + 1);
        close(fd[1]);
    } else {
        // Pai lê
        close(fd[1]); // Fecha escrita
        char buffer[100];
        read(fd[0], buffer, sizeof(buffer));
        printf("Pai recebeu: %s\n", buffer);
        close(fd[0]);
        wait(NULL);
    }

    return 0;
}

Limitações: pipes são anônimos (sem nome no sistema de arquivos) e unidirecionais. Para comunicação bidirecional, são necessários dois pipes.

8. Boas Práticas e Erros Comuns

  • Sempre verificar o retorno de fork(): -1 indica erro (falta de memória, limite de processos atingido).
  • Evitar processos zumbis: use wait() ou waitpid() para coletar o status de saída dos filhos.
  • Fechar descritores de arquivo não utilizados: após fork(), o filho herda todos os descritores do pai. Feche os que não forem necessários para evitar vazamentos.
  • Cuidado com threads: fork() em programas multithread pode causar deadlocks, pois apenas a thread que chamou fork() é duplicada. Em ambientes com threads, considere usar posix_spawn() ou clone() com cuidado.
  • Evite usar fork() em loops sem limite: pode esgotar rapidamente os recursos do sistema.

Referências