fork e exec
ELF Loading cobre como os segmentos de um binário acabam mapeados na memória, mas não como um processo novo passa a existir em primeiro lugar, nem por que o Unix historicamente divide isso em duas operações separadas em vez de uma única chamada combinada de “spawn”. fork duplica um processo já em execução em duas cópias independentes que continuam executando a partir do exato mesmo ponto; exec substitui a própria imagem de memória de um processo no lugar por um binário diferente, sem criar processo algum novo. Nenhuma das duas isoladamente é o que a maioria dos chamadores realmente quer (rodar um programa diferente como um processo novo); o padrão comum de chamar uma logo depois da outra, fork e depois exec, é o que realiza isso, e a razão pela qual as duas existem como primitivas separadas em vez de uma única chamada de “rode este programa como um processo novo” é o que o resto deste artigo cobre.
O que fork duplica
Seção intitulada “O que fork duplica”fork dá ao processo chamador uma cópia quase completa de si mesmo: o mesmo código, os mesmos dados, os mesmos descritores de arquivo abertos, o mesmo layout de espaço de endereçamento virtual, diferindo apenas no valor de retorno que o próprio fork produz (zero no novo filho, o ID de processo do filho no pai original) e nos próprios identificadores dos dois processos a partir desse ponto. Copiar de fato toda a memória física equivalente a um espaço de endereçamento inteiro em toda chamada de fork seria proibitivamente caro para o que muitas vezes é uma duplicata de vida extremamente curta, particularmente no padrão comum onde o filho imediatamente chama exec e descarta a memória copiada por completo em microssegundos.
pid_t pid = fork();if (pid == 0) { // filho: pid contém 0} else { // pai: pid contém o PID real do filho}Copy-on-write
Seção intitulada “Copy-on-write”Implementações reais evitam esse custo através de copy-on-write, já coberto do lado da paginação: fork marca toda página do espaço de endereçamento do pai como somente leitura tanto nas tabelas de página do pai quanto nas do novo filho, em vez de alocar e copiar frames físicos de antemão para qualquer parte dela. Ambos os processos continuam lendo da mesma memória física sem perturbação, e só no momento em que um dos dois tenta escrever numa página compartilhada ocorre uma falha, ponto em que o kernel aloca uma cópia física genuinamente privada para qualquer processo que causou a falha e atualiza apenas aquele mapeamento, deixando a página do outro processo, e o frame original por baixo, intocados. Isso é o que torna fork barato apesar de superficialmente parecer duplicar um espaço de endereçamento inteiro: o custo é adiado para páginas individuais de fato modificadas depois, e um filho que chama exec imediatamente, substituindo seu espaço de endereçamento antes de sequer escrever na maior parte do que fork lhe deu, acaba pagando por quase nada dessa duplicação aparente.
O que exec substitui
Seção intitulada “O que exec substitui”exec descarta o código e os dados atuais de um processo e os substitui pelos de um binário novo, exatamente o processo de mapeamento que ELF Loading já descreve, mas reaproveita o processo existente em vez de alocar um novo: o ID de processo permanece o mesmo, e também permanece qualquer coisa que o processo chamador tenha explicitamente arranjado para sobreviver à substituição (descritores de arquivo abertos, por padrão, a menos que individualmente marcados close-on-exec). A execução retoma no ponto de entrada do binário novo como se o processo tivesse acabado de começar, sem retorno algum de uma chamada de exec bem-sucedida, já que o código que a chamou não existe mais na memória do processo para retornar a ele; exec só retorna ao chamador em caso de falha, quando a substituição nunca aconteceu.
execve("/bin/ls", argv, envp);// inalcançável em caso de sucesso: este processo agora roda /bin/lsperror("execve failed");Por que duas chamadas em vez de uma
Seção intitulada “Por que duas chamadas em vez de uma”Separar criação de substituição dá ao processo filho uma janela, entre o retorno de fork e a chamada de exec, para ajustar seu próprio ambiente antes de o programa novo sequer começar a rodar: fechar um descritor de arquivo herdado que não quer que o programa novo veja, duplicar uma ponta de pipe sobre um descritor de fluxo padrão específico para redirecionar entrada ou saída, ou abrir mão de privilégios, são todas operações comuns realizadas nessa janela usando a interface de chamada de sistema já familiar do processo, em vez de exigir uma única chamada combinada estilo spawn capaz de aceitar parâmetros para todo ajuste desse tipo que um chamador algum dia pudesse querer. Um shell implementando um pipeline (ls | grep foo) é o exemplo clássico: ele faz fork uma vez por estágio, religa os descritores padrão de cada filho ao pipe que o conecta ao vizinho inteiramente dentro daquele filho, e só então chama exec, trabalho que não tem lugar natural para acontecer se criação de processo e substituição de programa fossem uma única etapa inseparável.
Notas de implementação
Seção intitulada “Notas de implementação”Um kernel implementando esse par precisa ter cuidado para que a marcação somente leitura de tabela de página do copy-on-write não vaze para o que os próprios processos observam: uma página genuinamente gravável antes do fork, da perspectiva de qualquer um dos dois processos depois dele, ainda precisa se comportar como gravável, com a marcação somente leitura existindo puramente como um mecanismo interno que o tratador de falha de copy-on-write resolve de forma transparente, não como uma permissão que o próprio código de qualquer processo deveria conseguir detectar diretamente. A substituição de espaço de endereçamento do exec similarmente precisa desmontar os mapeamentos e entradas de TLB antigos por completo antes de estabelecer os do binário novo, já que um mapeamento desatualizado sobrevivendo à troca deixaria código antigo e novo coexistir no mesmo espaço de endereçamento de uma forma que viola todo o propósito do exec.
Referências
Seção intitulada “Referências”- ^ W. R. Stevens e S. A. Rago, Advanced Programming in the UNIX Environment, Capítulo 8: a referência padrão sobre o modelo de processo fork/exec e suas variantes.
Ver também
Seção intitulada “Ver também”- Paging & Virtual Memory: o mecanismo de copy-on-write que torna fork barato apesar de parecer duplicar um espaço de endereçamento inteiro.
- ELF Loading: o que de fato acontece dentro do exec, uma vez que o processo que ele substitui já existe.
- Término de Processo, Zumbis, e wait(): a outra metade do ciclo de vida de processo que a metade de criação deste artigo é completada por.