LRM Prof. Mantovani ← Aulas da disciplina
Semana 6 · Aula 6 de 14

U2 · Criação, término e a árvore de processos

Como processos nascem e morrem: fork e exec, hierarquia, órfãos e zumbis — com laboratório e simulador para montar a árvore com as próprias mãos.

📚 Sistemas Operacionais🧪 1 simulador(es)📝 mini-quiz ao final
Objetivos da aula

O que você vai aprender

1

Distinguir o papel de fork() e o de exec() na criação de um processo.

2

Explicar a hierarquia de processos e o destino dos órfãos.

3

Identificar a origem de um processo zumbi e como evitá-lo.

4

Comparar a criação dinâmica de processos com a alocação estática de tarefas em sistemas embarcados.

1 · Motivação

De onde vem um processo?

Na semana passada dissecamos um processo por dentro: o PCB, os estados, o custo da troca de contexto. Ficou uma pergunta em aberto — quem cria o primeiro? E os demais?

A resposta do Unix é elegante e estranha ao mesmo tempo: um processo só pode nascer sendo clonado de outro. Não existe "criar processo do zero". Essa decisão de projeto explica a árvore de processos, os zumbis e boa parte do comportamento do seu terminal.

2 · Mapa

Roteiro da aula

1 fork: o processo que vira dois
2 exec: trocar de programa sem trocar de processo
3 Término: wait, órfãos e zumbis
4 Simulador e laboratório
3 · Conceito

fork() e exec()

fork(). Cria um novo processo que é uma cópia do que chamou: mesmo código, mesmas variáveis, mesmos arquivos abertos. Devolve o PID do filho ao pai e 0 ao filho — é assim que cada um descobre quem é.
exec(). Substitui a imagem de memória do processo atual por outro programa. O PID não muda, o processo é o mesmo — trocou-se apenas o que ele executa.
🔑
São operações ortogonais. fork sem exec = dois processos rodando o mesmo programa (servidores fazem isso). exec sem fork = o processo vira outro programa (o shell faz isso ao rodar com exec).
4 · Explicação

Por que dois passos e não um?

Parece desperdício copiar um processo inteiro só para logo substituí-lo. E seria, se a cópia fosse literal — mas não é: o núcleo usa cópia sob escrita (copy-on-write). Pai e filho compartilham as mesmas páginas físicas, marcadas como somente leitura; a duplicação real só ocorre na página que algum dos dois escrever.

A vantagem de separar em dois passos aparece no intervalo entre eles: é ali, já dentro do filho e antes de virar outro programa, que o shell redireciona a saída, fecha descritores e ajusta permissões. Se criação e execução fossem um só passo, não haveria esse ponto de intervenção.

// como o shell executa "ls > saida.txt" pid = fork(); if (pid == 0) { // filho close(1); // fecha a saída padrão open("saida.txt", O_WRONLY); // agora o descritor 1 é o arquivo exec("/bin/ls"); // vira o ls, já redirecionado } else { // pai wait(NULL); // espera o filho terminar }
5 · Exemplo

Um fork, duas continuações

// quantas linhas este programa imprime? printf("A\n"); fork(); printf("B\n");

Imprime A uma vez e B duas vezes. Depois do fork existem dois processos, ambos posicionados na mesma linha, ambos seguindo daí em diante. O código é um só; as execuções, duas.

⚠️
Se houver dois fork() em sequência, são quatro processos ao final — a contagem dobra a cada chamada. Com três, oito. É pegadinha frequente de prova.
6 · Interativo

Passo a passo: o shell rodando um comando

Passo 1
O shell está bloqueado lendo o teclado. Você digita ls e confirma.
Passo 2
O shell chama fork(). Agora existem dois shells idênticos; um recebeu o PID do outro, o outro recebeu 0.
Passo 3
O filho (que recebeu 0) ajusta o que for preciso — redirecionamentos, descritores.
Passo 4
O filho chama exec("/bin/ls"). Mesmo PID, programa novo: ele deixa de ser shell e passa a ser o ls.
Passo 5
O pai chama wait() e fica Bloqueado até o filho terminar. Não gasta CPU nenhuma nessa espera.
Passo 6
O ls termina com exit(0). O núcleo acorda o pai, que recolhe o código de saída e volta a exibir o prompt.
7 · Conceito

A árvore e os seus acidentes

Árvore de processos. Como todo processo tem exatamente um pai, o conjunto forma uma árvore enraizada no PID 1.
Órfão. Processo cujo pai terminou antes dele. É adotado pelo init e segue funcionando normalmente.
Zumbi. Processo que já terminou mas cujo pai ainda não recolheu o código de saída com wait(). Não consome CPU nem memória, mas ocupa uma entrada na tabela de processos.
8 · Explicação

Por que o zumbi existe (e por que é culpa do pai)

Quando um processo termina, alguém precisa saber como ele terminou — deu certo? falhou com que código? Essa informação é do pai. Por isso o núcleo não descarta a entrada imediatamente: guarda o mínimo necessário até o pai perguntar.

Se o pai nunca pergunta, a entrada nunca é liberada. Um servidor que cria mil filhos por minuto e nunca chama wait() esgota a tabela de processos e trava a máquina inteira — sem consumir CPU nem memória perceptíveis. É um dos vazamentos mais traiçoeiros que existem, porque não aparece nos gráficos habituais.

💡
Órfão é situação normal e bem resolvida pelo sistema. Zumbi acumulado é defeito de programação. Não confunda os dois: um é adoção, o outro é negligência.
9 · Analogia

Cartório e certidão de óbito

Pense na tabela de processos como um cartório. Quando alguém morre, o registro não pode simplesmente sumir: fica um assento aberto até que o responsável vá lá retirar a certidão e encerrar o caso.

O órfão é o menor cujo responsável faleceu — o Estado (init) assume a tutela e a vida segue. O zumbi é o assento que ninguém foi encerrar: o cartório continua obrigado a guardar aquela linha. Um cartório com um milhão de assentos abertos não consegue registrar mais nada.

10 · Comparação

Criar processos × criar tarefas num RTOS

Unix (propósito geral)RTOS embarcado
Como se criafork() + exec()xTaskCreate() na inicialização
Quando se criaA qualquer momentoQuase sempre só no início
MemóriaEspaço isolado por processoEm geral memória compartilhada
Custo de criaçãoAlto e variávelBaixo e conhecido
Por quê?FlexibilidadePrevisibilidade — nada pode surpreender em voo

A diferença não é técnica por acaso: criar processos em tempo de execução torna o consumo de memória imprevisível, e imprevisibilidade é exatamente o que um sistema crítico não pode aceitar.

11 · Fluxo

O ciclo completo, do nascimento ao encerramento

1 Pai chama fork()
2 Filho existe, estado Pronto
3 Filho opcionalmente chama exec()
4 Filho executa e chama exit()
5 Filho vira Zumbi, aguardando
6 Pai chama wait() e recolhe
7 Entrada liberada — fim

Se o passo 6 nunca acontece, o processo trava no passo 5 para sempre. Se o pai morre antes do passo 4, o filho pula para a adoção pelo init.

12 · Aprofundamento

Cópia sob escrita, na prática

Um fork() de um processo com 2 GB de memória não copia 2 GB. O núcleo marca todas as páginas de ambos como somente leitura e compartilha as mesmas molduras físicas. Enquanto os dois apenas leem, não há cópia alguma.

Na primeira escrita, a MMU dispara uma falta de proteção; o núcleo então duplica aquela página específica, dá permissão de escrita e devolve o controle. O programa nem percebe.

🔑
É por isso que o par fork+exec é barato apesar de parecer esbanjador: o exec vem logo em seguida e descarta o espaço todo, então quase nenhuma página chegou a ser copiada de fato. Este mecanismo depende diretamente da MMU — assunto da U6.
13 · Interativo

Verifique seu entendimento

Um processo chamou exec() com sucesso. O que aconteceu com o PID?

exec não cria processo nenhum: ele troca o conteúdo do processo que já existe. Mesmo PID, mesmo pai, mesmos descritores abertos — programa diferente. Quem cria processo é o fork.
14 · Caso prático

Um servidor que derrubou a máquina sem consumir nada

Serviço em produção criava um processo filho para cada requisição e seguia atendendo, sem nunca chamar wait(). CPU baixa, memória estável, disco tranquilo — todos os alarmes em verde.

Depois de algumas horas, qualquer tentativa de iniciar um programa novo falhava com "não foi possível criar processo". A tabela de processos estava cheia de zumbis. O ps mostrava milhares de linhas com estado Z.

A correção foi de uma linha: tratar o sinal SIGCHLD para recolher os filhos assim que terminassem. O episódio ensina que nem todo vazamento aparece como consumo — este vazava um recurso contável que ninguém estava contando.

15 · Erros comuns

Onde se tropeça

  • "exec cria um processo novo." Não cria. Quem cria é o fork; exec só substitui o programa.
  • "Zumbi consome memória." Praticamente nada — o que ele consome é uma entrada na tabela, recurso limitado e invisível nos gráficos usuais.
  • "Órfão é erro." Não é. É situação prevista e resolvida pela adoção pelo init.
  • "Depois do fork o filho começa do começo." Não: ele continua exatamente da linha seguinte, com a mesma pilha.
  • Contar processos errado. Com n forks em sequência resultam 2n processos, não n+1.
16 · Laboratório

Roteiro da prática

Esta unidade tem 8 horas práticas — a maior carga prática do semestre depois da U1. Sugestão de roteiro:

EtapaComando / tarefaO que provar
1pstree -pQue a árvore existe e tem raiz no PID 1
2Programa com fork() imprimindo getpid() e getppid()Que pai e filho seguem do mesmo ponto com identidades diferentes
3Filho dorme e o pai termina antesQue o filho é adotado — o getppid() passa a 1
4Filho termina e o pai dorme sem wait()Que aparece um Z no ps
5Acrescentar wait() ao paiQue o zumbi desaparece
💡
Faça a etapa 4 antes da 5. Ver o zumbi nascer e depois sumir com uma única linha a mais fixa o conceito muito melhor do que escrever o código já correto.
17 · Interativo

Revele a resposta

Um programa executa três fork() em sequência, sem nenhum if. Quantos processos existem ao final, contando o original?
Oito. Cada fork dobra a população: 1 → 2 → 4 → 8. Em geral, 2n para n forks. Se o enunciado pedir "quantos processos novos foram criados", a resposta é 7 — descontando o original. Ler a pergunta com cuidado vale meio ponto.
18 · Flashcards

Revisão relâmpago

fork()virar
Cria um processo cópia do atual. Devolve o PID do filho ao pai e 0 ao filho.
exec()virar
Substitui o programa do processo atual. O PID não muda.
Órfãovirar
Filho cujo pai terminou. Adotado pelo init (PID 1). Situação normal.
Zumbivirar
Filho que terminou e não foi recolhido com wait(). Ocupa a tabela de processos.
Copy-on-writevirar
Pai e filho compartilham páginas até alguém escrever; só então a página é duplicada.
19 · Conexões

Para onde isso leva

Este é o alicerce de várias unidades à frente:

  • Os processos criados aqui são os que o escalonador vai organizar na U5.
  • A cópia sob escrita só é possível por causa da MMU, que veremos na U6.
  • Quando dois processos precisarem conversar em vez de apenas coexistir, entra a comunicação entre processos da U4.
  • A alocação estática de tarefas do RTOS reaparece no aprofundamento de tempo real.
20 · Síntese

Resumo da aula

🔑
Processos nascem por clonagem: fork() duplica, exec() troca o programa sem trocar o processo. O resultado é uma árvore enraizada no PID 1. Quando um pai morre antes do filho, o init adota; quando o filho morre e o pai não recolhe, sobra um zumbi ocupando a tabela — defeito, não estado natural. Em sistemas embarcados críticos, tudo isso é decidido na inicialização, em nome da previsibilidade.
Mão na massa · colaborativo

Atividade em grupo · Caçando o zumbi

Em trios, provoquem e depois eliminem um vazamento de processos zumbis.

⏱️ 30 min👥 trios🧩 laboratório guiado

Roteiro

  1. Escrevam um programa em que o pai cria cinco filhos, todos terminam de imediato, e o pai dorme 60 s sem chamar wait().
  2. Com o programa rodando, executem ps -el | grep Z e contem os zumbis.
  3. Acrescentem wait() para cada filho e repitam a observação.
  4. Discutam: por que a memória usada quase não muda entre as duas versões? Que recurso, então, estava vazando?
Programadorescreve e ajusta o código
Observadormonitora com ps e registra as contagens
Relatorexplica qual recurso vazou e por quê
📤 Entrega: Os dois códigos, a contagem de zumbis em cada caso e um parágrafo explicando por que o vazamento não aparece no consumo de memória.
Teste seu conhecimento

Mini-quiz · Aula 6

20 questões sobre esta aula. Escolha e veja a explicação na hora.

0/20

📌 Resumo — leve isto para a prova

  • fork() duplica o processo; exec() troca o programa mantendo o mesmo PID. São operações independentes.
  • Todo processo tem um pai, formando uma árvore cuja raiz é o PID 1.
  • Órfão é o filho cujo pai morreu — é adotado pelo init e continua normalmente.
  • Zumbi é o filho que morreu e cujo pai não chamou wait() — ocupa a tabela de processos e é bug, não estado natural.
  • Em embarcado com RTOS, tarefas costumam ser criadas uma única vez na inicialização, justamente para tornar o consumo previsível.