¿Regla para invocar subshell en Bash?

24

Parece que no entiendo la regla de Bash para crear una subshell. Pensé que los paréntesis siempre crean una subshell, que se ejecuta como su propio proceso.

Sin embargo, este no parece ser el caso. En el fragmento de código A (a continuación), el segundo sleepcomando no se ejecuta en un shell separado (como lo determina pstreeotro terminal). Sin embargo, en el fragmento de código B, el segundo sleepcomando se ejecuta en un shell separado. La única diferencia entre los fragmentos es que el segundo fragmento tiene dos comandos entre paréntesis.

¿Podría alguien explicar la regla de cuándo se crean subcapas?

CÓDIGO SNIPPET A:

sleep 5
(
sleep 5
)

CÓDIGO SNIPPET B:

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

Respuestas:

20

Los paréntesis siempre comienzan una subshell. Lo que sucede es que bash detecta que sleep 5es el último comando ejecutado por esa subshell, por lo que llama en execlugar de fork+ exec. El sleepcomando reemplaza la subshell en el mismo proceso.

En otras palabras, el caso base es:

  1. ( … )crear una subshell. El proceso original llama forky wait. En el subproceso, que es una subshell:
    1. sleepes un comando externo que requiere un subproceso del subproceso. El subshell llama forky wait. En el subproceso:
      1. El subproceso ejecuta el comando externo → exec.
      2. Finalmente, el comando termina → exit.
    2. wait completa en la subshell.
  2. wait completa en el proceso original.

La optimización es:

  1. ( … )crear una subshell. El proceso original llama forky wait. En el subproceso, que es un subshell hasta que llama exec:
    1. sleep es un comando externo, y es lo último que debe hacer este proceso.
    2. El subproceso ejecuta el comando externo → exec.
    3. Finalmente, el comando termina → exit.
  2. wait completa en el proceso original.

Cuando agrega algo más después de la llamada sleep, la subshell debe mantenerse, por lo que esta optimización no puede suceder.

Cuando agrega algo más antes de la llamada a sleep, la optimización podría hacerse (y ksh lo hace), pero bash no lo hace (es muy conservador con esta optimización).

Gilles 'SO- deja de ser malvado'
fuente
Subshell se crea llamando forky se crea un proceso hijo (para ejecutar comandos externos) llamandofork + exec . Pero su primer párrafo sugiere que también fork + execse requiere subshell. ¿Qué me estoy equivocando aquí?
piratea el
1
@haccks fork+ execno se llama para la subshell, se llama para el comando externo. Sin ninguna optimización, hay una forkllamada para la subshell y otra para el comando externo. Agregué una descripción detallada del flujo a mi respuesta.
Gilles 'SO- deja de ser malvado'
Muchas gracias por la actualización. Ahora se explica mejor. Puedo deducir de esto que en el caso de (...)(en el caso base), puede o no haber una llamada para que execdependa de si el subshell tiene algún comando externo para ejecutar, mientras que en caso de ejecutar cualquier comando externo debe haberlo fork + exec.
piratea el
Una pregunta más: ¿Esta optimización funciona solo para subshell o se puede hacer para un comando como dateen un shell?
piratea el
@haccks No entiendo la pregunta. Esta optimización consiste en invocar un comando externo como lo último que hace un proceso de shell. No está restringido a subcapas: compara strace -f -e clone,execve,write bash -c 'date'ystrace -f -e clone,execve,write bash -c 'date; true'
Gilles 'SO- deja de ser malvado'
4

De la Guía de programación avanzada de Bash :

"En general, un comando externo en un script bifurca un subproceso, mientras que un comando interno Bash no lo hace. Por esta razón, los comandos internos se ejecutan más rápidamente y usan menos recursos del sistema que sus equivalentes de comandos externos".

Y un poco más abajo:

"Una lista de comandos incrustada entre paréntesis se ejecuta como un subshell".

Ejemplos:

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

Ejemplo que usa el código OP (con sueños más cortos porque soy impaciente):

echo $BASHPID

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

La salida:

[root@talara test]# bash sub_bash
6606
6608
6608
Tim
fuente
2
Gracias por la respuesta Tim. Sin embargo, no estoy seguro de que responda completamente a mi pregunta. Como "Una lista de comandos incrustada entre paréntesis se ejecuta como una subshell", esperaría que la segunda se sleepejecute en una subshell (tal vez en el proceso de la subshell ya que es una función incorporada, en lugar de un subproceso de la subshell). Sin embargo, en cualquier caso, hubiera esperado que existiera una subshell, es decir, un subproceso Bash bajo el proceso Bash principal. Para el fragmento B anterior, este no parece ser el caso.
tímido
Corrección: debido a sleepque no parece estar integrado, esperaría que la segunda sleepllamada en ambos fragmentos se ejecute en un subproceso del proceso de subshell.
tímido
@bashful Me tomé la libertad de hackear tu código con mi $BASHPIDvariable. Lamentablemente, la forma en que lo estaba haciendo no le estaba dando toda la historia, creo. Vea mi salida agregada en la respuesta.
Tim
4

Una nota adicional a la respuesta de @Gilles.

Como dijo Gilles: The parentheses always start a subshell.

Sin embargo, los números que tiene dicho sub-shell podrían repetirse:

$ (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 puede ver, $$ sigue repitiéndose, y eso es como se esperaba, porque (ejecute este comando para encontrar la man bashlínea correcta ):

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

BASHPID se
expande al ID de proceso del proceso bash actual. Esto difiere de $$ en ciertas circunstancias, como subcapas que no requieren reiniciar bash.

Es decir: si el shell no se reinicia, el $$ es el mismo.

O con esto:

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

Parámetros especiales
$ Se expande al ID de proceso del shell. En un subshell (), se expande al ID del proceso del shell actual, no al subshell.

El $$es el ID del shell actual (no el subshell).


fuente
1
Buen truco para abrir la página de manual de bash en una sección específica
Daniel Serodio