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?
Odpowiedzi:
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:
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 aaaa
piszeaaaa
wiecznie w tym, co jest zasadniczo równoważne trybowi buforowanemu liniowo.yes
Narzę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 b
piszebbbb
wiecznie 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.
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:
stdbuf -o0
znajdującym się w GNU coreutils i niektórych innych systemach, takich jak FreeBSD. Alternatywnie możesz przejść do buforowania linii za pomocąstdbuf -oL
.unbuffer
. Niektóre programy mogą zachowywać się inaczej na inne sposoby, na przykładgrep
domyślnie używa kolorów, jeśli ich wyjściem jest terminal.--line-buffered
do GNU grep.Zobaczmy ponownie fragment powyżej, tym razem z buforowaniem linii po obu stronach.
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,
foo
chce zapisać 2 * P bajtów ibar
chce zapisać 3 bajty, wówczas jednym z możliwych przeplotów jest P bajtów zfoo
, następnie 3 bajty zbar
i P bajtów zfoo
.Wracając do powyższego przykładu tak + grep, w moim systemie
yes aaaa
zdarza 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 ziaaaa
samotnya
. Stąd linia z tym samotnym,a
po której następuje jedno pismo od grep, dające linię zabbbb
.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ą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.
źródło
cat
atomowo, 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?awk
utworzył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 zfind -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.http://mywiki.wooledge.org/BashPitfalls#Non-atomic_writes_with_xargs_-P przyjrzał się temu:
źródło
xargs echo
nie wywołuje wbudowanego bash echa, aleecho
narzę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.