potok, {lista; } działa tylko z niektórymi programami

13

Potrzebujesz wyjaśnień od zaawansowanych użytkowników na temat takiego nieprzewidzianego zachowania:

ps -eF | { head -n 1;grep worker; }
UID        PID  PPID  C    SZ   RSS PSR STIME TTY          TIME CMD
root       441     2  0     0     0   2 paź15 ?       00:00:00 [kworker/2:1H]

wszystko wygląda dobrze, podczas gdy

ls -la / | { head -n 1;grep sbin; }

wyświetla tylko dane wyjściowe z head

... Myślałem o tym stdout 2>&1i nie działa ani dla mnie to dziwne, jakieś wyjaśnienia lub sugestie, jak sobie z tym poradzić?

ast
źródło
1
Ten ostatni powinien wydrukować wszystko. headI grepzrobić nic tam.
jordanm
tak masz rację. Ale zamiast tego, dlaczego ps -eF działa, gdy ls -la / not?
ast

Odpowiedzi:

9

Zrobiłem trochę badań przy użyciu stracei wydaje się, że jest to spowodowane sposobem, w jaki program po lewej stronie potoku zapisuje do terminala. Po wykonaniu lspolecenia zapisuje wszystkie dane w jednym write(). Powoduje headto zużycie całego standardu.

Z drugiej strony pszapisuje dane w partiach, więc tylko pierwsza write()jest zużywana head, a potem istnieje. Późniejsze wywołania write()przejdą do nowo odrodzonego grepprocesu.

Oznacza to, że nie zadziałałoby, gdyby proces, który próbujesz wykonać, grepnie wystąpiłby w pierwszym write(), ponieważ grepnie zobaczy wszystkich danych (widzi nawet mniej niż dane minus pierwszy wiersz).

Oto przykład próby grep dla pid 1 w moim systemie:

$ ps -eF | { head -n2; }
UID        PID  PPID  C    SZ   RSS PSR STIME TTY          TIME CMD
root         1     0  0  1697  3768   2 Oct03 ?        00:00:03 /lib/systemd/systemd
$ ps -eF | grep '/lib/systemd/systemd$'
root         1     0  0  1697  3768   2 Oct03 ?        00:00:03 /lib/systemd/systemd
$ ps -eF | { head -n1; grep '/lib/systemd/systemd$'; }
UID        PID  PPID  C    SZ   RSS PSR STIME TTY          TIME CMD

Twój ps -eFprzykład działa tylko przez przypadek.

Jordan
źródło
wielkie i kompleksowe expalnation dziękuję
ast
1
W rzeczywistości jest to bardziej wyścig. Po prostu wolniej wykonuje wiele write()połączeń. Jeśli headbyły powolne wykonać To read()połączenie (tak, że bufor rura posiada wszystkie dane w nim), to zachowuje się tak samo do obu lsi ps.
Patrick,
6

Jest to spowodowane buforowaniem w glibc. W przypadku lswyjścia znajduje się w jednym buforze wewnętrznym i jako takie jest przekazywane tylko do head. W przypadku ps -eFdanych wyjściowych wartość jest większa, więc po headzakończeniu następująca grepotrzyma pozostałe części (ale nie całą) danych wyjściowych ps.

Możesz się go pozbyć usuwając buforowanie potoku - na przykład za pomocą sed -u(nie jestem pewien, czy nie jest to rozszerzenie GNU):

$ ls -al / | sed -u "#" | { head -n 1; grep bin; }
total 76
drwxr-xr-x   2 root root  4096 Oct  2 21:52 bin
drwxr-xr-x   2 root root  8192 Oct  3 01:54 sbin
Peter
źródło
4

Dzieje się tak, że head -n 1czyta więcej niż 1 linię. Aby uzyskać optymalną przepustowość, head odczytuje bajty, więc może czytać 1024 bajty na raz, a następnie przeglądać te bajty w poszukiwaniu pierwszego wiersza. Ponieważ podział linii może wystąpić w środku tych 1024 bajtów, reszta danych zostaje utracona. Nie można go ponownie umieścić na rurze. Tak więc następny proces, który się wykonuje, pobiera tylko bajty 1025 i dalej.

Twoje pierwsze polecenie się powiedzie, ponieważ kworkerproces następuje po pierwszym fragmencie, który headczyta.

Aby to zadziałało, headmusiałbym czytać 1 znak na raz. Ale to jest bardzo wolne, więc nie.
Jedynym sposobem na wydajne zrobienie czegoś takiego jest posiadanie jednego procesu zarówno „head”, jak i „grep”.

Oto 2 sposoby na zrobienie tego:

echo -e '1\n2\n3\n4\n5' | perl -ne 'print if $i++ == 0 || /4/'

lub

echo -e '1\n2\n3\n4\n5' | awk '{if (NR == 1 || /4/) print }'

Jest o wiele więcej ...

Patrick
źródło
tak, znałem „sposób awk” do obsługi tego zadania, ale zastanawiałem się, dlaczego zachowanie było tak nieprzewidywalne w przypadku {list; }. Dziękujemy za wyjaśnienie, jak to działa. Jestem pod wrażeniem wszystkich powyższych odpowiedziach
ast
2

Jeśli chcesz tylko pierwszy wiersz lub dwa, następujący typ lewy działa i pozwala uniknąć problemów z buforowaniem spowodowanych użyciem dwóch różnych poleceń do odczytu strumienia wyjściowego:

$ ps -eF   | { IFS= read -r x ; echo "$x" ; grep worker; }
$ ls -la / | { IFS= read -r x ; echo "$x" ; grep sbin; }

Jest readon wbudowany w powłokę i nie zużywa całego bufora danych wejściowych tylko do wyprowadzenia jednego wiersza, więc użycie readpozostawia resztę danych wyjściowych dla następującego polecenia.

Jeśli chcesz zaakcentować problemy z buforowaniem pokazane w przykładach, które używają dwóch różnych poleceń, dodaj sleepje, aby wyeliminować problemy z synchronizacją i pozwól, aby polecenie po lewej stronie wygenerowało wszystkie dane wyjściowe, zanim polecenia po prawej spróbują odczytać dowolne z to:

$ ps -eF   | { sleep 5 ; head -n 1 ; grep worker; }
$ ls -la / | { sleep 5 ; head -n 1 ; grep sbin; }

Teraz oba powyższe przykłady zawodzą w ten sam sposób - headodczytuje cały bufor danych wyjściowych tylko w celu wytworzenia jednej linii, a bufor ten nie jest dostępny dla następujących grep.

Możesz zobaczyć problem buforowania jeszcze wyraźniej, używając kilku przykładów, które numerują linie wyjściowe, dzięki czemu możesz stwierdzić, które linie brakuje:

$ ps -eF          | cat -n | { sleep 5 ; head -n 1 ; head ; }
$ ls -la /usr/bin | cat -n | { sleep 5 ; head -n 1 ; head ; }

Prostym sposobem na dostrzeżenie problemu buforowania jest użycie, seqktóra generuje listę liczb. Możemy łatwo stwierdzić, które liczby zaginęły:

$ seq 1 100000    | { sleep 5 ; head -n 1 ; head ; }
1

1861
1862
1863
1864
1865
1866
1867
1868
1869

Moje rozwiązanie polegające na użyciu powłoki do odczytu i echa pierwszego wiersza działa poprawnie nawet po dodaniu opóźnienia uśpienia:

$ seq 1 100000 | { sleep 5 ; IFS= read -r x ; echo "$x" ; head ; }
1
2
3
4
5
6
7
8
9
10
11

Poniżej znajduje się pełny przykład pokazujący headproblemy z buforowaniem, pokazujący, jak headzużywa cały bufor danych wyjściowych, aby za każdym razem wygenerować pięć wierszy. Zużyty bufor nie jest dostępny dla następnego headpolecenia w sekwencji:

$ seq 1 100000 | { sleep 5 ; head -5 ; head -5 ; head -5 ; head -5 ; }
1
2
3
4
5

1861
1862
1863
1864
499
3500
3501
3502
3503
7
5138
5139
5140
5141

Patrząc na 1861powyższą liczbę , możemy obliczyć wielkość używanego bufora, headzliczając dane seqwyjściowe od 1do 1860:

$ seq 1 1860 | wc -c
8193

Widzimy, że headbuforowanie polega na tym, że jednocześnie odczytuje pełne 8KB (8 * 1024 bajtów) danych wyjściowych potoku, nawet w celu wytworzenia zaledwie kilku wierszy własnych danych wyjściowych.

Ian D. Allen
źródło