Por que os reservatórios chamam fork ()?


32

Quando um processo é iniciado a partir de um shell, por que o shell se bifurca antes de executar o processo?

Por exemplo, quando o usuário digita grep blabla foo, por que o shell não pode simplesmente chamar exec()grep sem um shell filho?

Além disso, quando um shell se bifurca dentro de um emulador de terminal da GUI, ele inicia outro emulador de terminal? (como pts/13iniciar pts/14)

Respostas:


34

Quando você chama um execmétodo de família, ele não cria um novo processo; em vez disso, execsubstitui a memória do processo atual e o conjunto de instruções etc pelo processo que você deseja executar.

Como exemplo, você deseja executar grepusando exec. bashé um processo (que possui memória separada, espaço de endereço). Agora, quando você ligar exec(grep), o exec substituirá a memória do processo atual, o espaço de endereço, o conjunto de instruções etc. pelos grep'sdados. Isso significa que o bashprocesso não existirá mais. Como resultado, você não pode voltar ao terminal após concluir o grepcomando. É por isso que os métodos da família exec nunca retornam. Você não pode executar nenhum código após exec; é inacessível.


Quase ok --- substituí o Terminal por bash. ;-)
Rmano 2/14/14

2
BTW, você pode dizer bash para executar grep sem bifurcação primeiro, usando o comando exec grep blabla foo. Obviamente, nesse caso em particular, não será muito útil (já que a janela do terminal será fechada assim que o grep terminar), mas pode ser útil ocasionalmente (por exemplo, se você estiver iniciando outro shell, talvez via ssh / sudo / screen, e não pretende retornar ao original, ou se o processo do shell em que você está executando isso for um sub-shell que nunca deve executar mais de um comando).
Ilmari Karonen

7
Conjunto de instruções tem um significado muito específico. E não é o significado que você está usando-o em.
Andrew Savinykh

@IlmariKaronen Seria útil em scripts de wrapper, nos quais você deseja preparar argumentos e ambiente para um comando. E o caso que você mencionou, onde o bash não serve para executar mais de um comando, isso é realmente bash -c 'grep foo bar'e exec chamando existe forma de festa otimização faz para você automaticamente
Sergiy Kolodyazhnyy

3

De acordo com o pts, verifique você mesmo: em um shell, execute

echo $$ 

para conhecer seu ID do processo (PID), eu tenho por exemplo

echo $$
29296

Em seguida, execute por exemplo sleep 60e, em outro terminal

(0)samsung-romano:~% ps -edao pid,ppid,tty,command | grep 29296 | grep -v grep
29296  2343 pts/11   zsh
29499 29296 pts/11   sleep 60

Portanto, não, em geral você tem o mesmo tty associado ao processo. (Observe que este é seu sleepporque ele tem seu shell como pai).


2

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 foofosse 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 -hexecutado 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 stdoutaka 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, dfo processo filho não pensa em nada stdoute 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 grepcomexec(). 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 sourcecomando. 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


1

Para cada comando (exemplo: grep) emitido no prompt do bash, você realmente pretende iniciar um novo processo e depois retornar ao prompt do bash após a execução.

Se o processo do shell (bash) chamar exec () para executar o grep, o processo do shell será substituído pelo grep. O Grep funcionará bem, mas após a execução, o controle não poderá retornar ao shell porque o processo bash já foi substituído.

Por esse motivo, o bash chama fork (), que não substitui o processo atual.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.