TL; DR : porque este é o método ideal para criar novos processos e manter o controle no shell interativo
fork () é necessário para processos e tubos
Para responder à parte específica desta pergunta, se grep blabla foo
fosse chamado exec()
diretamente via pai, o pai aproveitaria a existência e seu PID com todos os recursos seria assumido grep blabla foo
.
No entanto, vamos falar em geral sobre exec()
e fork()
. A principal razão para esse comportamento é porque fork()/exec()
é o método padrão de criação de um novo processo no Unix / Linux, e isso não é algo específico do bash; esse método existe desde o início e é influenciado por esse mesmo método nos sistemas operacionais já existentes da época. Parafraseando um pouco a resposta de goldilocks em uma pergunta relacionada, fork()
criar um novo processo é mais fácil, pois o kernel tem menos trabalho a ser feito no que diz respeito à alocação de recursos e muitas propriedades (como descritores de arquivo, ambiente etc.) - tudo pode ser herdado do processo pai (neste caso, de bash
).
Em segundo lugar, no que diz respeito aos shells interativos, você não pode executar um comando externo sem fazer bifurcação. Para iniciar um executável que vive no disco (por exemplo /bin/df -h
), é necessário chamar uma das exec()
funções da família, como execve()
, que substituirá o pai pelo novo processo, assumirá o seu PID e os descritores de arquivo existentes, etc. Para o shell interativo, você deseja que o controle retorne ao usuário e deixe o shell interativo pai continuar. Portanto, a melhor maneira é criar um subprocesso via fork()
e deixar que esse processo seja retomado via execve()
. Portanto, o PID 1156 do shell interativo geraria um filho via fork()
PID 1157 e depois chamaria execve("/bin/df",["df","-h"],&environment)
, o que é /bin/df -h
executado com o PID 1157. Agora, o shell precisa aguardar o processo sair e retornar o controle a ele.
No caso de você precisar criar um canal entre dois ou mais comandos, por exemplo df | grep
, é necessário criar dois descritores de arquivo (isto é, ler e gravar o final do canal que vem do pipe()
syscall) e, de alguma forma, permitir que dois novos processos os herdem. Isso é feito no processo de bifurcação de novos processos e, em seguida, copiando a extremidade de gravação do canal via dup2()
chamada para seu stdout
aka fd 1 (por isso, se a extremidade de gravação for fd 4, nós o fazemos dup2(4,1)
). Quando ocorre a exec()
desova, df
o processo filho não pensa em nada stdout
e escreve para ela sem estar ciente (a menos que verifique ativamente) de que sua saída realmente é prejudicial. Mesmo processo acontece grep
, exceto nós fork()
, tome fim de leitura de tubo com fd 3 e dup(3,0)
antes da desova grep
comexec()
. Todo esse processo pai ainda está lá, esperando para recuperar o controle assim que o pipeline for concluído.
No caso de comandos internos, geralmente o shell não funciona fork()
, com exceção do source
comando. Subshells exigem fork()
.
Em suma, este é um mecanismo necessário e útil.
Desvantagens de bifurcação e otimizações
Agora, isso é diferente para shells não interativos , como bash -c '<simple command>'
. Apesar de fork()/exec()
ser o método ideal para processar muitos comandos, é um desperdício de recursos quando você tem apenas um único comando. Para citar Stéphane Chazelas a partir deste post :
A bifurcação é cara, em tempo de CPU, memória, descritores de arquivos alocados ... Ter um processo shell aguardando apenas outro processo antes de sair é apenas um desperdício de recursos. Além disso, torna difícil relatar corretamente o status de saída do processo separado que executaria o comando (por exemplo, quando o processo é finalizado).
Portanto, muitos shells (não apenas bash
) são usados exec()
para permitir que isso bash -c ''
seja assumido por esse único comando simples. E exatamente pelas razões expostas acima, é melhor minimizar os pipelines nos scripts de shell. Muitas vezes, você pode ver iniciantes fazendo algo assim:
cat /etc/passwd | cut -d ':' -f 6 | grep '/home'
Claro, isso vai fork()
3 processos. Este é um exemplo simples, mas considere um arquivo grande, no intervalo de Gigabytes. Seria muito mais eficiente com um processo:
awk -F':' '$6~"/home"{print $6}' /etc/passwd
O desperdício de recursos, na verdade, pode ser uma forma de ataque de negação de serviço e, em particular, bombas de garfo são criadas por meio de funções de shell que se chamam em pipeline, o que bifurca várias cópias de si mesmas. Atualmente, isso é mitigado via limitação do número máximo de processos nos cgroups no systemd , que o Ubuntu também usa desde a versão 15.04.
Claro que isso não significa bifurcação é apenas ruim. Ainda é um mecanismo útil, como discutido anteriormente, mas, no caso de você poder se safar com menos processos e consecutivamente menos recursos e, portanto, com melhor desempenho, evite, fork()
se possível.
Veja também