Aula 07 — Prática: fork(), waitpid() e execve()
:::note Ambiente sugerido
Todos os exemplos foram testados em Linux (Ubuntu 22.04 / Debian 12) com gcc. Compile e execute — a prática é o objetivo desta aula.
:::
Objetivos
Ao final desta aula você deve ser capaz de:
- Usar corretamente todas as variantes da família
exec(execl,execv,execvp,execve) e escolher a mais adequada para cada situação. - Aplicar
waitpid()com as opçõesWNOHANGeWUNTRACEDe interpretar o status retornado com as macrosWIFEXITED,WEXITSTATUS,WIFSIGNALED,WTERMSIG. - Criar e gerenciar múltiplos filhos em loop, coletando seus status individualmente.
- Implementar comunicação pai↔filho via
pipe()no padrão fork-exec. - Distinguir processo zumbi de processo órfão e escrever código que evite ambos.
- Raciocinar sobre quantos processos são criados em cadeias de
fork()aninhadas.
Conteúdo
Revisão: o que fork() herda
Após fork(), o filho herda do pai:
| Herda | Não herda |
|---|---|
| Espaço de endereçamento (cópia COW) | PID (recebe novo PID) |
| Descritores de arquivo abertos | Pending signals |
| Variáveis de ambiente | Bloqueios de memória (mlock) |
Diretório de trabalho (cwd) | Timers (alarm, setitimer) |
| Permissões (UID, GID) | Threads (apenas o thread que chamou fork é duplicado) |
| Máscara de sinais | Semáforos POSIX com O_CLOEXEC |
Copy-on-write (COW): no Linux,
fork()não copia fisicamente o espaço de endereçamento imediatamente. As páginas são compartilhadas como somente-leitura; uma cópia só é feita quando o processo que a modifica realmente escreve na página. Isso tornafork()extremamente eficiente.
A família exec
execve() é a syscall base; as demais são wrappers da libc:
execve(path, argv[], envp[]) ← syscall direta, controle total
execv (path, argv[]) ← sem envp (herda env do processo)
execvp(file, argv[]) ← busca no PATH (sem path absoluto)
execl (path, arg0, ..., NULL) ← argumentos como lista variádica
execlp(file, arg0, ..., NULL) ← lista variádica + busca PATH
execle(path, arg0,..., NULL, envp[]) ← lista + envp explícito
Regra mnemônica:
l= lista de argumentos (variádica, terminada em NULL)v= vetor de argumentos (char *argv[])p= busca no PATH (não precisa de caminho absoluto)e= envp explícito
Programa 1: comparando execvp e execve
/* Executável — família exec na prática */
/* Compile: gcc -o exec_demo exec_demo.c */
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
if (argc < 2) {
fprintf(stderr, "Uso: %s [v|vp|ve|l|lp]\n", argv[0]);
exit(1);
}
char *cmd_args[] = {"ls", "-lh", "/tmp", NULL};
/* ambiente mínimo para execve */
char *env[] = {"PATH=/bin:/usr/bin", "HOME=/tmp", NULL};
switch (argv[1][0]) {
case 'v': /* execv — caminho absoluto, vetor */
execv("/bin/ls", cmd_args);
break;
case 'p': /* execvp — busca PATH, vetor */
execvp("ls", cmd_args);
break;
case 'e': /* execve — caminho absoluto, vetor, env explícito */
execve("/bin/ls", cmd_args, env);
break;
case 'l': /* execl — caminho absoluto, lista */
execl("/bin/ls", "ls", "-lh", "/tmp", NULL);
break;
}
/* chegou aqui = exec falhou */
perror("exec");
exit(1);
}
execnão retorna em caso de sucesso. O código apósexecsó executa seexecfalhar.
waitpid() em profundidade
/* Somente leitura — assinatura */
pid_t waitpid(pid_t pid, int *status, int options);
Valores de pid:
| Valor | Comportamento |
|---|---|
> 0 | Aguarda o filho com esse PID específico |
-1 | Aguarda qualquer filho (equivale a wait()) |
0 | Aguarda qualquer filho no mesmo grupo de processos |
< -1 | Aguarda qualquer filho cujo PGID = ` |
Opções principais:
| Flag | Efeito |
|---|---|
0 | Bloqueia até o filho terminar |
WNOHANG | Retorna imediatamente (0) se nenhum filho terminou |
WUNTRACED | Também retorna se filho for parado por sinal |
WCONTINUED | Retorna se filho parado foi retomado com SIGCONT |
Macros de status:
/* Somente leitura — interpretando o status de waitpid */
WIFEXITED(s) → verdadeiro se filho terminou com exit()
WEXITSTATUS(s) → código de saída (0–255); use só se WIFEXITED
WIFSIGNALED(s) → verdadeiro se filho foi morto por sinal
WTERMSIG(s) → número do sinal; use só se WIFSIGNALED
WIFSTOPPED(s) → filho foi parado (SIGSTOP/SIGTSTP)
WSTOPSIG(s) → sinal que parou o filho
Programa 2: múltiplos filhos com waitpid individual
/* Executável — criar N filhos e coletar cada um pelo PID */
/* Compile: gcc -o multi_filhos multi_filhos.c */
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#define N_FILHOS 4
int main(void)
{
pid_t pids[N_FILHOS]; /* armazena PID de cada filho */
int status;
/* --- cria N filhos --- */
for (int i = 0; i < N_FILHOS; i++) {
pids[i] = fork();
if (pids[i] < 0) {
perror("fork");
exit(1);
}
if (pids[i] == 0) {
/* filho: dorme i+1 segundos e sai com código i */
printf("[filho %d] PID=%d dormindo %ds\n", i, getpid(), i+1);
sleep(i + 1);
exit(i); /* código de saída = índice do filho */
}
/* pai continua o loop, cria o próximo filho */
}
/* --- pai aguarda cada filho PELO SEU PID --- */
for (int i = 0; i < N_FILHOS; i++) {
pid_t terminado = waitpid(pids[i], &status, 0);
if (WIFEXITED(status)) {
printf("[pai] filho PID=%d terminou normalmente, código=%d\n",
terminado, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("[pai] filho PID=%d morto por sinal %d\n",
terminado, WTERMSIG(status));
}
}
puts("[pai] todos os filhos coletados.");
return 0;
}
Comunicação pai↔filho com pipe()
pipe() cria um par de descritores de arquivo: fd[0] (leitura) e fd[1] (escrita). Após fork(), pai e filho herdam ambos. O padrão correto é fechar a ponta não usada em cada processo.
/* Executável — pipe unidirecional pai → filho */
/* Compile: gcc -o pipe_demo pipe_demo.c */
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
int fd[2]; /* fd[0]=leitura, fd[1]=escrita */
pid_t pid;
char msg[] = "Olá do pai!";
char buf[128];
if (pipe(fd) == -1) { perror("pipe"); exit(1); }
pid = fork();
if (pid < 0) { perror("fork"); exit(1); }
if (pid == 0) {
/* ─── FILHO: lê do pipe ─────────────── */
close(fd[1]); /* fecha a ponta de escrita */
ssize_t n = read(fd[0], buf, sizeof(buf) - 1);
buf[n] = '\0';
printf("[filho] recebeu: '%s'\n", buf);
close(fd[0]);
exit(0);
}
/* ─── PAI: escreve no pipe ──────────────── */
close(fd[0]); /* fecha a ponta de leitura */
write(fd[1], msg, strlen(msg));
close(fd[1]); /* fecha = envia EOF ao filho */
waitpid(pid, NULL, 0);
puts("[pai] filho concluído.");
return 0;
}
Regra dos pipes: sempre feche a ponta não usada em cada processo. Se o pai não fechar fd[0], o filho nunca recebe EOF ao ler. Se o filho não fechar fd[1], o pai nunca recebe EOF se precisar ler.
Zumbi vs. Órfão — revisão e prevenção
Processo Zumbi:
filho terminou → kernel mantém PCB (status de saída)
pai NÃO chamou wait() → zumbi ocupa entrada na tabela
Solução: sempre chamar waitpid() após fork()
Processo Órfão:
pai terminou antes do filho → filho "perde" o pai
Solução do SO: init (PID 1) adota todos os órfãos
Não é crítico, mas pode indicar bug de design
Prevenção de zumbis com SIGCHLD:
/* Somente leitura — ignorar SIGCHLD evita zumbis automaticamente */
#include <signal.h>
signal(SIGCHLD, SIG_IGN); /* kernel coleta filhos sem wait() */
/* ou instale um handler que chame waitpid(-1, NULL, WNOHANG) */
Contando processos criados por fork() encadeado
Regra: cada fork() dobra o número de processos existentes naquele ponto. Em uma sequência de n forks sem condicionais, o número total é 2ⁿ.
Exemplo com condicional — análise cuidadosa:
/* Somente leitura — quantos processos executam printf? */
pid_t p = fork(); /* P0 cria P1 → 2 processos */
if (p == 0) { /* apenas P1 (filho) entra */
fork(); /* P1 cria P2 → agora 3 processos total */
}
printf("processo %d\n", getpid()); /* P0, P1 e P2 executam */
Resposta: 3 processos executam o printf.
Tabela de contagem:
| Código | Processos criados | Total (incluindo original) |
|---|---|---|
fork() | 1 | 2 |
fork(); fork() | 3 | 4 |
fork(); fork(); fork() | 7 | 8 |
if(fork()) fork(); | 2 | 3 |
Exercícios
Questões dissertativas
Qual a diferença entre execv() e execvp()? Em que situação execvp() é mais conveniente? Quando execve() é necessário?
O código abaixo cria 3 filhos e usa wait() para coletar. Qual problema pode ocorrer se os filhos terminarem fora de ordem? Como waitpid() resolve isso?
Trace a execução do código: fork(); fork(); fork(). Desenhe a árvore de processos e diga quantos processos executam a linha após o terceiro fork().
Explique o que é um processo zumbi. Por que o kernel mantém o PCB do filho após ele chamar exit()? O que acontece se o pai nunca chamar wait()?
Implemente um mini-shell simplificado: leia um comando do usuário, execute-o via fork+execvp, aguarde a conclusão e mostre o código de saída. Mostre o código comentado.
O que WNOHANG faz em waitpid()? Escreva um trecho de código que monitore múltiplos filhos sem bloquear o pai, usando WNOHANG em loop.
Após fork(), pai e filho herdam os mesmos descritores de arquivo abertos. O que acontece se tanto o pai quanto o filho escrevem no mesmo fd (ex.: stdout) sem sincronização?
Analise: fork() é chamado dentro de um processo multithreaded (com pthreads). Apenas o thread que chamou fork() é duplicado no filho. Que problema sério isso pode causar com mutexes?
Como implementar um pipeline de dois comandos (equivalente a 'ls | grep .c') usando fork(), pipe() e execvp()? Descreva os passos e indique quais fds devem ser fechados e por quê.
Qual o código de saída padrão por convenção para 'sucesso' e 'falha' em programas Unix? O que a constante exit(127) significa especificamente?
Quiz de múltipla escolha
1. Qual variante da família exec busca o executável nas pastas do PATH sem precisar do caminho absoluto?
- a)execv()
- b)execve()
- c)execl()
- d)execvp()
- e)execle()
2. Um filho termina com exit(42). O pai usa waitpid(). Qual expressão C retorna o valor 42?
- a)WIFSIGNALED(status)
- b)WTERMSIG(status)
- c)WEXITSTATUS(status)
- d)WIFEXITED(status)
- e)WSTOPSIG(status)
3. waitpid(pid, &status, WNOHANG) retornou 0. O que isso significa?
- a)O filho com PID=pid terminou com código 0
- b)Erro — o filho não existe
- c)O filho ainda está em execução — ninguém terminou ainda
- d)O filho foi morto por sinal 0
- e)O pai deve chamar waitpid novamente imediatamente
4. Quantos processos são criados (além do original) pelo código: if(fork()) fork();
- a)1 processo
- b)2 processos
- c)3 processos
- d)4 processos
- e)Depende do escalonador
5. Por que é essencial fechar fd[1] (escrita) no processo que lê de um pipe?
- a)Para evitar que o leitor escreva acidentalmente no pipe
- b)Para liberar memória do kernel
- c)Um pipe só envia EOF quando todos os fds de escrita estão fechados — manter fd[1] aberto no leitor impede o EOF
- d)Para evitar que o pipe estoure seu buffer
- e)Não é necessário — é apenas uma boa prática
6. O que acontece imediatamente quando execvp() é chamado com sucesso?
- a)Um novo processo é criado e o atual continua
- b)O processo atual é duplicado e o duplicado executa o novo programa
- c)A imagem de memória do processo atual é completamente substituída pelo novo programa
- d)O processo atual é suspenso enquanto o novo programa executa
- e)O processo retorna ao ponto após execvp() quando o novo programa terminar
7. Um processo zumbi ocupa qual recurso do sistema de forma significativa?
- a)CPU — continua consumindo ciclos de processamento
- b)Memória RAM — mantém toda a imagem do processo
- c)Entrada na tabela de processos (PID) — consome um slot da tabela de PIDs
- d)Descritores de arquivo — mantém todos os fds abertos
- e)Espaço em disco — salva o core dump automaticamente
8. Qual é a convenção Unix para o código de saída de 'comando não encontrado' ao usar exec() em um shell?
- a)exit(0)
- b)exit(1)
- c)exit(2)
- d)exit(127)
- e)exit(255)
9. Copy-on-write (COW) no Linux faz fork() ser mais eficiente. Como?
- a)Copia apenas as variáveis modificadas pelo filho
- b)Marca as páginas como compartilhadas somente-leitura; copia fisicamente apenas quando pai ou filho modifica uma página
- c)Usa compressão para reduzir o tamanho da cópia
- d)Copia apenas o código (text segment), não os dados
- e)Executa fork() em background sem suspender o processo pai
10. Após fork(), o filho chama execvp() que falha. O filho chama exit(1). O pai estava em waitpid(pid, &status, 0). O que o pai verá em status?
- a)WIFSIGNALED(status) == true, WTERMSIG(status) == 1
- b)WIFEXITED(status) == true, WEXITSTATUS(status) == 1
- c)WIFEXITED(status) == true, WEXITSTATUS(status) == 0
- d)waitpid retorna -1 porque execvp falhou
- e)O status é indeterminado — depende do valor de retorno de execvp
Referências
Principais (essenciais)
- SILBERSCHATZ, A.; GALVIN, P. B.; GAGNE, G. Fundamentos de Sistemas Operacionais. 9. ed. LTC, 2015. §2.4, §3.3.
man 2 fork,man 2 execve,man 2 waitpid,man 2 pipe— páginas de manual Linux.
Aprofundamento (opcionais)
- KERRISK, M. The Linux Programming Interface. No Starch Press, 2010. Cap. 24–27 (criação de processos, exec, wait).
Recursos Complementares
Opcional — Vídeos para reforçar os conceitos desta aula.
25:00:00Operating Systems — Full Course (25h)
freeCodeCamp.org
Para esta aula, foque nos módulos sobre Process Creation e fork/exec/wait.