Jak sprawić, by odczyt i zapis tego samego pliku w tym samym potoku zawsze „kończyły się niepowodzeniem”?

9

Powiedz, że mam następujący skrypt:

#!/bin/bash
for i in $(seq 1000)
do
    cp /etc/passwd tmp
    cat tmp | head -1 | head -1 | head -1 > tmp  #this is the key line
    cat tmp
done

W kluczowym wierszu czytam i piszę ten sam plik, tmpktóry czasem się nie udaje.

(Czytam, że dzieje się tak z powodu warunków wyścigu, ponieważ procesy w potoku są wykonywane równolegle, co nie rozumiem dlaczego - każdy headmusi wziąć dane z poprzedniego, prawda? To nie jest moje główne pytanie, ale możesz też odpowiedzieć.)

Po uruchomieniu skryptu wyświetla około 200 wierszy. Czy jest jakiś sposób, w jaki mogę zmusić ten skrypt do wyświetlania zawsze 0 linii (więc przekierowanie We / Wy tmpjest zawsze przygotowywane jako pierwsze, a więc dane są zawsze niszczone)? Dla jasności mam na myśli zmianę ustawień systemu, a nie ten skrypt.

Dziękuję za twoje pomysły.

karlosss
źródło

Odpowiedzi:

2

Odpowiedź Gillesa wyjaśnia stan wyścigu. Odpowiem tylko na tę część:

Czy jest jakiś sposób, w jaki mogę zmusić ten skrypt do wyprowadzania zawsze 0 linii (więc przekierowanie We / Wy do tmp jest zawsze przygotowywane jako pierwsze, więc dane są zawsze niszczone)? Dla jasności mam na myśli zmianę ustawień systemu

IDK, jeśli narzędzie do tego już istnieje, ale mam pomysł, jak można je wdrożyć. (Zauważ jednak, że nie zawsze będzie to 0 linii, tylko przydatny tester, który łatwo łapie takie proste wyścigi i kilka bardziej skomplikowanych wyścigów. Zobacz komentarz @Gillesa .) Nie gwarantuje to, że skrypt jest bezpieczny , ale może być użytecznym narzędziem do testowania, podobnym do testowania wielowątkowego programu na różnych procesorach, w tym słabo uporządkowanych procesorach innych niż x86, takich jak ARM.

Uruchomiłbyś to jako racechecker bash foo.sh

Użyj tego samego systemu-call śledzenie / przechwytywania obiektów, które strace -fi ltrace -fwykorzystanie dołączyć do każdego procesu potomnego. (W systemie Linux jest to to samo ptracewywołanie systemowe, którego używa GDB i inne debugery do ustawiania punktów przerwania, pojedynczego kroku i modyfikowania pamięci / rejestrów innego procesu.)

Instrumentu openi openatukład połączeń: kiedy każdy proces uruchomiony w ramach tego narzędzia sprawia wywołania systemowego (lub ) z , uśpienia przez jakieś 1/2 lub 1 sekundę. Pozwól innym wywołaniom systemowym (zwłaszcza tym ) wykonać się bezzwłocznie.open(2)openatO_RDONLYopenO_TRUNC

Powinno to pozwolić pisarzowi wygrać wyścig w prawie każdym stanie wyścigu, chyba że obciążenie systemu było również wysokie lub był to skomplikowany stan wyścigu, w którym obcinanie nastąpiło dopiero po kolejnym przeczytaniu. Tak więc losowa odmiana, które są opóźnione open()(a może read()s lub zapisuje) , zwiększyłaby moc wykrywania tego narzędzia, ale oczywiście bez testowania przez nieskończony czas za pomocą symulatora opóźnienia, który ostatecznie obejmie wszystkie możliwe sytuacje, w których można się spotkać w prawdziwym świecie, nie możesz być pewien, że twoje skrypty są wolne od ras, chyba że przeczytasz je uważnie i udowodnisz, że nie są.


Prawdopodobnie byłbyś potrzebny do dodania do białej listy (nie opóźniania open) plików, /usr/bina /usr/libwięc proces uruchamiania nie trwa wiecznie. (Dynamiczne dowiązanie środowiska wykonawczego musi obejmować open()wiele plików (spójrz na niego strace -eopen /bin/truelub /bin/lskiedyś), chociaż jeśli sama powłoka nadrzędna wykonuje obcinanie, to będzie w porządku. Ale to nadal dobrze, aby to narzędzie nie powodowało nadmiernego spowolnienia skryptów).

A może najpierw umieść na białej liście każdy plik, do którego proces wywołujący nie ma uprawnień do obcięcia. tzn. proces śledzenia może wykonać access(2)wywołanie systemowe przed faktycznym zawieszeniem procesu, który chciał open()utworzyć plik.


racecheckersam musiałby być napisany w języku C, a nie w powłoce, ale być może mógłby użyć stracekodu jako punktu wyjścia i może nie zająć dużo pracy.

Możesz mieć tę samą funkcjonalność z systemem plików FUSE . Prawdopodobnie istnieje BEZPIECZNY przykład czystego systemu plików typu pass-through, więc możesz dodać kontrole do open()funkcji w tym, co sprawia, że ​​jest ona uśpiona dla otwierania tylko do odczytu, ale pozwala od razu obciąć.

Peter Cordes
źródło
Twój pomysł na kontrolera wyścigu tak naprawdę nie działa. Po pierwsze, istnieje problem polegający na tym, że przekroczenia limitu czasu nie są niezawodne: pewnego dnia drugi facet zajmie więcej czasu, niż się spodziewasz (jest to klasyczny problem ze skryptami kompilacji lub testowania, które wydają się działać przez jakiś czas, a następnie zawodzą w trudny do debugowania sposób kiedy obciążenie się zwiększa i wiele rzeczy działa równolegle). Ale poza tym, do którego otwarcia zamierzasz dodać opóźnienie? Aby wykryć coś interesującego, musisz wykonać wiele przebiegów z różnymi wzorcami opóźnień i porównać ich wyniki.
Gilles 'SO - przestań być zły'
@Gilles: Racja, żadne krótkie opóźnienie nie gwarantuje, że ścięcie wygra wyścig (na mocno obciążonej maszynie, jak wskazałeś ). Chodzi o to, że używasz tego do testowania skryptu kilka razy, a nie przez racecheckercały czas. Prawdopodobnie chciałbyś, aby czas uśpienia typu otwartego do odczytu był konfigurowalny z korzyścią dla osób na bardzo obciążonych komputerach, które chcą ustawić go wyżej, na przykład 10 sekund. Lub ustaw niższą wartość, na przykład 0,1 sekundy dla długich lub nieefektywnych skryptów, które często otwierają pliki .
Peter Cordes,
@Gilles: Świetny pomysł na różne wzorce opóźnień, które mogą pozwolić ci złapać więcej ras niż tylko proste rzeczy w tym samym potoku, które „powinny być oczywiste (kiedy już wiesz, jak działają powłoki)” jak przypadek PO. Ale „który otwiera?” każdy otwarty tylko do odczytu, z białą listą lub w inny sposób, aby nie opóźniać uruchamiania procesu.
Peter Cordes,
Myślę, że myślisz o bardziej złożonych rasach z zadaniami w tle, które nie są obcinane, dopóki nie zakończy się jakiś inny proces? Tak, aby to uchwycić, może być wymagana losowa zmiana. A może spójrz na drzewo procesów i opóźnij „wczesne” czytanie więcej, aby spróbować odwrócić zwykłe porządkowanie. Możesz sprawić, że narzędzie będzie coraz bardziej skomplikowane, aby symulować coraz więcej możliwości zmiany kolejności, ale w pewnym momencie nadal musisz poprawnie projektować swoje programy, jeśli wykonujesz wielozadaniowość. Automatyczne testowanie może być przydatne w prostszych skryptach, w których możliwe problemy są bardziej ograniczone.
Peter Cordes,
Jest to bardzo podobne do testowania wielowątkowego kodu, szczególnie algorytmów bez blokady: logiczne uzasadnienie, dlaczego jest poprawny, jest bardzo ważne, a także testowanie, ponieważ nie można liczyć na testowanie na żadnym konkretnym zestawie maszyn w celu uzyskania wszystkich ponownych zamówień, które mogą bądź problemem, jeśli nie zamknąłeś wszystkich luk. Ale podobnie jak testowanie na słabo uporządkowanej architekturze, takiej jak ARM lub PowerPC, jest dobrym pomysłem w praktyce, testowanie skryptu w systemie, który sztucznie opóźnia rzeczy, może ujawnić niektóre rasy, więc jest lepszy niż nic. Zawsze możesz wprowadzić błędy, których nie złapie!
Peter Cordes,
18

Dlaczego występuje warunek wyścigu?

Dwie strony rury są wykonywane równolegle, a nie jedna po drugiej. Jest to bardzo prosty sposób, aby to wykazać: uruchomić

time sleep 1 | sleep 1

To zajmuje jedną sekundę, a nie dwie.

Powłoka uruchamia dwa procesy potomne i czeka na zakończenie ich obu. Te dwa procesy wykonać równolegle: jedynym powodem, dlaczego jeden z nich będzie synchronizować z drugiej jest, gdy trzeba czekać na drugą. Najczęstszym punktem synchronizacji jest sytuacja, gdy prawa strona blokuje oczekiwanie na odczyt danych na standardowym wejściu i zostaje odblokowana, gdy lewa strona zapisuje więcej danych. Odwrotna sytuacja może się również zdarzyć, gdy prawa strona wolno odczytuje dane, a lewa strona blokuje się w operacji zapisu, dopóki prawa strona nie odczyta większej ilości danych (w samym potoku znajduje się bufor zarządzany przez jądro, ale ma mały maksymalny rozmiar).

Aby zaobserwować punkt synchronizacji, należy przestrzegać następujących poleceń ( sh -xwypisuje każde polecenie podczas jego wykonywania):

time sh -x -c '{ sleep 1; echo a; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { sleep 1; cat; }'
time sh -x -c '{ sleep 2; echo a; } | { cat; sleep 1; }'

Graj odmianami, aż poczujesz się komfortowo z tym, co obserwujesz.

Biorąc pod uwagę złożone polecenie

cat tmp | head -1 > tmp

proces po lewej stronie wykonuje następujące czynności (wymieniłem tylko kroki, które są istotne dla mojego wyjaśnienia):

  1. Uruchom program zewnętrzny catz argumentem tmp.
  2. Otwarty tmpdo czytania.
  3. Chociaż nie osiągnął końca pliku, przeczytaj fragment pliku i zapisz go na standardowe wyjście.

Proces po prawej stronie wykonuje następujące czynności:

  1. Przekieruj standardowe wyjście do tmp, obcięcie pliku w tym procesie.
  2. Uruchom program zewnętrzny headz argumentem -1.
  3. Odczytaj jeden wiersz ze standardowego wejścia i zapisz go na standardowe wyjście.

Jedynym punktem synchronizacji jest to, że prawy-3 czeka, aż lewy-3 przetworzy jedną pełną linię. Nie ma synchronizacji między lewym-2 a prawym-1, więc mogą się zdarzyć w dowolnej kolejności. Kolejność, w jakiej występują, nie jest przewidywalna: zależy to od architektury procesora, powłoki, jądra, od których rdzeni procesy zostaną zaplanowane, od tego, co zakłóca procesor w tym czasie itp.

Jak zmienić zachowanie

Nie można zmienić zachowania, zmieniając ustawienie systemowe. Komputer robi to, co mu każesz. Kazałeś skrócić tmpi czytać tmprównolegle, więc robi to dwie rzeczy równolegle.

Ok, jest jedno „ustawienie systemowe”, które możesz zmienić: możesz zastąpić /bin/bashgo innym programem, który nie jest bash. Mam nadzieję, że zrozumiałoby to, że nie jest to dobry pomysł.

Jeśli chcesz, aby obcięcie miało miejsce przed lewą stroną rury, musisz umieścić je poza rurociągiem, na przykład:

{ cat tmp | head -1; } >tmp

lub

( exec >tmp; cat tmp | head -1 )

Nie mam pojęcia, dlaczego tego chcesz. Po co czytać z pliku, o którym wiesz, że jest pusty?

I odwrotnie, jeśli chcesz, aby przekierowanie danych wyjściowych (w tym obcinanie) miało miejsce po catzakończeniu odczytu, musisz albo całkowicie buforować dane w pamięci, np.

line=$(cat tmp | head -1)
printf %s "$line" >tmp

lub napisz do innego pliku, a następnie przenieś go na miejsce. Jest to zwykle solidny sposób wykonywania skryptów i ma tę zaletę, że plik jest zapisywany w całości, zanim będzie widoczny przez oryginalną nazwę.

cat tmp | head -1 >new && mv new tmp

Moreutils kolekcja zawiera program, który nie tylko, że nazywa sponge.

cat tmp | head -1 | sponge tmp

Jak automatycznie wykryć problem

Jeśli Twoim celem było wzięcie źle napisanych skryptów i automatyczne ustalenie, gdzie się psują, przepraszam, życie nie jest takie proste. Analiza środowiska wykonawczego nie znajdzie problemu w sposób wiarygodny, ponieważ czasami catkończy się odczyt, zanim nastąpi obcięcie. Analiza statyczna może w zasadzie to zrobić; uproszczony przykład twojego pytania został złapany przez Shellcheck , ale może nie wychwycić podobnego problemu w bardziej złożonym skrypcie.

Gilles „SO- przestań być zły”
źródło
To był mój cel, aby ustalić, czy skrypt jest dobrze napisany, czy nie. Jeśli skrypt mógł zniszczyć dane w ten sposób, po prostu chciałem, aby zniszczył je za każdym razem. Nie jest dobrze słyszeć, że jest to prawie niemożliwe. Dzięki tobie wiem już na czym polega problem i spróbuję znaleźć rozwiązanie.
karlosss,
@karlosss: Hmm, zastanawiam się, czy mógłbyś użyć tych samych funkcji śledzenia / przechwytywania wywołań systemowych jak strace(np. Linux ptrace), aby wszystkie openwywołania systemowe do odczytu (we wszystkich procesach potomnych) spały przez pół sekundy, więc podczas wyścigu z obcięcie, obcięcie prawie zawsze wygrywa.
Peter Cordes,
@PeterCordes Jestem nowicjuszem w tym temacie, jeśli potrafisz zarządzać sposobem na osiągnięcie tego i napisać go jako odpowiedź, zaakceptuję.
karlosss,
@PeterCordes Nie możesz zagwarantować, że obcięcie wygra z opóźnieniem. Będzie działał przez większość czasu, ale czasami na mocno obciążonej maszynie twój skrypt zawiedzie w mniej lub bardziej tajemniczy sposób.
Gilles „SO- przestań być zły”
@Gilles: Porozmawiajmy o tym pod moją odpowiedzią.
Peter Cordes,