Regra para chamar o subshell no Bash?

24

Parece que eu entendi mal a regra do Bash para criar um subshell. Eu pensei que parênteses sempre cria um subshell, que roda como seu próprio processo.

No entanto, este não parece ser o caso. No Snippet de código A (abaixo), o segundo sleepcomando não é executado em um shell separado (conforme determinado por pstreeoutro terminal). No entanto, no Snippet de código B, o segundo sleepcomando é executado em um shell separado. A única diferença entre os trechos é que o segundo trecho possui dois comandos entre parênteses.

Alguém poderia, por favor, explicar a regra para quando subcascas são criadas?

SNIPPET A DO CÓDIGO:

sleep 5
(
sleep 5
)

SNIPPET B DO CÓDIGO:

sleep 5
(
x=1
sleep 5
)
tímido
fonte

Respostas:

20

Os parênteses sempre iniciam um subshell. O que está acontecendo é que o bash detecta sleep 5o último comando executado por esse subshell, portanto, ele chama em execvez de fork+ exec. O sleepcomando substitui o subshell no mesmo processo.

Em outras palavras, o caso base é:

  1. ( … )crie um subshell. O processo original chama forke wait. No subprocesso, que é um subshell:
    1. sleepé um comando externo que requer um subprocesso do subprocesso. O subshell chama forke wait. No sub-processo:
      1. O sub-processo executa o comando externo → exec.
      2. Eventualmente, o comando termina → exit.
    2. wait termina no subshell.
  2. wait termina no processo original.

A otimização é:

  1. ( … )crie um subshell. O processo original chama forke wait. No subprocesso, que é um subshell até chamar exec:
    1. sleep é um comando externo e é a última coisa que esse processo precisa fazer.
    2. O subprocesso executa o comando externo → exec.
    3. Eventualmente, o comando termina → exit.
  2. wait termina no processo original.

Quando você adiciona outra coisa após a chamada sleep, o subshell precisa ser mantido, para que essa otimização não possa acontecer.

Quando você adiciona algo antes da chamada sleep, a otimização pode ser feita (e o ksh faz isso), mas o bash não faz isso (é muito conservador com essa otimização).

Gilles 'SO- parar de ser mau'
fonte
O subshell é criado chamando forke o processo filho é criado (para executar comandos externos) chamandofork + exec . Mas o seu primeiro parágrafo sugere que também fork + execé necessário um subshell. O que estou errado aqui?
haccks
1
@haccks fork+ execnão é chamado para o subshell, é chamado para o comando externo. Sem qualquer otimização, há uma forkchamada para o subshell e outra para o comando externo. Adicionei uma descrição detalhada do fluxo à minha resposta.
Gilles 'SO- stop be evil'
Muito obrigado pela atualização. Agora isso explica melhor. Posso deduzir que, no caso de (...)(no caso base), pode ou não haver uma chamada para execdepende se o subshell tem algum comando externo para executar, enquanto no caso de executar qualquer comando externo deve haver fork + exec.
haccks
Mais uma pergunta: essa otimização funciona apenas para subshell ou pode ser feita para um comando como dateem um shell?
haccks
@ haccks Eu não entendo a pergunta. Essa otimização é sobre chamar um comando externo como a última coisa que um processo de shell faz. Ele não se restringe a subshells: compare strace -f -e clone,execve,write bash -c 'date'estrace -f -e clone,execve,write bash -c 'date; true'
Gilles 'SO parada sendo mal'
4

No Guia de programação avançada do Bash :

"Em geral, um comando externo em um script cria um subprocesso, enquanto um Bash embutido não. Por esse motivo, os embutidos são executados mais rapidamente e usam menos recursos do sistema que seus equivalentes de comando externos."

E um pouco mais abaixo:

"Uma lista de comandos incorporada entre parênteses é executada como um subshell."

Exemplos:

[root@talara test]# echo $BASHPID
10792
[root@talara test]# (echo $BASHPID)
4087
[root@talara test]# (echo $BASHPID)
4088
[root@talara test]# (echo $BASHPID)
4089

Exemplo usando o código de OPs (com sono mais curto porque estou impaciente):

echo $BASHPID

sleep 2
(
    echo $BASHPID
    sleep 2
    echo $BASHPID
)

A saída:

[root@talara test]# bash sub_bash
6606
6608
6608
Tim
fonte
2
Obrigado pela resposta Tim. Não tenho certeza se responde totalmente à minha pergunta. Como "Uma lista de comandos incorporada entre parênteses é executada como um subshell", eu esperaria que o segundo fosse sleepexecutado em um subshell (talvez no processo do subshell, pois é um built-in, em vez de um subprocesso). No entanto, em qualquer caso, eu esperava que existisse um subshell, ou seja, um subprocesso Bash no processo Bash pai. Para o Snippet B acima, este não parece ser o caso.
bashful
Correção: como sleepnão parece ser interno, eu esperaria que a segunda sleepchamada nos dois trechos fosse executada em um subprocesso do processo de subshell.
bashful
@ashashful Tomei a liberdade de invadir seu código com minha $BASHPIDvariável. Infelizmente, o jeito que você estava fazendo não estava lhe contando a história toda. Veja minha saída adicionada na resposta.
Tim
4

Uma observação adicional à resposta do @Gilles.

Como disse Gilles: The parentheses always start a subshell.

No entanto, os números que esse sub-shell pode repetir:

$ (echo "$BASHPID and $$"; sleep 1)
2033 and 31679
$ (echo "$BASHPID and $$"; sleep 1)
2040 and 31679
$ (echo "$BASHPID and $$"; sleep 1)
2047 and 31679

Como você pode ver, o $$ continua repetindo, e é o esperado, porque (execute este comando para encontrar a man bashlinha correta ):

$ LESS=+/'^ *BASHPID' man bash

BASHPID
Expande para o ID do processo atual do bash. Isso difere de $$ sob certas circunstâncias, como subshells que não exigem que o bash seja reinicializado.

Ou seja: Se o shell não for reinicializado, o $$ será o mesmo.

Ou com isso:

$ LESS=+/'^ *Special Parameters' man bash

Parâmetros especiais
$ Expande para o ID do processo do shell. Em um subshell (), ele se expande para o ID do processo do shell atual, não para o subshell.

O $$é o ID do shell atual (não o subshell).


fonte
1
Truque legal para abrir a página de manual do bash em uma seção específica
Daniel Serodio