Perché c'è una condizione di gara
I due lati di un tubo vengono eseguiti in parallelo, non uno dopo l'altro. C'è un modo molto semplice per dimostrarlo: corri
time sleep 1 | sleep 1
Questo richiede un secondo, non due.
La shell avvia due processi figlio e attende il completamento di entrambi. Questi due processi vengono eseguiti in parallelo: l'unico motivo per cui uno di essi si sincronizzerebbe con l'altro è quando deve attendere l'altro. Il punto più comune di sincronizzazione è quando il lato destro si blocca in attesa della lettura dei dati sull'input standard e si sblocca quando il lato sinistro scrive più dati. Il contrario può anche accadere, quando il lato destro è lento nella lettura dei dati e il lato sinistro si blocca nella sua operazione di scrittura fino a quando il lato destro non legge più dati (c'è un buffer nel pipe stesso, gestito dal kernel, ma ha una dimensione massima ridotta).
Per osservare un punto di sincronizzazione, osservare i seguenti comandi ( sh -x
stampa ogni comando mentre lo esegue):
time sh -x -c '{ sleep 1; echo a; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { sleep 1; cat; }'
time sh -x -c '{ sleep 2; echo a; } | { cat; sleep 1; }'
Gioca con le variazioni fino a quando non ti senti a tuo agio con ciò che osservi.
Dato il comando composto
cat tmp | head -1 > tmp
il processo a sinistra esegue le seguenti operazioni (ho elencato solo i passaggi rilevanti per la mia spiegazione):
- Eseguire il programma esterno
cat
con l'argomento tmp
.
- Aperto
tmp
per la lettura.
- Sebbene non abbia raggiunto la fine del file, leggi un blocco dal file e scrivilo nell'output standard.
Il processo a destra procede come segue:
- Reindirizzare l'output standard su
tmp
, troncando il file nel processo.
- Eseguire il programma esterno
head
con l'argomento -1
.
- Leggere una riga dallo standard input e scriverlo nello standard output.
L'unico punto di sincronizzazione è che right-3 attende che left-3 abbia elaborato una riga completa. Non c'è sincronizzazione tra left-2 e right-1, quindi possono avvenire in entrambi gli ordini. L'ordine in cui si verificano non è prevedibile: dipende dall'architettura della CPU, dalla shell, dal kernel, da quali core si pianificano i processi, da ciò che interrompe la CPU in quel momento, ecc.
Come cambiare il comportamento
Non è possibile modificare il comportamento modificando un'impostazione di sistema. Il computer fa quello che gli dici di fare. Gli hai detto di troncare tmp
e leggere tmp
in parallelo, quindi fa le due cose in parallelo.
Ok, c'è un'impostazione di sistema che potresti cambiare: potresti sostituirla /bin/bash
con un altro programma che non è bash. Spero che sia ovvio che questa non è una buona idea.
Se si desidera che il troncamento avvenga prima del lato sinistro del tubo, è necessario inserirlo all'esterno della tubazione, ad esempio:
{ cat tmp | head -1; } >tmp
o
( exec >tmp; cat tmp | head -1 )
Non ho idea del perché tu voglia questo però. Qual è il punto di lettura di un file che sai essere vuoto?
Al contrario, se si desidera che il reindirizzamento dell'output (incluso il troncamento) si verifichi al cat
termine della lettura, è necessario bufferizzare completamente i dati in memoria, ad es.
line=$(cat tmp | head -1)
printf %s "$line" >tmp
oppure scrivi in un altro file e poi spostalo in posizione. Questo è di solito il modo più efficace per eseguire operazioni negli script e presenta il vantaggio che il file viene scritto per intero prima che sia visibile attraverso il nome originale.
cat tmp | head -1 >new && mv new tmp
La collezione moreutils include un programma che fa proprio questo, chiamato sponge
.
cat tmp | head -1 | sponge tmp
Come rilevare il problema automaticamente
Se il tuo obiettivo era prendere script scritti male e capire automaticamente dove si rompono, allora scusa, la vita non è così semplice. L'analisi di runtime non trova in modo affidabile il problema perché a volte cat
termina la lettura prima che si verifichi il troncamento. L'analisi statica può in linea di principio farlo; l'esempio semplificato nella tua domanda viene colto da Shellcheck , ma potrebbe non rilevare un problema simile in uno script più complesso.