Pular para o conteúdo principal

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ções WNOHANG e WUNTRACED e interpretar o status retornado com as macros WIFEXITED, 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:

HerdaNão herda
Espaço de endereçamento (cópia COW)PID (recebe novo PID)
Descritores de arquivo abertosPending signals
Variáveis de ambienteBloqueios 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 sinaisSemá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 torna fork() 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);
}

exec não retorna em caso de sucesso. O código após exec só executa se exec falhar.


waitpid() em profundidade

/* Somente leitura — assinatura */
pid_t waitpid(pid_t pid, int *status, int options);

Valores de pid:

ValorComportamento
> 0Aguarda o filho com esse PID específico
-1Aguarda qualquer filho (equivale a wait())
0Aguarda qualquer filho no mesmo grupo de processos
< -1Aguarda qualquer filho cujo PGID = `

Opções principais:

FlagEfeito
0Bloqueia até o filho terminar
WNOHANGRetorna imediatamente (0) se nenhum filho terminou
WUNTRACEDTambém retorna se filho for parado por sinal
WCONTINUEDRetorna 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 (0255); 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ódigoProcessos criadosTotal (incluindo original)
fork()12
fork(); fork()34
fork(); fork(); fork()78
if(fork()) fork();23

Exercícios

Questões dissertativas

Q1

Qual a diferença entre execv() e execvp()? Em que situação execvp() é mais conveniente? Quando execve() é necessário?

Q2

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?

Q3

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().

Q4

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()?

Q5Difícil

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.

Q6

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.

Q7

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?

Q8Difícil

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?

Q9

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ê.

Q10

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

Quiz10 questões

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.

Thumbnail do vídeo: Operating Systems — Full Course (25h)25:00:00
ENAprofundamento

Operating Systems — Full Course (25h)

freeCodeCamp.org

Para esta aula, foque nos módulos sobre Process Creation e fork/exec/wait.