Fanno interleave! Hai provato solo brevi raffiche di output, che rimangono non divise, ma in pratica è difficile garantire che qualsiasi particolare uscita rimanga non divisa.
Buffering dell'output
Dipende da come i programmi bufferizzano il loro output. La libreria stdio utilizzata dalla maggior parte dei programmi durante la scrittura utilizza buffer per rendere l'output più efficiente. Invece di emettere dati non appena il programma chiama una funzione di libreria per scrivere su un file, la funzione archivia questi dati in un buffer e li genera in realtà solo una volta riempito il buffer. Ciò significa che l'output viene eseguito in batch. Più precisamente, ci sono tre modalità di output:
- Senza buffer: i dati vengono scritti immediatamente, senza utilizzare un buffer. Questo può essere lento se il programma scrive il suo output in piccoli pezzi, ad esempio carattere per carattere. Questa è la modalità predefinita per l'errore standard.
- Completamente bufferizzato: i dati vengono scritti solo quando il buffer è pieno. Questa è la modalità predefinita quando si scrive su una pipe o su un file normale, tranne con stderr.
- Buffer di riga: i dati vengono scritti dopo ogni nuova riga o quando il buffer è pieno. Questa è la modalità predefinita quando si scrive su un terminale, tranne con stderr.
I programmi possono riprogrammare ogni file in modo che si comporti in modo diverso e cancellare esplicitamente il buffer. Il buffer viene scaricato automaticamente quando un programma chiude il file o esce normalmente.
Se tutti i programmi che scrivono sulla stessa pipe utilizzano la modalità buffer di linea o usano la modalità non bufferizzata e scrivono ogni riga con una singola chiamata su una funzione di output e se le linee sono abbastanza corte da scrivere in un singolo blocco, allora l'output sarà un'interleaving di intere righe. Ma se uno dei programmi utilizza la modalità completamente buffer, o se le linee sono troppo lunghe, vedrai linee miste.
Ecco un esempio in cui interleave l'output di due programmi. Ho usato i coreutils GNU su Linux; versioni diverse di queste utility possono comportarsi diversamente.
yes aaaa
scrive aaaa
per sempre in ciò che è essenzialmente equivalente alla modalità buffer di riga. L' yes
utilità in realtà scrive più righe alla volta, ma ogni volta che emette output, l'output è un numero intero di righe.
echo bbbb; done | grep b
scrive bbbb
per sempre in modalità con buffer completo. Utilizza una dimensione del buffer di 8192 e ogni riga è lunga 5 byte. Poiché 5 non divide 8192, i confini tra le scritture non sono in linea di massima in generale.
Mettiamoli insieme.
$ { yes aaaa & while true; do echo bbbb; done | grep b & } | head -n 999999 | grep -e ab -e ba
bbaaaa
bbbbaaaa
baaaa
bbbaaaa
bbaaaa
bbbaaaa
ab
bbbbaaa
Come puoi vedere, a volte sì interrompe grep e viceversa. Solo circa lo 0,001% delle linee è stato interrotto, ma è successo. L'output è randomizzato, quindi il numero di interruzioni varierà, ma ho visto almeno alcune interruzioni ogni volta. Vi sarebbe una frazione maggiore di linee interrotte se le linee fossero più lunghe, poiché la probabilità di un'interruzione aumenta al diminuire del numero di linee per buffer.
Esistono diversi modi per regolare il buffering dell'output . I principali sono:
- Disattiva il buffering nei programmi che usano la libreria stdio senza cambiare le sue impostazioni predefinite con il programma
stdbuf -o0
trovato nei coreutils GNU e alcuni altri sistemi come FreeBSD. In alternativa, puoi passare al buffering di linea con stdbuf -oL
.
- Passa al buffering di linea indirizzando l'output del programma attraverso un terminale creato appositamente per questo scopo
unbuffer
. Alcuni programmi possono comportarsi diversamente in altri modi, ad esempio grep
utilizza i colori per impostazione predefinita se il loro output è un terminale.
- Configurare il programma, ad esempio passando
--line-buffered
a GNU grep.
Vediamo di nuovo lo snippet sopra, questa volta con buffering di riga su entrambi i lati.
{ stdbuf -oL yes aaaa & while true; do echo bbbb; done | grep --line-buffered b & } | head -n 999999 | grep -e ab -e ba
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
Quindi questa volta sì non ha mai interrotto grep, ma a volte grep ha interrotto sì. Verrò al perché più tardi.
Interleaving del tubo
Fintanto che ciascun programma emette una riga alla volta e le linee sono abbastanza corte, le linee di uscita saranno ordinatamente separate. Ma c'è un limite a quanto possono essere lunghe le linee affinché questo funzioni. La pipe stessa ha un buffer di trasferimento. Quando un programma viene emesso su una pipe, i dati vengono copiati dal programma di scrittura nel buffer di trasferimento della pipe, quindi successivamente dal buffer di trasferimento della pipe nel programma di lettura. (Almeno concettualmente - il kernel a volte può ottimizzare questo in una singola copia.)
Se ci sono più dati da copiare di quelli che si adattano al buffer di trasferimento della pipe, il kernel copia un buffering alla volta. Se più programmi stanno scrivendo sulla stessa pipe, e il primo programma scelto dal kernel vuole scrivere più di un buffling, allora non c'è garanzia che il kernel sceglierà di nuovo lo stesso programma la seconda volta. Ad esempio, se P è la dimensione del buffer, foo
vuole scrivere 2 * P byte e bar
vuole scrivere 3 byte, allora una possibile interfogliatura è P byte da foo
, quindi 3 byte da bar
e P byte da foo
.
Tornando all'esempio yes + grep sopra, sul mio sistema, yes aaaa
sembra che scriva quante più righe si possano adattare in un buffer di 8192 byte in una volta sola. Poiché ci sono 5 byte da scrivere (4 caratteri stampabili e la nuova riga), ciò significa che scrive ogni volta 8190 byte. La dimensione del buffer del tubo è 4096 byte. È quindi possibile ottenere 4096 byte da yes, quindi un po 'di output da grep e quindi il resto della scrittura da yes (8190 - 4096 = 4094 byte). 4096 byte lascia spazio a 819 linee con aaaa
e solo a
. Quindi una linea con questo solitario a
seguita da una scrittura da grep, dando una linea con abbbb
.
Se vuoi vedere i dettagli di ciò che sta succedendo, getconf PIPE_BUF .
ti dirà la dimensione del buffer di pipe sul tuo sistema e puoi vedere un elenco completo delle chiamate di sistema effettuate da ciascun programma con
strace -s9999 -f -o line_buffered.strace sh -c '{ stdbuf -oL yes aaaa & while true; do echo bbbb; done | grep --line-buffered b & }' | head -n 999999 | grep -e ab -e ba
Come garantire l'interleaving della linea pulita
Se la lunghezza della linea è inferiore alla dimensione del buffer del tubo, il buffering della linea garantisce che non ci sarà alcuna linea mista nell'output.
Se le lunghezze delle linee possono essere maggiori, non c'è modo di evitare il mixaggio arbitrario quando più programmi scrivono sulla stessa pipe. Per garantire la separazione, è necessario fare in modo che ciascun programma scriva su una pipe diversa e utilizzare un programma per combinare le linee. Ad esempio GNU Parallel lo fa per impostazione predefinita.