Quando la sostituzione di comando genera più sotto shell degli stessi comandi in isolamento?

Ieri mi è stato suggerito che l’uso della sostituzione di comando in bash causa la generazione di una subshell non necessaria. Il consiglio era specifico per questo caso d’uso :

# Extra subshell spawned foo=$(command; echo $?) # No extra subshell command foo=$? 

Come meglio posso capire questo sembra essere corretto per questo caso d’uso. Tuttavia, una rapida ricerca cercando di verificare ciò porta a una serie di consigli confusi e contraddittori. Sembra che la saggezza popolare dice che TUTTO l’uso della sostituzione di comando genererà una subshell. Per esempio:

La sostituzione di comando si espande all’output dei comandi. Questi comandi vengono eseguiti in una subshell e i loro dati di stdout sono ciò a cui la syntax di sostituzione si espande. ( fonte )

Questo sembra abbastanza semplice a meno che non continuiate a scavare, nel qual caso inizierete a trovare riferimenti a suggerimenti che non è questo il caso.

La sostituzione di comando non richiede necessariamente una subshell e, nella maggior parte dei casi, non lo farà. L’unica cosa che garantisce è la valutazione out-of-order: valuta semplicemente le espressioni all’interno della sostituzione prima, quindi valuta l’istruzione circostante usando i risultati della sostituzione. ( fonte )

    Questo sembra ragionevole, ma è vero? Questa risposta a una domanda relativa alla subshell mi ha fatto capire che l’ man bash ha questo da notare:

    Ogni comando in una pipeline viene eseguito come processo separato (ad esempio, in una sottoshell).

    Questo mi porta alla domanda principale. Cosa, esattamente, causerà la sostituzione di comando per generare una subshell che non sarebbe stata generata comunque per eseguire gli stessi comandi in isolamento?

    Si prega di considerare i seguenti casi e di spiegare quali sono i costi generali di una subshell extra:

     # Case #1 command1 var=$(command1) # Case #2 command1 | command2 var=$(command1 | command2) # Case #3 command1 | command 2 ; var=$? var=$(command1 | command2 ; echo $?) 

    Ciascuna di queste coppie incorre nello stesso numero di subshells da eseguire? C’è differenza in POSIX rispetto alle implementazioni bash? Ci sono altri casi in cui l’utilizzo della sostituzione di comando genera una subshell in cui non eseguire lo stesso insieme di comandi in isolamento?

    Aggiornamento e avvertenza :

    Questa risposta ha un passato travagliato in quanto ho affermato con sicurezza che le cose non si sono rivelate vere. Credo che abbia valore nella sua forma attuale , ma per favore aiutami a eliminare altre imprecisioni (o convincimi che dovrebbe essere cancellato del tutto).

    Ho sostanzialmente rivisto, e in gran parte sventrato, questa risposta dopo che @kojiro ha sottolineato che i miei metodi di test erano imperfetti (inizialmente ho usato ps per cercare processi figli, ma è troppo lento per individuarli sempre); un nuovo metodo di prova è descritto di seguito.

    All’inizio sostenevo che non tutte le subshell di bash venivano eseguite nel loro processo figlio, ma ciò risulta non essere vero.

    Come afferma @kojiro nella sua risposta, alcune shell – a parte il bash – DO a volte evitano la creazione di processi figli per le subshell, quindi, in generale , nel mondo delle shell, non si deve presumere che una subshell implichi un processo figlio.

    Per quanto riguarda i casi dell’OP in bash (si presuppone che le istanze di command{n} siano semplici comandi ):

     # Case #1 command1 # NO subshell var=$(command1) # 1 subshell (command substitution) # Case #2 command1 | command2 # 2 subshells (1 for each pipeline segment) var=$(command1 | command2) # 3 subshells: + 1 for command subst. # Case #3 command1 | command2 ; var=$? # 2 subshells (due to the pipeline) var=$(command1 | command2 ; echo $?) # 3 subshells: + 1 for command subst.; # note that the extra command doesn't add # one 

    Sembra che l’uso della sostituzione di comando ( $(...) ) aggiunga sempre una subshell extra in bash – così come racchiude qualsiasi comando in (...) .

    Credo, ma non sono sicuro che questi risultati siano corretti ; ecco come ho provato (bash 3.2.51 su OS X 10.9.1) – per favore dimmi se questo approccio è difettoso :

    • Fatto in modo che funzionassero solo 2 shell interattive bash: una per eseguire i comandi, l’altra per monitorare.
    • Nella seconda shell ho monitorato le chiamate a fork() nel primo con sudo dtruss -t fork -f -p {pidOfShell1} (il -f è necessario anche per rintracciare fork() chiama “transitively”, cioè per includere quelli creati da sottotende stesse).
    • Usato solo il builtin : (no-op) nei comandi di test (per evitare di confondere l’immagine con ulteriori fork() chiamate per eseguibili esterni); in particolare:

      • :
      • $(:)
      • : | :
      • $(: | :)
      • : | :; :
      • $(: | :; :)
    • dtruss solo quelle dtruss output di dtruss che contenevano un PID diverso da zero (dato che ogni processo figlio riporta anche la chiamata a fork() che lo ha creato, ma con PID 0).

    • Sottratto 1 dal numero risultante, in quanto anche l’esecuzione di un built-in da una shell intertriggers implica almeno 1 fork() .
    • Infine, si presume che il conteggio risultante rappresenti il ​​numero di sottoshell create.

    Di seguito è riportato ciò che ritengo ancora corretto dal mio post originale: quando bash crea sottotitoli.


    bash crea sottochiavi nelle seguenti situazioni:

    • per un’espressione circondata da parentesi ( (...) )
      • tranne direttamente all’interno [[ ... ]] , dove le parentesi sono utilizzate solo per il raggruppamento logico.
    • per ogni segmento di una pipeline ( | ), incluso il primo
      • Nota che ogni subshell coinvolta è un clone della shell originale in termini di contenuto (process-wise, le subshell possono essere biforcute da altre sotto shell ( prima che i comandi vengano eseguiti)).
        Pertanto, le modifiche delle subshell nei segmenti precedenti della pipeline non influenzano quelle successive.
        (Dalla progettazione, i comandi in una pipeline vengono lanciati simultaneamente – il sequenziamento avviene solo attraverso le pipe stdin / stdout collegate).
      • bash 4.2+ ha l’opzione shell lastpipe (OFF per impostazione predefinita), che fa sì che l’ ultimo segmento della pipeline NON venga eseguito in una subshell.
    • per la sostituzione di comando ( $(...) )

    • per la sostituzione del processo ( <(...) )

      • tipicamente crea 2 subshell; nel caso di un semplice comando , @konsolebox ha creato una tecnica per creare solo 1 : anteponi il comando semplice con exec ( <(exec ...) ).
    • esecuzione in background ( & )

    La combinazione di questi costrutti genererà più di una subshell.

    In Bash, una subshell viene sempre eseguita in un nuovo spazio di processo. Puoi verificarlo in modo abbastanza banale in Bash 4, che ha le variabili di ambiente $BASHPID e $$ :

    • $$ Si espande nell’ID di processo della shell. In una subshell (), si espande nell’ID di processo della shell corrente, non nella subshell.
    • BASHPID Espande l’id di processo dell’attuale processo di bash. Questo differisce da $$ in determinate circostanze, come le subshell che non richiedono la reinizializzazione di bash

    in pratica:

     $ type echo echo is a shell builtin $ echo $$-$BASHPID 4671-4671 $ ( echo $$-$BASHPID ) 4671-4929 $ echo $( echo $$-$BASHPID ) 4671-4930 $ echo $$-$BASHPID | { read; echo $REPLY:$$-$BASHPID; } 4671-5086:4671-5087 $ var=$(echo $$-$BASHPID ); echo $var 4671-5006 

    Circa l’unico caso in cui la shell può elidere una subshell extra è quando si passa a una subshell esplicita:

     $ echo $$-$BASHPID | ( read; echo $REPLY:$$-$BASHPID; ) 4671-5118:4671-5119 

    Qui, la subshell implicita dalla pipe viene esplicitamente applicata, ma non duplicata.

    Questo varia da altre shell che si sforzano molto per evitare la fork . Pertanto, mentre sento che l’argomentazione fatta in js-shell-parse fuorviante, è vero che non tutte le shell forzano sempre per tutte le subshell.