Em 'fork', processos filhos e “subshells”

4

Esta postagem é basicamente uma continuação de uma pergunta anterior minha.

A partir da resposta a essa pergunta, percebi que não apenas não entendo completamente todo o conceito de um "subshell", mas de maneira mais geral, não entendo a relação entre fork -ing e processos filhos.

Eu costumava pensar que quando o processo X executa fork , um novo processo Y é criado, cujo pai é X , mas de acordo com a resposta a essa pergunta,

[a] subshell is not a completely new process, but a fork of the existing process.

A implicação aqui é que um "fork" não é (ou não resulta em) "um processo completamente novo".

Agora estou muito confuso, muito confuso, na verdade, para formular uma pergunta coerente para dissipar diretamente minha confusão.

No entanto, posso formular uma pergunta que pode levar à iluminação indiretamente.

Como, de acordo com zshall(1) , $ZDOTDIR/.zshenv é originado sempre que uma nova instância de zsh é iniciada, qualquer comando em $ZDOTDIR/.zshenv que resulte na criação de um "processo [zsh] completamente novo" resultar em uma regressão infinita. Por outro lado, incluir uma das seguintes linhas em um arquivo $ZDOTDIR/.zshenv não não resulta em uma regressão infinita:

echo $(date; printenv; echo $$) > /dev/null    #1
(date; printenv; echo $$)                      #2

A única maneira que encontrei de induzir uma regressão infinita pelo mecanismo descrito acima foi incluir uma linha como a seguinte 1 no arquivo $ZDOTDIR/.zshenv :

$SHELL -c 'date; printenv; echo $$'            #3

Minhas perguntas são:

  1. que diferença entre os comandos marcados com #1 , #2 acima e o marcado #3 conta essa diferença de comportamento?

  2. se os shells criados em #1 e #2 forem chamados de "subshells", quais são aqueles como o gerado por #3 chamado?

  3. é possível racionalizar (e talvez generalizar) os resultados empíricos / anedóticos descritos acima em termos da "teoria" (por falta de uma palavra melhor) dos processos Unix?

A motivação para a última questão é ser capaz de determinar antecipadamente (isto é, sem recorrer à experimentação) que comandos levariam a uma regressão infinita se fossem incluídos em $ZDOTDIR/.zshenv

1 A sequência particular dos comandos date; printenv; echo $$ que eu usei nos vários exemplos acima não é muito importante. Por acaso são comandos cuja saída foi potencialmente útil para interpretar os resultados de minhas "experiências". (Eu, no entanto, queria que essas sequências consistissem em mais de um comando, pelo motivo explicado aqui .)

    
por kjo 18.02.2016 / 21:19

3 respostas

6

Since, according to zshall(1), $ZDOTDIR/.zshenv gets sourced whenever a new instance of zsh starts

Se você se concentrar na palavra "começa" aqui, você terá um tempo melhor das coisas. O efeito de fork() é criar outro processo que comece exatamente onde o processo atual já é . Está clonando um processo existente, com a única diferença sendo o valor de retorno de fork . A documentação está usando "starts" para significar entrar no programa desde o começo.

Seu exemplo # 3 é executado em $SHELL -c 'date; printenv; echo $$' , iniciando um processo totalmente novo desde o início. Ele passará pelo comportamento de inicialização comum. Você pode ilustrar isso, por exemplo, trocando em outro shell: execute bash -c ' ... ' em vez de zsh -c ' ... ' . Não há nada de especial sobre o uso de $SHELL aqui.

Os exemplos # 1 e # 2 executam subpastas. O shell fork s em si e executa seus comandos dentro desse processo filho e, em seguida, executa sua própria execução quando a criança está pronta.

A resposta à sua pergunta # 1 é a seguinte: o exemplo 3 executa um shell inteiramente novo desde o início, enquanto os outros dois executam sub-unidades. O comportamento de inicialização inclui o carregamento de .zshenv .

A razão pela qual eles chamam esse comportamento especificamente, que é provavelmente o que leva à sua confusão, é que esse arquivo (ao contrário de outros) carrega em shells interativos e não interativos.

Para sua pergunta # 2:

if the shells that get created in #1 and #2 are called "subshells", what are those like the one generated by #3 called?

Se você quiser um nome, pode chamá-lo de "shell filho", mas na verdade não é nada. Não é diferente de qualquer outro processo iniciado a partir do shell, seja o mesmo shell, um shell diferente ou cat .

Para sua pergunta # 3:

is it possible to rationalize (and maybe generalize) the empirical/anecdotal findings described above in terms of the "theory" (for lack of a better word) of Unix processes?

fork faz um novo processo, com um novo PID, que começa a ser executado em paralelo de exatamente onde este parou. exec substitui o código atualmente em execução por um novo programa carregado de algum lugar, executado a partir do começando. Quando você gera um novo programa, você primeiro fork e exec desse programa no filho. Essa é a teoria fundamental dos processos que se aplica em todos os lugares, dentro e fora das conchas.

Subshells são fork s, e todos os comandos não incorporados que você executa levam a um fork e um exec .

Note que $$ se expande para o PID do shell pai em qualquer shell compatível com POSIX , então você pode não estar obtendo a saída esperada, independentemente. Note também que zsh agressivamente otimiza a execução de subshell de qualquer maneira, e geralmente exec s o último comando, ou não gera o subshell se todos os comandos estiverem seguros sem ele.

Um comando útil para testar suas intuições é:

strace -e trace=process -f $SHELL -c ' ... '

Isso imprimirá em erro padrão todos os eventos relacionados ao processo (e nenhum outro) para o comando ... executado em um novo shell. Você pode ver o que é executado e não executado em um novo processo e onde exec s ocorre.

Outro comando possivelmente útil é o pstree -h , que imprimirá e realçará a árvore dos processos pai do processo atual. Você pode ver quantas camadas você está na saída.

    
por 18.02.2016 / 23:03
1

Quando o manual diz que os comandos em .zshenv são "originados", isso significa que eles são executados dentro do shell que os executa. Eles não causam uma chamada para fork() , portanto, eles não geram uma subcamada. Seu terceiro exemplo explicitamente executa um subshell, chamando invocar uma chamada para fork() e, portanto, recursivamente infinitamente. Isso, creio eu, deveria (pelo menos parcialmente) responder à sua primeira pergunta.

  1. Não há nada "criado" nos comandos 1 e 2, portanto não há nada a ser chamado - esses comandos são executados dentro do contexto do shell de fornecimento.

  2. A generalização é a diferença entre "chamar" uma rotina ou programa shell e "pesquisar" uma rotina ou programa shell - com a última sendo aplicável apenas a comandos / scripts de shell, e não a programas externos. "Sourcing" geralmente é feito via . <scriptname> em vez de ./<scriptname> ou /full/path/to/script - observe a sequência "espaço de pontos" no início da diretiva de fornecimento. O sourcing também pode ser chamado usando source <scriptname> , o comando source sendo um shell interno.

por 18.02.2016 / 21:23
0

fork , supondo que tudo corra bem, retorna duas vezes. Um retorno está no processo pai (que possui o ID do processo original) e o outro no novo processo filho (um ID de processo diferente, mas compartilha muito em comum com o processo pai). Nesse ponto, o filho poderia exec(3) something, o que faria com que algum binário "novo" fosse carregado nesse processo, embora o filho não precise fazer isso e possa executar outro código já carregado por meio do processo pai (funções zsh, por exemplo). Portanto, um fork pode ou não resultar em um processo "completamente novo", se "completamente novo" for considerado algo carregado por meio de uma chamada de sistema exec(3) .

Adivinhar quais comandos causam regressão infinita com antecedência é complicado; além do caso do fork-calling-fork (a.k.a. a "forkbomb"), outro fácil é através de um wrapper de função ingênua em torno de algum comando

function ssh() {
   ssh -o UseRoaming=no "$@"
}

que provavelmente deve ser escrito como

function ssh() {
  =ssh -o UseRoaming=no "$@"
}

ou command ssh ... para evitar chamadas de função infinitas da função ssh chamando a função ssh chamando o ... Isso não envolve fork , pois as chamadas de função são internas ao processo ZSH, mas vai acontecer alegremente até o infinito até que algum limite seja atingido por aquele único processo ZSH.

strace , como sempre, é útil para revelar exatamente quais chamadas de sistema estão envolvidas para qualquer comando (em particular, aqui fork e talvez alguns exec call); shells podem ser depurados com -x ou similar que mostra o que o shell está fazendo internamente (por exemplo, chamadas de função). Para mais leitura, Stevens em "Programação Avançada no Ambiente Unix" tem alguns capítulos relacionados à criação e ao manuseio de novos processos.

    
por 18.02.2016 / 22:48