Co zapobiega przeplataniu stdout / stderr?

14

Powiedzmy, że uruchamiam kilka procesów:

#!/usr/bin/env bash

foo &
bar &
baz &

wait;

Uruchomiłem powyższy skrypt tak:

foobarbaz | cat

o ile wiem, kiedy którykolwiek z procesów zapisuje do stdout / stderr, ich wyjście nigdy się nie przeplata - każda linia stdio wydaje się być atomowa. Jak to działa? Jakie narzędzie kontroluje atomowość każdej linii?

Alexander Mills
źródło
3
Ile danych generują twoje polecenia? Spróbuj, aby wyprowadzili kilka kilobajtów.
Kusalananda
Masz na myśli to, gdzie jedno z poleceń generuje kilka kilobajtów przed nową linią?
Alexander Mills
Nie, coś takiego: unix.stackexchange.com/a/452762/70524
muru

Odpowiedzi:

23

Przeplatają się! Próbowałeś tylko krótkich serii wyjściowych, które pozostają nieoświetlone, ale w praktyce trudno jest zagwarantować, że jakikolwiek konkretny wynik pozostaje nieoświetlony.

Buforowanie wyjściowe

To zależy od tego, jak programy buforują swoje dane wyjściowe. Biblioteki stdio , że większość programów używać, kiedy piszesz użyje bufory wyjściowe, aby bardziej wydajne. Zamiast wyprowadzać dane, gdy tylko program wywoła funkcję biblioteki w celu zapisu do pliku, funkcja przechowuje te dane w buforze i faktycznie wysyła dane dopiero po zapełnieniu bufora. Oznacza to, że dane wyjściowe są wykonywane partiami. Dokładniej, istnieją trzy tryby wyjściowe:

  • Niebuforowane: dane są zapisywane natychmiast, bez użycia bufora. Może to być powolne, jeśli program zapisuje dane wyjściowe w małych kawałkach, np. Znak po znaku. Jest to domyślny tryb dla standardowego błędu.
  • W pełni buforowane: dane są zapisywane tylko wtedy, gdy bufor jest pełny. Jest to tryb domyślny podczas zapisywania do potoku lub zwykłego pliku, z wyjątkiem stderr.
  • Buforowany wiersz: dane są zapisywane po każdej nowej linii lub po zapełnieniu bufora. Jest to tryb domyślny podczas pisania do terminala, z wyjątkiem stderr.

Programy mogą przeprogramowywać każdy plik, aby zachowywać się inaczej, i mogą jawnie opróżniać bufor. Bufor jest opróżniany automatycznie, gdy program zamyka plik lub kończy pracę normalnie.

Jeśli wszystkie programy, które piszą do tego samego potoku albo używają trybu buforowanego linii, albo trybu niebuforowanego i zapisują każdą linię pojedynczym wywołaniem funkcji wyjściowej, a jeśli linie są wystarczająco krótkie, aby pisać w jednym fragmencie, wyjście będzie przeplotem całych linii. Ale jeśli jeden z programów korzysta z trybu pełnego buforowania lub jeśli linie są zbyt długie, zobaczysz linie mieszane.

Oto przykład, w którym przeplatam dane wyjściowe z dwóch programów. Użyłem GNU coreutils na Linuksie; różne wersje tych narzędzi mogą zachowywać się inaczej.

  • yes aaaapisze aaaawiecznie w tym, co jest zasadniczo równoważne trybowi buforowanemu liniowo. yesNarzędzie rzeczywiście pisze wiele wierszy na raz, ale za każdym razem emituje wyjście, wyjście jest cała ilość linii.
  • echo bbbb; done | grep bpisze bbbbwiecznie w trybie pełnego buforowania. Wykorzystuje rozmiar bufora 8192, a każda linia ma długość 5 bajtów. Ponieważ 5 nie dzieli 8192, granice między zapisami nie są ogólnie na granicy linii.

Złóżmy je razem.

$ { 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

Jak widać, tak czasami przerywało grep i vice versa. Tylko około 0,001% linii zostało przerwanych, ale tak się stało. Wyjście jest losowe, więc liczba przerwań będzie się różnić, ale za każdym razem widziałem co najmniej kilka przerw. Gdyby linie były dłuższe, byłby większy ułamek przerwanych linii, ponieważ prawdopodobieństwo przerwania wzrasta wraz ze spadkiem liczby linii na bufor.

Istnieje kilka sposobów dostosowania buforowania wyjściowego . Najważniejsze z nich to:

  • Wyłącz buforowanie w programach korzystających z biblioteki stdio bez zmiany jej domyślnych ustawień w programie stdbuf -o0znajdującym się w GNU coreutils i niektórych innych systemach, takich jak FreeBSD. Alternatywnie możesz przejść do buforowania linii za pomocą stdbuf -oL.
  • Przełącz na buforowanie linii, kierując wyjście programu przez terminal utworzony właśnie w tym celu za pomocą unbuffer. Niektóre programy mogą zachowywać się inaczej na inne sposoby, na przykład grepdomyślnie używa kolorów, jeśli ich wyjściem jest terminal.
  • Skonfiguruj program, na przykład przekazując --line-buffereddo GNU grep.

Zobaczmy ponownie fragment powyżej, tym razem z buforowaniem linii po obu stronach.

{ 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

Więc tym razem tak nigdy nie przeszkadzało grep, ale grep czasami przerywało tak. Zobaczę dlaczego później.

Przeplatanie rur

Tak długo, jak każdy program wyprowadza po jednej linii na raz, a linie są wystarczająco krótkie, linie wyjściowe będą starannie oddzielone. Ale istnieje limit czasu, przez jaki linie mogą działać, aby to zadziałało. Sama rura ma bufor przesyłania. Gdy program wysyła dane do potoku, dane są kopiowane z programu piszącego do bufora przesyłania potoku, a następnie z bufora przesyłania potoku do programu czytającego. (Przynajmniej koncepcyjnie - jądro może czasami zoptymalizować to do pojedynczej kopii.)

Jeśli jest więcej danych do skopiowania niż mieści się w buforze przesyłania potoku, wówczas jądro kopiuje jeden bufor na raz. Jeśli wiele programów pisze do tego samego potoku, a pierwszy program wybrany przez jądro chce napisać więcej niż jeden bufor, to nie ma gwarancji, że jądro ponownie wybierze ten sam program za drugim razem. Na przykład, jeśli P jest rozmiarem bufora, foochce zapisać 2 * P bajtów i barchce zapisać 3 bajty, wówczas jednym z możliwych przeplotów jest P bajtów z foo, następnie 3 bajty z bari P bajtów z foo.

Wracając do powyższego przykładu tak + grep, w moim systemie yes aaaazdarza się pisać tyle wierszy, ile można zmieścić w buforze 8192 bajtów za jednym razem. Ponieważ do zapisania jest 5 bajtów (4 znaki do wydrukowania i nowa linia), oznacza to, że zapisuje 8190 bajtów za każdym razem. Rozmiar bufora potoku wynosi 4096 bajtów. Możliwe jest zatem pobranie 4096 bajtów z yes, następnie część danych wyjściowych z grep, a następnie reszta zapisu z yes (8190 - 4096 = 4094 bajtów). 4096 bajtów pozostawia miejsce na 819 linii zi aaaasamotny a. Stąd linia z tym samotnym, apo której następuje jedno pismo od grep, dające linię z abbbb.

Jeśli chcesz zobaczyć szczegóły tego, co się dzieje, getconf PIPE_BUF .powie ci rozmiar bufora potoku w twoim systemie i możesz zobaczyć pełną listę wywołań systemowych wykonanych przez każdy program za pomocą

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

Jak zagwarantować czyste przeplatanie linii

Jeśli długości linii są mniejsze niż rozmiar bufora rury, buforowanie linii gwarantuje, że na wyjściu nie będzie linii mieszanej.

Jeśli długości linii mogą być większe, nie można uniknąć arbitralnego mieszania, gdy wiele programów pisze na tym samym potoku. Aby zapewnić separację, musisz zmusić każdy program do zapisu do innej potoku i użyć programu do połączenia linii. Na przykład GNU Parallel robi to domyślnie.

Gilles „SO- przestań być zły”
źródło
interesujące, więc co może być dobrym sposobem na upewnienie się, że wszystkie wiersze zostały zapisane catatomowo, tak że proces cat odbiera całe linie z foo / bar / baz, ale nie pół linii z jednej i pół linii z drugiej itd. Czy mogę coś zrobić ze skryptem bash?
Alexander Mills
1
brzmi to odnosi się do mojego przypadku również tam, gdzie miałem setki plików i awkutworzyłem dwa (lub więcej) wierszy wyjścia dla tego samego identyfikatora, find -type f -name 'myfiles*' -print0 | xargs -0 awk '{ seen[$1]= seen[$1] $2} END { for(x in seen) print x, seen[x] }' ale z find -type f -name 'myfiles*' -print0 | xargs -0 cat| awk '{ seen[$1]= seen[$1] $2} END { for(x in seen) print x, seen[x] }'tym poprawnie utworzyłem tylko jeden wiersz dla każdego identyfikatora.
αғsнιη
Aby zapobiec przeplataniu, mogę to zrobić w środowisku programistycznym, takim jak Node.js, ale używając bash / shell, nie jestem pewien, jak to zrobić.
Alexander Mills
1
@JoL Jest to spowodowane wypełnianiem się bufora rur. Wiedziałem, że będę musiał napisać drugą część historii… Gotowe.
Gilles „SO- przestań być zły”
1
@OlegzandrDenman TLDR dodał: przeplatają się. Powód jest skomplikowany.
Gilles „SO- przestań być zły”
1

http://mywiki.wooledge.org/BashPitfalls#Non-atomic_writes_with_xargs_-P przyjrzał się temu:

GNU xargs obsługuje równoległe uruchamianie wielu zadań. -P n gdzie n to liczba zadań do uruchomienia równoległego.

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

Działa to dobrze w wielu sytuacjach, ale ma zwodniczą wadę: jeśli $ a zawiera więcej niż ~ 1000 znaków, echo może nie być atomowe (może być podzielone na wiele wywołań write ()) i istnieje ryzyko, że dwie linie będą mieszane.

$ 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 +++

Oczywiście ten sam problem pojawia się, jeśli istnieje wiele wywołań echa lub 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!

Dane wyjściowe z równoległych zadań są mieszane razem, ponieważ każde zadanie składa się z dwóch (lub więcej) osobnych wywołań write ().

Jeśli potrzebujesz niezmiksowanych danych wyjściowych, zaleca się użycie narzędzia, które gwarantuje, że dane wyjściowe zostaną zserializowane (takie jak GNU Parallel).

Ole Tange
źródło
Ta sekcja jest błędna. xargs echonie wywołuje wbudowanego bash echa, ale echonarzędzie z $PATH. Poza tym nie mogę odtworzyć tego zachowania echa bashu z bash 4.4. W systemie Linux zapisy do potoku (nie / dev / null) większego niż 4K nie są jednak gwarantowane jako atomowe.
Stéphane Chazelas