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():-1indica erro (falta de memória, limite de processos atingido). - Evitar processos zumbis: use
wait()ouwaitpid()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 chamoufork()é duplicada. Em ambientes com threads, considere usarposix_spawn()ouclone()com cuidado. - Evite usar
fork()em loops sem limite: pode esgotar rapidamente os recursos do sistema.
Referências
- Manual da chamada fork (Linux man-pages) — Documentação oficial completa sobre
fork(), incluindo erros e exemplos. - Manual da chamada exec (Linux man-pages) — Detalhes sobre a família de funções
exec. - Manual da chamada wait (Linux man-pages) — Documentação de
wait()ewaitpid()com macros de status. - Manual da chamada pipe (Linux man-pages) — Referência para criação de pipes anônimos.
- Beej's Guide to Unix IPC — Tutorial prático sobre comunicação entre processos, incluindo forks, pipes e sinais.
- The Linux Programming Interface (Michael Kerrisk) — Livro de referência com capítulos detalhados sobre processos, forks, exec e sincronização.