Término de Processo, Zumbis, e wait()
Término de processo não remove um processo do sistema no momento em que ele para de rodar: fork e exec cobrem metade do ciclo de vida de um processo, a criação, e este artigo cobre a outra metade, o que de fato acontece uma vez que um processo sai e por que o kernel não consegue simplesmente descartá-lo na hora.
Para onde vai o status de saída
Seção intitulada “Para onde vai o status de saída”Um processo termina chamando exit (diretamente, ou implicitamente ao retornar de main), passando um status de saída inteiro que o kernel precisa preservar, já que um processo pai pode querer saber depois se seu filho teve sucesso ou falhou e como. Esse status não tem onde viver uma vez que a própria memória do processo se foi, então o kernel o mantém na própria entrada de tabela de processos, a mesma estrutura que Multitasking já cobre como contendo o estado de escalonamento de uma tarefa, e tudo que o processo terminado de fato possuía, seu espaço de endereçamento, descritores de arquivo abertos, memória alocada, é liberado imediatamente, independentemente de alguém já ter pedido pelo status de saída.
void do_exit(int status) { free_address_space(current->mm); close_all_files(current->files); current->exit_status = status; current->state = ZOMBIE; notify_parent(current->parent, SIGCHLD); schedule(); // nunca retorna}O estado zumbi
Seção intitulada “O estado zumbi”Um processo que liberou tudo que possuía mas cujo status de saída ainda não foi coletado é um zumbi: sua entrada de tabela de processos sobrevive especificamente para conter esse status, ps ainda o lista, mas ele não consome tempo de CPU algum, memória alguma além da própria entrada de tabela, e não roda código algum. Isso é deliberado, não um bug nem um vazamento de recurso: a entrada de tabela de processos é o único lugar que sobra para o status de saída viver, e um kernel que a liberasse imediatamente na saída não teria de onde retornar esse status quando o pai de fato o pedir via wait.
fork() ──► rodando ──► exit() ──► ZUMBI ──► pai chama wait() ──► entrada liberada │ └─ SIGCHLD entregue ao pai aquiUm zumbi que nunca é coletado permanece zumbi indefinidamente, o que é um vazamento de recurso real, ainda que de acúmulo lento: cada um ocupa um slot de tabela de processos e um PID que não pode ser reaproveitado, e um processo pai de longa duração que faz fork de filhos sem nunca chamar wait sobre eles eventualmente esgota a tabela de processos do sistema por completo, um erro fácil de cometer num shell ou processo supervisor que gera filhos mais rápido do que os colhe.
Coletando o status com wait
Seção intitulada “Coletando o status com wait”Um pai recupera o status de saída de um filho terminado chamando wait ou waitpid, que bloqueia (a menos que uma flag não bloqueante seja dada) até pelo menos um de seus filhos ter se tornado zumbi, depois retorna o PID e o status de saída daquele filho enquanto simultaneamente libera a entrada de tabela de processos, encerrando a existência do zumbi.
pid_t child = fork();if (child == 0) { exit(42); // o filho vira um zumbi carregando status 42} else { int status; waitpid(child, &status, 0); // bloqueia até o filho sair, depois o colhe int code = WEXITSTATUS(status);}IPC já cobre SIGCHLD como a notificação assíncrona que um pai recebe no momento em que um filho vira zumbi, o que é o que permite a um pai chamar wait em resposta a um evento em vez de sondar por término de filho em algum cronograma fixo; um pai que ignora SIGCHLD por completo e nunca chama wait é exatamente o modo de falha que acumula zumbis indefinidamente.
Órfãos e reparentagem
Seção intitulada “Órfãos e reparentagem”Um filho cujo pai sai (ou é morto) antes dele se torna um órfão, e o kernel trata isso reparentando-o imediatamente para um ancestral designado, convencionalmente o processo init (PID 1), em vez de deixá-lo sem pai algum para eventualmente coletar seu status de saída. Essa garantia é o que impede um órfão de virar um zumbi permanente e não colhível no momento em que eventualmente sai: o init é escrito especificamente para chamar wait num loop, coletando o status de saída de todo órfão reparentado para ele (e de todo zumbi que ele mesmo produz) como rotina, tenha ele ou não qualquer outra relação com aquele processo.
Notas de implementação
Seção intitulada “Notas de implementação”Entregar SIGCHLD e atualizar o ponteiro de pai precisam acontecer atomicamente em relação ao próprio término do pai: um filho saindo no exato momento em que seu pai também está saindo precisa ser reparentado antes de o kernel terminar de desmontar o pai original, não depois, ou o filho apontaria brevemente para uma estrutura de pai que não existe mais. WIFEXITED, WIFSIGNALED, e as outras macros ao redor do valor de status de waitpid existem porque um único inteiro codifica duas informações genuinamente diferentes empacotadas juntas, se o processo saiu normalmente versus foi morto por um sinal, e qual valor específico (código de saída ou número de sinal) se aplica em cada caso, uma distinção que um chamador precisa checar antes de interpretar os bits brutos de status diretamente.
Referências
Seção intitulada “Referências”- ^ W. R. Stevens e S. A. Rago, Advanced Programming in the UNIX Environment, Capítulo 8: término de processo, o estado zumbi, e a família wait/waitpid por completo.
Ver também
Seção intitulada “Ver também”- fork e exec: a metade de criação do ciclo de vida de processo que a metade de término deste artigo completa.
- IPC: o sinal SIGCHLD que um pai usa para saber que um filho virou zumbi sem sondar por isso.