Cosa impedisce l'interdizione tra stdout / stderr?


13

Supponiamo che io esegua alcuni processi:

#!/usr/bin/env bash

foo &
bar &
baz &

wait;

Eseguo lo script sopra in questo modo:

foobarbaz | cat

per quanto posso dire, quando uno qualsiasi dei processi scrive su stdout / stderr, il loro output non si interseca mai - ogni riga di stdio sembra essere atomica. Come funziona? Quale utility controlla come ogni linea è atomica?


3
Quanti dati emettono i tuoi comandi? Prova a farli emettere qualche chilobyte.
Kusalananda

Intendi dove uno dei comandi genera qualche kb prima di una nuova riga?
Alexander Mills,

No, qualcosa del genere: unix.stackexchange.com/a/452762/70524
muru,

Risposte:


22

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 aaaascrive aaaaper sempre in ciò che è essenzialmente equivalente alla modalità buffer di riga. L' yesutilità in realtà scrive più righe alla volta, ma ogni volta che emette output, l'output è un numero intero di righe.
  • echo bbbb; done | grep bscrive bbbbper 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 -o0trovato 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 greputilizza i colori per impostazione predefinita se il loro output è un terminale.
  • Configurare il programma, ad esempio passando --line-buffereda 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, foovuole scrivere 2 * P byte e barvuole scrivere 3 byte, allora una possibile interfogliatura è P byte da foo, quindi 3 byte da bare P byte da foo.

Tornando all'esempio yes + grep sopra, sul mio sistema, yes aaaasembra 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 aaaae solo a. Quindi una linea con questo solitario aseguita 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.


interessante, quindi quale potrebbe essere un buon modo per garantire che tutte le righe siano state scritte catatomicamente, in modo tale che il processo cat riceva intere righe da foo / bar / baz ma non mezza riga da una e mezza riga da un'altra, ecc. C'è qualcosa che posso fare con lo script bash?
Alexander Mills,

1
sembra che questo valga anche per il mio caso in cui avevo centinaia di file e sono awkstate prodotte due (o più) righe di output per lo stesso ID con find -type f -name 'myfiles*' -print0 | xargs -0 awk '{ seen[$1]= seen[$1] $2} END { for(x in seen) print x, seen[x] }' ma con find -type f -name 'myfiles*' -print0 | xargs -0 cat| awk '{ seen[$1]= seen[$1] $2} END { for(x in seen) print x, seen[x] }'esso correttamente prodotto solo una riga per ogni ID.
αғsнιη,

Per evitare qualsiasi interleaving, posso farlo con un env di programmazione come Node.js, ma con bash / shell, non sono sicuro di come farlo.
Alexander Mills,

1
@JoL È dovuto al riempimento del buffer del tubo. Sapevo che avrei dovuto scrivere la seconda parte della storia ... Fatto.
Gilles 'SO- smetti di essere malvagio' il

1
@OlegzandrDenman TLDR aggiunto: fanno interleave. Il motivo è complicato
Gilles 'SO- smetti di essere malvagio' il

1

http://mywiki.wooledge.org/BashPitfalls#Non-atomic_writes_with_xargs_-P ha dato un'occhiata a questo:

GNU xargs supporta l'esecuzione di più lavori in parallelo. -P n dove n è il numero di lavori da eseguire in parallelo.

seq 100 | xargs -n1 -P10 echo "$a" | grep 5
seq 100 | xargs -n1 -P10 echo "$a" > myoutput.txt

Funzionerà bene in molte situazioni ma ha un difetto ingannevole: se $ a contiene più di ~ 1000 caratteri, l'eco potrebbe non essere atomica (potrebbe essere suddivisa in più chiamate write ()) e c'è il rischio che due righe sarà mescolato.

$ perl -e 'print "a"x2000, "\n"' > foo
$ strace -e write bash -c 'read -r foo < foo; echo "$foo"' >/dev/null
write(1, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 1008) = 1008
write(1, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 993) = 993
+++ exited with 0 +++

Ovviamente lo stesso problema si presenta se ci sono più chiamate da echo o printf:

slowprint() {
  printf 'Start-%s ' "$1"
  sleep "$1"
  printf '%s-End\n' "$1"
}
export -f slowprint
seq 10 | xargs -n1 -I {} -P4 bash -c "slowprint {}"
# Compare to no parallelization
seq 10 | xargs -n1 -I {} bash -c "slowprint {}"
# Be sure to see the warnings in the next Pitfall!

Gli output dei lavori paralleli vengono mescolati insieme, poiché ogni lavoro è costituito da due (o più) chiamate write () separate.

Se hai bisogno di output non miscelati, si consiglia quindi di utilizzare uno strumento che garantisca che l'output verrà serializzato (come GNU Parallel).


Quella sezione è sbagliata. xargs echonon chiama l'eco bash incorporato, ma l' echoutilità da $PATH. E comunque non riesco a riprodurre quel comportamento eco di bash con bash 4.4. Su Linux, le scritture su una pipe (non / dev / null) più grandi di 4K non sono garantite per essere atomiche.
Stéphane Chazelas il
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.