„Nieszczelne” rury w systemie Linux

12

Załóżmy, że masz potok podobny do następującego:

$ a | b

Jeśli bprzestanie przetwarzać standardowe wejście, po pewnym czasie rura się azapełni i zacznie pisać, aż do brozpoczęcia standardowego , zostanie zablokowane (dopóki albo nie zacznie ponownie przetwarzać, albo umrze).

Gdybym chciał tego uniknąć, mogłem pokusić się o użycie większej rury (lub prościej buffer(1)) w taki sposób:

$ a | buffer | b

To po prostu dałoby mi więcej czasu, ale w akońcu przestałoby.

Chciałbym mieć (w bardzo konkretnym scenariuszu, do którego się odnoszę), aby mieć „nieszczelną” rurkę, która po zapełnieniu upuszczałaby niektóre dane (najlepiej wiersz po wierszu) z bufora, aby akontynuować przetwarzanie (jak zapewne można sobie wyobrazić, dane płynące w potoku są zbędne, tj. przetwarzanie danych bjest mniej ważne niż amożliwość uruchomienia bez blokowania).

Podsumowując, chciałbym mieć coś w rodzaju ograniczonego, nieszczelnego bufora:

$ a | leakybuffer | b

Prawdopodobnie mógłbym to zaimplementować dość łatwo w dowolnym języku, po prostu zastanawiałem się, czy brakuje mi czegoś „gotowego do użycia” (lub czegoś w rodzaju bash-line).

Uwaga: w przykładach używam zwykłych potoków, ale pytanie w równym stopniu dotyczy nazwanych potoków


Przyznając odpowiedź poniżej, zdecydowałem się również wdrożyć komendę leakybuffer, ponieważ poniższe proste rozwiązanie miało pewne ograniczenia: https://github.com/CAFxX/leakybuffer

CAFxX
źródło
Czy nazwane rury naprawdę się wypełniają? Myślałem, że nazwane rury rozwiązaniem tego problemu, ale nie mogłem powiedzieć tego na pewno.
Wildcard,
3
Nazwane potoki mają (domyślnie) taką samą pojemność jak potoki bez nazw, AFAIK
CAFxX 11.08.16

Odpowiedzi:

14

Najłatwiejszym sposobem byłoby przepuszczenie przez jakiś program, który ustawia wyjście nieblokujące. Oto prosty perl oneliner (który możesz zapisać jako dziurawy bufor ), który to robi:

więc a | bstajesz się:

a | perl -MFcntl -e \
    'fcntl STDOUT,F_SETFL,O_NONBLOCK; while (<STDIN>) { print }' | b

odczytuje dane wejściowe i zapisuje dane wyjściowe (tak samo jak cat(1)), ale dane wyjściowe nie są blokowane - co oznacza, że ​​jeśli zapis nie powiedzie się, zwróci błąd i utraci dane, ale proces będzie kontynuowany z następnym wierszem danych wejściowych, ponieważ wygodnie ignorujemy błąd. Proces jest buforowany tak, jak chcesz, ale patrz zastrzeżenie poniżej.

możesz przetestować na przykład:

seq 1 500000 | perl -w -MFcntl -e \
    'fcntl STDOUT,F_SETFL,O_NONBLOCK; while (<STDIN>) { print }' | \
    while read a; do echo $a; done > output

otrzymasz outputplik z utraconymi liniami (dokładne wyjście zależy od prędkości twojej powłoki itp.) w następujący sposób:

12768
12769
12770
12771
12772
12773
127775610
75611
75612
75613

widzisz, po czym powłoka straciła linie 12773, ale także anomalia - perl nie miał wystarczającej ilości bufora, 12774\nale zrobił 1277to, więc napisał właśnie to - i dlatego następna liczba 75610nie zaczyna się na początku linii, co czyni ją małą brzydki.

Można to poprawić poprzez wykrywanie perla, gdy zapis nie powiódł się całkowicie, a następnie spróbować wyczyścić pozostałą część wiersza, ignorując nowe linie, ale to znacznie skomplikowałoby skrypt perla, więc jest to ćwiczenie zainteresowany czytelnik :)

Aktualizacja (dla plików binarnych): Jeśli nie przetwarzasz wierszy zakończonych znakiem nowej linii (takich jak pliki dziennika lub podobne), musisz nieco zmienić polecenie, lub perl zużyje dużą ilość pamięci (w zależności od tego, jak często znaki nowego wiersza pojawiają się na wejściu):

perl -w -MFcntl -e 'fcntl STDOUT,F_SETFL,O_NONBLOCK; while (read STDIN, $_, 4096) { print }' 

będzie działać poprawnie również dla plików binarnych (bez zużywania dodatkowej pamięci).

Update2 - ładniejszy plik wyjściowy: Unikanie buforów wyjściowych ( syswritezamiast print):

seq 1 500000 | perl -w -MFcntl -e \
    'fcntl STDOUT,F_SETFL,O_NONBLOCK; while (<STDIN>) { syswrite STDOUT,$_ }' | \
    while read a; do echo $a; done > output

wydaje się, że naprawia dla mnie problemy ze „połączonymi liniami”:

12766
12767
12768
16384
16385
16386

(Uwaga: można sprawdzić, które linie wyjściowe zostały odcięte za pomocą: perl -ne '$c++; next if $c==$_; print "$c $_"; $c=$_' outputoneliner)

Matija Nalis
źródło
Uwielbiam oneliner: nie jestem ekspertem od Perla, jeśli ktokolwiek mógłby zasugerować powyższe ulepszenia, byłoby to niesamowite
CAFxX
1
To wydaje się działać do pewnego stopnia . Ale kiedy obserwuję moje polecenie, to znaczy perl -w -MFcntl -e 'fcntl STDOUT,F_SETFL,O_WRONLY|O_NONBLOCK; while (<STDIN>) { print }' | aplay -t raw -f dat --buffer-size=16000, perl wydaje się ciągle przydzielać więcej pamięci, dopóki nie zostanie zabita przez menedżera OOM.
Ponkadoodle,
@Wallacoloo dziękuję za zwrócenie na to uwagi, moja sprawa przesyłała strumieniowo pliki dziennika ... Zobacz zaktualizowaną odpowiedź na drobną zmianę potrzebną do obsługi plików binarnych.
Matija Nalis
Zobacz także GNU ddS” dd oflag=nonblock status=none.
Stéphane Chazelas,
1
Przepraszam, znowu mój zły, faktycznie zapisuje mniej niż PIPE_BUF bajtów (4096 w Linuksie, wymagane przez POSIX co najmniej 512) jest gwarantowany, że są atomowe, więc $| = 1twoje syswrite()podejście naprawdę zapobiega krótkim zapisom, o ile linie są wystarczająco krótkie.
Stéphane Chazelas,