O que você vai aprender
Distinguir o papel de fork() e o de exec() na criação de um processo.
Explicar a hierarquia de processos e o destino dos órfãos.
Identificar a origem de um processo zumbi e como evitá-lo.
Comparar a criação dinâmica de processos com a alocação estática de tarefas em sistemas embarcados.
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.
Roteiro da aula
fork() e exec()
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.
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).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.
Um fork, duas continuações
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.
fork() em sequência, são quatro processos ao final — a contagem dobra a cada chamada. Com três, oito. É pegadinha frequente de prova.Passo a passo: o shell rodando um comando
ls e confirma.fork(). Agora existem dois shells idênticos; um recebeu o PID do outro, o outro recebeu 0.exec("/bin/ls"). Mesmo PID, programa novo: ele deixa de ser shell e passa a ser o ls.wait() e fica Bloqueado até o filho terminar. Não gasta CPU nenhuma nessa espera.exit(0). O núcleo acorda o pai, que recolhe o código de saída e volta a exibir o prompt.A árvore e os seus acidentes
Ó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.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.
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.
Criar processos × criar tarefas num RTOS
| Unix (propósito geral) | RTOS embarcado | |
|---|---|---|
| Como se cria | fork() + exec() | xTaskCreate() na inicialização |
| Quando se cria | A qualquer momento | Quase sempre só no início |
| Memória | Espaço isolado por processo | Em geral memória compartilhada |
| Custo de criação | Alto e variável | Baixo e conhecido |
| Por quê? | Flexibilidade | Previsibilidade — 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.
O ciclo completo, do nascimento ao encerramento
fork()exec()exit()wait() e recolheSe 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.
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.
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.Verifique seu entendimento
Um processo chamou exec() com sucesso. O que aconteceu com o PID?
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.
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.
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:
| Etapa | Comando / tarefa | O que provar |
|---|---|---|
| 1 | pstree -p | Que a árvore existe e tem raiz no PID 1 |
| 2 | Programa com fork() imprimindo getpid() e getppid() | Que pai e filho seguem do mesmo ponto com identidades diferentes |
| 3 | Filho dorme e o pai termina antes | Que o filho é adotado — o getppid() passa a 1 |
| 4 | Filho termina e o pai dorme sem wait() | Que aparece um Z no ps |
| 5 | Acrescentar wait() ao pai | Que o zumbi desaparece |
Revele a resposta
Um programa executa três fork() em sequência, sem nenhum if. Quantos processos existem ao final, contando o original?
Revisão relâmpago
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.
Resumo da aula
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.Atividade em grupo · Caçando o zumbi
Em trios, provoquem e depois eliminem um vazamento de processos zumbis.
Roteiro
- Escrevam um programa em que o pai cria cinco filhos, todos terminam de imediato, e o pai dorme 60 s sem chamar wait().
- Com o programa rodando, executem
ps -el | grep Ze contem os zumbis. - Acrescentem
wait()para cada filho e repitam a observação. - Discutam: por que a memória usada quase não muda entre as duas versões? Que recurso, então, estava vazando?
Mini-quiz · Aula 6
20 questões sobre esta aula. Escolha e veja a explicação na hora.
📌 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.