W jaki sposób ten skrypt zapewnia, że ​​działa tylko jedna instancja?

22

W dniu 19 sierpnia 2013 r. Randal L. Schwartz opublikował ten skrypt powłoki, który miał zapewnić w systemie Linux, że „działa tylko jedna instancja skryptu [bez] warunków wyścigu lub konieczności czyszczenia plików blokujących”:

#!/bin/sh
# randal_l_schwartz_001.sh
(
    if ! flock -n -x 0
    then
        echo "$$ cannot get flock"
        exit 0
    fi
    echo "$$ start"
    sleep 10 # for testing.  put the real task here
    echo "$$ end"
) < $0

Wygląda na to, że działa zgodnie z reklamą:

$ ./randal_l_schwartz_001.sh & ./randal_l_schwartz_001.sh
[1] 11863
11863 start
11864 cannot get flock
$ 11863 end

[1]+  Done                    ./randal_l_schwartz_001.sh
$

Oto, co rozumiem:

  • Skrypt przekierowuje ( <) kopię własnej zawartości (tj. Z $0) do STDIN (tj. Deskryptora pliku 0) podpowłoki.
  • W ramach podpowłoki skrypt próbuje uzyskać nieblokującą, wyłączną blokadę ( flock -n -x) deskryptora pliku 0.
    • Jeśli ta próba się nie powiedzie, podpowłoka kończy działanie (podobnie jak główny skrypt, ponieważ nie ma nic więcej do zrobienia).
    • Jeśli próba zakończy się powodzeniem, podpowłoka uruchamia żądane zadanie.

Oto moje pytania:

  • Dlaczego skrypt musi przekierowywać do deskryptora pliku odziedziczonego przez podpowłokę kopię własnej zawartości zamiast, powiedzmy, zawartości innego pliku? (Próbowałem przekierować z innego pliku i uruchomić ponownie jak wyżej, a kolejność wykonywania uległa zmianie: zadanie bez tła zyskało blokadę przed drugim. Więc może użycie własnej zawartości pliku pozwala uniknąć warunków wyścigu; ale jak?)
  • Dlaczego w każdym razie skrypt musi przekierowywać do deskryptora pliku odziedziczonego przez podpowłokę, kopię zawartości pliku?
  • Dlaczego trzymanie wyłącznej blokady deskryptora pliku 0w jednej powłoce uniemożliwia kopii tego samego skryptu działającego w innej powłoce uzyskanie wyłącznej blokady deskryptora pliku 0? Nie muszle mają swoje własne, oddzielne kopie standardowych deskryptorów ( 0, 1, i 2, tj stdin, stdout i stderr)?
sampablokuper
źródło
Jaki był twój dokładny proces testowy, kiedy próbowałeś przekierować eksperyment z innego pliku?
Freiheit
1
Myślę, że możesz odwołać się do tego linku. stackoverflow.com/questions/185451/…
Deb Paikar

Odpowiedzi:

22

Dlaczego skrypt musi przekierowywać do deskryptora pliku odziedziczonego przez podpowłokę kopię własnej zawartości zamiast, powiedzmy, zawartości innego pliku?

Możesz użyć dowolnego pliku, o ile wszystkie kopie skryptu używają tego samego. Użycie $0tylko przywiązuje blokadę do samego skryptu: jeśli skopiujesz skrypt i zmodyfikujesz go w innym celu, nie musisz wymyślać nowej nazwy pliku blokady. To jest wygodne.

Jeśli skrypt jest wywoływany przez dowiązanie symboliczne, blokada znajduje się w rzeczywistym pliku, a nie w dowiązaniu.

(Oczywiście, jeśli jakiś proces uruchamia skrypt i nadaje mu wymyśloną wartość jako argument zerowy zamiast rzeczywistej ścieżki, wtedy się psuje. Ale to rzadko się zdarza.)

(Próbowałem użyć innego pliku i ponownie uruchomić jak wyżej, a kolejność wykonywania uległa zmianie)

Czy na pewno było to spowodowane użytym plikiem, a nie tylko przypadkową odmianą? Podobnie jak w przypadku potoku, tak naprawdę nie ma pewności, w jakiej kolejności będą uruchamiane polecenia cmd1 & cmd. To zależy głównie od harmonogramu systemu operacyjnego. Dostaję losowe zmiany w moim systemie.

Dlaczego w każdym razie skrypt musi przekierowywać do deskryptora pliku odziedziczonego przez podpowłokę, kopię zawartości pliku?

Wygląda na to, że sama powłoka przechowuje kopię opisu pliku zawierającego blokadę, a nie tylko flocknarzędzie przechowujące. Utworzona blokada flock(2)jest zwalniana, gdy deskryptory plików, które ją posiadają, są zamknięte.

flockma dwa tryby, albo do zablokowania na podstawie nazwy pliku i uruchomienia zewnętrznego polecenia (w którym to przypadku flockzawiera wymagany otwarty deskryptor pliku) lub do pobrania deskryptora pliku z zewnątrz, więc proces zewnętrzny jest odpowiedzialny za przechowywanie to.

Pamiętaj, że zawartość pliku nie ma tutaj znaczenia i nie ma kopii. Przekierowanie do podpowłoki nie kopiuje żadnych danych wokół siebie, po prostu otwiera uchwyt do pliku.

Dlaczego trzymanie wyłącznej blokady deskryptora pliku 0 w jednej powłoce uniemożliwia kopii tego samego skryptu działającego w innej powłoce uzyskanie wyłącznej blokady deskryptora pliku 0? Czy powłoki nie mają własnych, osobnych kopii standardowych deskryptorów plików (0, 1 i 2, tj. STDIN, STDOUT i STDERR)?

Tak, ale blokada pliku , a nie deskryptor pliku. Tylko jedna otwarta instancja pliku może przytrzymywać blokadę jednocześnie.


Myślę, że powinieneś być w stanie zrobić to samo bez podpowłoki, execotwierając uchwyt pliku blokady:

$ cat lock.sh
#!/bin/sh

exec 9< "$0"

if ! flock -n -x 9; then
    echo "$$/$1 cannot get flock" 
    exit 0
fi

echo "$$/$1 got the lock"
sleep 2
echo "$$/$1 exit"

$ ./lock.sh bg & ./lock.sh fg ; wait; echo
[1] 11362
11363/fg got the lock
11362/bg cannot get flock
11363/fg exit
[1]+  Done                    ./lock.sh bg
ilkkachu
źródło
1
Używanie { }zamiast zamiast ( )również działałoby i unikało podpowłoki.
R ..
W dalszej części komentarzy do postu na G + ktoś tam też zaproponował mniej więcej tę samą metodę exec.
David Z
@R .., jasne. Ale nadal jest brzydka z dodatkowymi nawiasami klamrowymi wokół samego skryptu.
ilkkachu
9

Blokada pliku jest dołączona do pliku poprzez opis pliku . Na wysokim poziomie sekwencja operacji w jednym wystąpieniu skryptu to:

  1. Otwórz plik, do którego dołączona jest blokada („plik blokady”).
  2. Zablokuj plik blokady.
  3. Robić coś.
  4. Zamknij plik blokady. Zwalnia to blokadę dołączoną do opisu pliku utworzonego przez otwarcie pliku.

Trzymanie blokady uniemożliwia uruchomienie kolejnej kopii tego samego skryptu, ponieważ tak właśnie działają blokady. Tak długo, jak gdzieś w systemie istnieje wyłączna blokada pliku, niemożliwe jest utworzenie drugiej instancji tej samej blokady, nawet poprzez inny opis pliku.

Otwarcie pliku tworzy opis pliku . Jest to obiekt jądra, który nie ma dużej bezpośredniej widoczności w interfejsach programistycznych. Dostęp do opisu pliku uzyskuje się pośrednio za pomocą deskryptorów plików, ale zwykle uważa się go za dostęp do pliku (odczyt lub zapis jego zawartości lub metadanych). Blokada jest jednym z atrybutów, które są właściwością opisu pliku, a nie pliku lub deskryptora.

Na początku, gdy plik jest otwierany, opis pliku ma jeden deskryptor pliku, ale więcej deskryptorów można utworzyć, tworząc inny deskryptor ( duprodzinę wywołań systemowych) lub rozwidlając podproces (po czym zarówno rodzic, jak i dziecko ma dostęp do tego samego opisu pliku). Deskryptor pliku można zamknąć jawnie lub gdy proces, w którym się znajduje, umiera. Gdy ostatni deskryptor pliku dołączony do pliku zostanie zamknięty, opis pliku zostanie zamknięty.

Oto jak powyższa sekwencja operacji wpływa na opis pliku.

  1. Przekierowanie <$0otwiera plik skryptu w podpowłoce, tworząc opis pliku. W tym momencie do opisu dołączony jest jeden deskryptor pliku: deskryptor numer 0 w podpowłoce.
  2. Podkładka wywołuje flocki czeka na zakończenie. Podczas działania stada do opisu dołączone są dwa deskryptory: liczba 0 w podpowłoce i liczba 0 w procesie flokowania. Kiedy stado bierze blokadę, ustawia to właściwość opisu pliku. Jeśli inny opis pliku ma już blokadę pliku, stado nie może przyjąć blokady, ponieważ jest to blokada wyłączna.
  3. Podkładka robi rzeczy. Ponieważ nadal ma otwarty deskryptor pliku w opisie z blokadą, opis ten istnieje i zachowuje swoją blokadę, ponieważ nikt nigdy nie usuwa blokady.
  4. Podkładka umiera w nawiasie zamykającym. To zamyka ostatni deskryptor pliku w opisie pliku, który ma blokadę, więc blokada znika w tym momencie.

Powodem, dla którego skrypt używa przekierowania $0jest to, że przekierowanie jest jedynym sposobem na otwarcie pliku w powłoce, a utrzymanie przekierowania jest jedynym sposobem na utrzymanie deskryptora pliku otwartego. Podpowłoka nigdy nie czyta ze swojego standardowego wejścia, wystarczy ją otworzyć. W języku, który zapewnia bezpośredni dostęp do otwierania i zamykania połączenia, możesz użyć

fd = open($0)
flock(fd, LOCK_EX)
do stuff
close(fd)

Możesz faktycznie uzyskać tę samą sekwencję operacji w powłoce, jeśli przekierujesz za pomocą execwbudowanego.

exec <$0
flock -n -x 0
# do stuff
exec <&-

Skrypt mógłby użyć innego deskryptora pliku, jeśli chce nadal uzyskiwać dostęp do oryginalnego standardowego wejścia.

exec 3<$0
flock -n -x 0
# do stuff
exec 3<&-

lub z podpowłoką:

(
  flock -n -x 3
  # do stuff
) 3<$0

Blokada nie musi znajdować się w pliku skryptu. Może to być dowolny plik, który można otworzyć do odczytu (więc musi istnieć, musi to być typ pliku, który można odczytać, taki jak zwykły plik lub nazwany potok, ale nie katalog, a proces skryptu musi mieć pozwolenie na przeczytanie go). Zaletą pliku skryptu jest to, że jest obecny i czytelny (z wyjątkiem przypadku krawędzi, w którym został usunięty zewnętrznie między momentem wywołania skryptu a momentem, w którym skrypt dotrze do <$0przekierowania).

Tak długo, jak się flockpowiedzie, a skrypt znajduje się w systemie plików, w którym blokady nie są wadliwe (niektóre sieciowe systemy plików, takie jak NFS, mogą być wadliwe), nie widzę, jak użycie innego pliku blokady może pozwolić na wyścig. Podejrzewam błąd manipulacji z twojej strony.

Gilles „SO- przestań być zły”
źródło
Występuje warunek wyścigu: nie można kontrolować, która instancja skryptu otrzymuje blokadę. Na szczęście dla prawie wszystkich celów nie ma to znaczenia.
Mark
4
@ Mark Wyścig do śluzy, ale nie jest to warunek wyścigu. Warunkiem wyścigu jest moment, w którym czas może pozwolić na coś złego, na przykład dwa procesy znajdujące się w tej samej krytycznej sekcji w tym samym czasie. Nie wiedząc, który proces wejdzie w sekcję krytyczną, należy się spodziewać niedeterminizmu, nie jest to warunek wyścigu.
Gilles „SO- przestań być zły”
1
Po prostu FYI, link w „opisie pliku” wskazuje raczej na stronę indeksu specyfikacji Open Group, a nie na szczegółowy opis koncepcji, co moim zdaniem zamierzałeś zrobić. Możesz także połączyć swoją starszą odpowiedź tutaj, a także unix.stackexchange.com/a/195164/85039
Sergiy Kolodyazhnyy
5

Plik używany do blokowania jest nieistotny, skrypt używa, $0ponieważ jest to plik, o którym wiadomo, że istnieje.

Kolejność uzyskiwania blokad będzie mniej więcej losowa, w zależności od tego, jak szybko Twoja maszyna jest w stanie uruchomić dwa zadania.

Możesz użyć dowolnego deskryptora pliku, niekoniecznie 0. Blokada jest utrzymywana na pliku otwartym dla deskryptora pliku, a nie na samym deskryptorze.

( flock -x 9 || exit 1
  echo 'Locking for 5 secs'; sleep 5; echo 'Done' ) 9>/tmp/lock &
Kusalananda
źródło