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 -x
wypisuje 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):
- Uruchom program zewnętrzny
cat
z argumentem tmp
.
- Otwarty
tmp
do czytania.
- 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:
- Przekieruj standardowe wyjście do
tmp
, obcięcie pliku w tym procesie.
- Uruchom program zewnętrzny
head
z argumentem -1
.
- 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ć tmp
i czytać tmp
równolegle, więc robi to dwie rzeczy równolegle.
Ok, jest jedno „ustawienie systemowe”, które możesz zmienić: możesz zastąpić /bin/bash
go 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 cat
zakoń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 cat
koń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.