L'output di sostituzione del processo è fuori ordine


16

Il

echo one; echo two > >(cat); echo three; 

il comando fornisce un output imprevisto.

Ho letto questo: come viene implementata la sostituzione di processo in bash? e molti altri articoli sulla sostituzione dei processi su Internet, ma non capisco perché si comporti in questo modo.

Uscita prevista:

one
two
three

Uscita reale:

prompt$ echo one; echo two > >(cat); echo three;
one
three
prompt$ two

Inoltre, questi due comandi dovrebbero essere equivalenti dal mio punto di vista, ma non lo fanno:

##### first command - the pipe is used.
prompt$ seq 1 5 | cat
1
2
3
4
5
##### second command - the process substitution and redirection are used.
prompt$ seq 1 5 > >(cat)
prompt$ 1
2
3
4
5

Perché penso che dovrebbero essere gli stessi? Perché, entrambi collegano l' seqoutput catall'input attraverso la pipe anonima: Wikipedia, Sostituzione del processo .

Domanda: perché si comporta in questo modo? Dov'è il mio errore? Si desidera la risposta completa (con la spiegazione di come bashfunziona sotto il cofano).


2
Anche se non è così chiaro a prima vista, è in realtà un duplicato di bash wait per il processo in sostituzione del processo anche se il comando non è valido
Stéphane Chazelas

2
In realtà, sarebbe meglio se quell'altra domanda fosse contrassegnata come duplicata a questa in quanto questa è più pertinente. Ecco perché ho copiato la mia risposta lì.
Stéphane Chazelas,

Risposte:


21

Sì, bashcome in ksh(da dove proviene la funzione), i processi all'interno della sostituzione del processo non sono attesi (prima di eseguire il comando successivo nello script).

per <(...)uno, di solito va bene come in:

cmd1 <(cmd2)

la shell attenderà cmd1e cmd1in genere attenderà in cmd2virtù della sua lettura fino alla fine del file sulla pipe che viene sostituita e tale fine del file si verifica in genere quando cmd2muore. Questa è la stessa ragione per diverse shell (non bash) non si preoccupano in attesa di cmd2in cmd2 | cmd1.

Per cmd1 >(cmd2), tuttavia, che non è generalmente il caso, come è più cmd2che tipicamente attende cmd1lì così sarà generalmente uscita dopo.

Ciò è stato risolto in zshquello che attende cmd2lì (ma non se lo scrivi come cmd1 > >(cmd2)e cmd1non è incorporato, usa {cmd1} > >(cmd2)invece come documentato ).

kshnon aspetta per impostazione predefinita, ma ti consente di aspettarlo con il waitbuilt-in (rende anche disponibile il pid $!, anche se ciò non aiuta se lo fai cmd1 >(cmd2) >(cmd3))

rc(con la cmd1 >{cmd2}sintassi), come kshse fosse possibile ottenere i pid di tutti i processi in background $apids.

es(anche con cmd1 >{cmd2}) attende cmd2like in zshe attende anche reindirizzamenti cmd2in <{cmd2}corso.

bashrende disponibile il pid di cmd2(o più esattamente della subshell mentre viene eseguito cmd2in un processo figlio di quella subshell anche se è l'ultimo comando lì) disponibile $!, ma non ti fa aspettare.

Se è necessario utilizzare bash, è possibile aggirare il problema utilizzando un comando che attenderà entrambi i comandi con:

{ { cmd1 >(cmd2); } 3>&1 >&4 4>&- | cat; } 4>&1

Ciò rende entrambi cmd1e cmd2hanno il loro fd 3 aperto a una pipe. catattenderà per la fine del file all'altra estremità, così sarà in genere solo uscire quando entrambi cmd1e cmd2sono morti. E la shell attenderà quel catcomando. Potresti vederlo come una rete per catturare la terminazione di tutti i processi in background (puoi usarlo per altre cose iniziate in background come con &, coprocs o anche comandi che in background stessi a condizione che non chiudano tutti i loro descrittori di file come fanno normalmente i demoni ).

Nota che grazie a quel processo di subshell sprecato menzionato sopra, funziona anche se cmd2chiude il suo fd 3 (i comandi di solito non lo fanno, ma alcuni lo fanno sudoo lo sshfanno). Le versioni future di bashpotrebbero eventualmente fare l'ottimizzazione lì come in altre shell. Quindi avresti bisogno di qualcosa come:

{ { cmd1 >(sudo cmd2; exit); } 3>&1 >&4 4>&- | cat; } 4>&1

Per assicurarsi che ci sia ancora un ulteriore processo di shell con quel fd 3 aperto in attesa di quel sudocomando.

Nota che catnon leggerà nulla (poiché i processi non scrivono sul loro fd 3). È lì solo per la sincronizzazione. Farà solo una read()chiamata di sistema che tornerà senza nulla alla fine.

Puoi effettivamente evitare l'esecuzione catutilizzando una sostituzione di comando per eseguire la sincronizzazione della pipe:

{ unused=$( { cmd1 >(cmd2); } 3>&1 >&4 4>&-); } 4>&1

Questa volta, è la shell invece catche sta leggendo dalla pipe la cui altra estremità è aperta su fd 3 di cmd1e cmd2. Stiamo utilizzando un'assegnazione variabile per cui lo stato di uscita di cmd1è disponibile in $?.

Oppure potresti fare manualmente la sostituzione del processo, e quindi potresti anche usare il tuo sistema in shquanto diventerebbe la sintassi della shell standard:

{ cmd1 /dev/fd/3 3>&1 >&4 4>&- | cmd2 4>&-; } 4>&1

tuttavia, come notato in precedenza, non tutte le shimplementazioni che aspetterebbero cmd1dopo il cmd2completamento (anche se è meglio del contrario). Quella volta, $?contiene lo stato di uscita di cmd2; sebbene bashe zshrendere lo cmd1stato di uscita disponibile in ${PIPESTATUS[0]}e $pipestatus[1]rispettivamente (vedere anche l' pipefailopzione in alcune shell in modo da $?poter segnalare il fallimento dei componenti del tubo diversi dall'ultimo)

Si noti che yashha problemi simili con la sua funzione di reindirizzamento del processo . cmd1 >(cmd2)sarebbe scritto cmd1 /dev/fd/3 3>(cmd2)lì. Ma cmd2non è atteso e non puoi nemmeno usarlo waite il suo pid non è reso disponibile $!nemmeno nella variabile. Useresti lo stesso lavoro come per bash.


In primo luogo, ho provato echo one; { { echo two > >(cat); } 3>&1 >&4 4>&- | cat; } 4>&1; echo three;, poi l' ho semplificato echo one; echo two > >(cat) | cat; echo three;e produce anche i valori nel giusto ordine. 3>&1 >&4 4>&-Sono necessarie tutte queste manipolazioni del descrittore ? Inoltre, non capisco >&4 4>&: reindirizziamo stdoutalla quarta fd, quindi chiudiamo la quarta fd, quindi usiamola di nuovo 4>&1. Perché è necessario e come funziona? Forse dovrei creare una nuova domanda su questo argomento?
MiniMax,

1
@MiniMax, ma lì, stai influenzando lo stdout di cmd1e cmd2, il punto con la piccola danza con il descrittore di file è ripristinare quelli originali e usare solo la pipe aggiuntiva per l' attesa invece di incanalare anche l'output dei comandi.
Stéphane Chazelas,

@MiniMax Mi ci è voluto un po 'di tempo per capire, non avevo mai avuto i tubi a un livello così basso prima. L'estrema destra 4>&1crea un descrittore di file (fd) 4 per l'elenco dei comandi delle parentesi graffe esterne e lo rende uguale allo stdout delle parentesi graffe esterne. Le parentesi graffe interne hanno stdin / stdout / stderr automaticamente configurate per connettersi alle parentesi graffe esterne. Tuttavia, 3>&1rende fd 3 collegato allo stdin delle parentesi graffe esterne. >&4rende lo stdout delle parentesi graffe interne collegato alle parentesi graffe esterne fd 4 (Quello che abbiamo creato prima). 4>&-chiude fd 4 dalle parentesi graffe interne (poiché lo stdout delle parentesi interne è già collegato alla fd 4 delle parentesi graffe esterne).
Nicholas Pipitone,

@MiniMax La parte confusa era la parte da destra a sinistra, 4>&1viene eseguita per prima, prima degli altri reindirizzamenti, quindi non "usi di nuovo 4>&1". Nel complesso, le parentesi graffe interne stanno inviando i dati al suo stdout, che è stato sovrascritto con qualsiasi fd 4 che è stato dato. La fd 4 data dalle parentesi graffe interne è la fd 4 delle parentesi graffe esterne, che è uguale allo stdout originale delle parentesi graffe esterne.
Nicholas Pipitone,

Bash fa sembrare che 4>5"4 vada a 5", ma in realtà "fd 4 viene sovrascritto con fd 5". E prima dell'esecuzione, fd 0/1/2 sono collegati automaticamente (insieme a qualsiasi fd della shell esterna) e puoi sovrascriverli come desideri. Questa è almeno la mia interpretazione della documentazione di Bash. Se hai capito qualcos'altro da questo , lmk.
Nicholas Pipitone,

4

È possibile reindirizzare il secondo comando in un altro cat, che attenderà fino alla chiusura della relativa pipe di input. Ex:

prompt$ echo one; echo two > >(cat) | cat; echo three;
one
two
three
prompt$

Breve e semplice

==========

Per quanto possa sembrare semplice, molto sta succedendo dietro le quinte. Puoi ignorare il resto della risposta se non sei interessato a come funziona.

Quando hai echo two > >(cat); echo three, >(cat)viene biforcuta dalla shell interattiva e funziona indipendentemente da echo two. Quindi, echo twotermina e quindi echo threeviene eseguito, ma prima del >(cat)traguardo. Quando bashriceve i dati da >(cat)quando non li aspettava (un paio di millisecondi dopo), ti dà quella situazione di tipo prompt in cui devi premere newline per tornare al terminale (come se un altro utente ti avesse scritto mesg).

Tuttavia, dato echo two > >(cat) | cat; echo three, vengono generati due subshells (secondo la documentazione del |simbolo).

Una subshell denominata A è per echo two > >(cat), e una subshell denominata B è per cat. A viene automaticamente connesso a B (lo stdout di A è lo stdin di B). Quindi, echo twoe >(cat)inizia l'esecuzione. >(cat)Lo stdout è impostato sullo stdout di A, che è uguale allo stdin di B. Al echo twotermine, A esce, chiudendo il suo stdout. Tuttavia, >(cat)mantiene ancora il riferimento allo stdin di B. Il secondo catstdin tiene in mano lo stdin di B, e questo catnon uscirà finché non vedrà un EOF. Un EOF viene dato solo quando nessuno ha più il file aperto in modalità di scrittura, quindi lo >(cat)stdout sta bloccando il secondo cat. B rimane in attesa in quel secondo cat. Da quando è echo twouscito, >(cat)alla fine ottiene un EOF, quindi>(cat)svuota il suo buffer ed esce. Nessuno tiene catpiù lo stdin di B / secondo , quindi il secondo catlegge un EOF (B non sta leggendo affatto il suo stdin, non gliene importa). Questo EOF fa sì che il secondo catsvuoti il ​​suo buffer, chiuda il suo stdout e esca, quindi B esce perché è catuscito e B era in attesa cat.

Un avvertimento di questo è che bash genera anche una subshell per >(cat)! Per questo motivo, lo vedrai

echo two > >(sleep 5) | cat; echo three

attenderà comunque 5 secondi prima dell'esecuzione echo three, anche se sleep 5non tiene in mano lo stdin di B. Questo perché una sottostruttura nascosta nascosta da C >(sleep 5)è in attesa sleepe C tiene in mano lo stdin di B. Puoi vedere come

echo two > >(exec sleep 5) | cat; echo three

Tuttavia, non aspetterà, poiché sleepnon tiene in mano lo stdin di B, e non c'è sottostruttura fantasma C che trattiene lo stdin di B (exec forzerà il sonno a sostituire C, invece di biforcarsi e far aspettare C sleep). Indipendentemente da questo avvertimento,

echo two > >(exec cat) | cat; echo three

eseguirà comunque correttamente le funzioni in ordine, come descritto in precedenza.


Come notato nella conversione con @MiniMax nei commenti alla mia risposta, ciò ha il rovescio della medaglia di influenzare lo stdout del comando e significa che l'output deve essere letto e scritto un tempo extra.
Stéphane Chazelas,

La spiegazione non è precisa Anon sta aspettando la catgenerazione >(cat). Come menziono nella mia risposta, il motivo per cui l' echo two > >(sleep 5 &>/dev/null) | cat; echo threeoutput threedopo 5 secondi è dovuto al fatto che le versioni correnti di bashspreco richiedono un ulteriore processo di shell in >(sleep 5)attesa sleepe tale processo ha ancora un valore pipeche impedisce al secondo catdi terminare. Se lo sostituisci con echo two > >(exec sleep 5 &>/dev/null) | cat; echo threeper eliminare quel processo aggiuntivo, scoprirai che ritorna immediatamente.
Stéphane Chazelas,

Crea una subshell nidificata? Ho cercato di esaminare l'implementazione di bash per capirlo, sono quasi sicuro che echo two > >(sleep 5 &>/dev/null)il minimo ottenga la sua sottostruttura. Si tratta di un dettaglio dell'implementazione non documentato che fa sì sleep 5che ottenga anche la propria sottostruttura? Se è documentato, sarebbe un modo legittimo per farlo con un minor numero di caratteri (A meno che non ci sia un ciclo stretto, non credo che qualcuno noterà problemi di prestazioni con una subshell o un gatto) `. Se non è documentato, rip, bel trucco però, non funzionerà su versioni future.
Nicholas Pipitone,

$(...), <(...)coinvolge effettivamente una subshell, ma ksh93 o zsh eseguono l'ultimo comando in quella subshell nello stesso processo, non bashè per questo che c'è ancora un altro processo che tiene aperta la pipe mentre sleepsta eseguendo e che non tiene aperta la pipe. Le versioni future di bashpotrebbero implementare un'ottimizzazione simile.
Stéphane Chazelas,

1
@ StéphaneChazelas Ho aggiornato la mia risposta e penso che l'attuale spiegazione della versione più corta sia corretta, ma sembra che tu conosca i dettagli di implementazione delle shell in modo da poter verificare. Penso che questa soluzione dovrebbe essere usata in contrapposizione alla danza del descrittore di file sebbene, dato che anche sotto exec, funziona come previsto.
Nicholas Pipitone,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.