Jak upewnić się, że działa tylko jedna instancja skryptu bash?


26

Preferowane byłoby rozwiązanie niewymagające dodatkowych narzędzi.


Co z plikiem blokady?
Marco

@Marco Znalazłem tę SO odpowiedź używając tego, ale jak stwierdzono w komentarzu , może to stworzyć warunki do wyścigu
Tobias Kienzler

3
To jest BashFAQ 45 .
jw013,

@ jw013 dzięki! Więc może coś takiego ln -s my.pid .lockprzejmie blokadę (a następnie echo $$ > my.pid), a po awarii może sprawdzić, czy przechowywany PID .lockjest naprawdę aktywną instancją skryptu
Tobias Kienzler

Odpowiedzi:


18

Prawie jak odpowiedź nsg: użyj katalogu blokady . Tworzenie katalogów jest atomowe pod Linuksem, Unixem i * BSD oraz wieloma innymi systemami operacyjnymi.

if mkdir $LOCKDIR
then
    # Do important, exclusive stuff
    if rmdir $LOCKDIR
    then
        echo "Victory is mine"
    else
        echo "Could not remove lock dir" >&2
    fi
else
    # Handle error condition
    ...
fi

Możesz umieścić PID blokady sh w pliku w katalogu blokady w celu debugowania, ale nie wpadnij w pułapkę myślenia, że ​​możesz sprawdzić ten PID, aby sprawdzić, czy proces blokowania nadal się wykonuje. Na ścieżce leży wiele warunków wyścigowych.


1
Rozważę użycie zapisanego PID, aby sprawdzić, czy instancja blokująca nadal żyje. Jednakże, tutaj jest twierdzenie, które mkdirnie jest atomowe na NFS (co nie jest w moim przypadku, ale myślę, że należy wspomnieć, że jeśli to prawda)
Tobias Kienzler

Tak, z całą pewnością używaj zapisanego PID, aby sprawdzić, czy proces blokowania jest nadal wykonywany, ale nie próbuj robić nic innego niż rejestrowanie wiadomości. Praca polegająca na sprawdzeniu zapisanego pid, utworzeniu nowego pliku PID itp. Pozostawia duże okno dla wyścigów.
Bruce Ediger,

Ok, jak stwierdził Ihunath , najprawdopodobniej byłby nim lockdir, w /tmpktórym zwykle nie jest udostępniany NFS, więc powinno być dobrze.
Tobias Kienzler,

Chciałbym użyć rm -rfdo usunięcia katalogu blokady. rmdirzawiedzie, jeśli ktoś (niekoniecznie ty) zdoła dodać plik do katalogu.
chepner

18

Aby dodać do odpowiedzi Bruce'a Edigera i zainspirować się tą odpowiedzią , powinieneś również dodać więcej sprytów do czyszczenia, aby uchronić się przed zakończeniem skryptu:

#Remove the lock directory
function cleanup {
    if rmdir $LOCKDIR; then
        echo "Finished"
    else
        echo "Failed to remove lock directory '$LOCKDIR'"
        exit 1
    fi
}

if mkdir $LOCKDIR; then
    #Ensure that if we "grabbed a lock", we release it
    #Works for SIGTERM and SIGINT(Ctrl-C)
    trap "cleanup" EXIT

    echo "Acquired lock, running"

    # Processing starts here
else
    echo "Could not create lock directory '$LOCKDIR'"
    exit 1
fi

Alternatywnie if ! mkdir "$LOCKDIR"; then handle failure to lock and exit; fi trap and do processing after if-statement.
Kusalananda

6

To może być zbyt uproszczone, popraw mnie, jeśli się mylę. Czy to nie jest proste ps?

#!/bin/bash 

me="$(basename "$0")";
running=$(ps h -C "$me" | grep -wv $$ | wc -l);
[[ $running > 1 ]] && exit;

# do stuff below this comment

1
Ładne i / lub genialne. :)
Spooky

1
Korzystałem z tego warunku przez tydzień i 2 razy nie przeszkadzało to w rozpoczęciu nowego procesu. Zrozumiałem, na czym polega problem - nowy pid jest podciągiem starego i zostaje ukryty grep -v $$. prawdziwe przykłady: stary - 14532, nowy - 1453, stary - 28858, nowy - 858.
Naktibalda

Naprawiłem to, zmieniając grep -v $$nagrep -v "^${$} "
Naktibalda

@Naktibalda dobry połów, dzięki! Możesz to również naprawić za pomocą grep -wv "^$$"(patrz edycja).
terdon

Dzięki za tę aktualizację. Mój wzór czasami nie udawał się, ponieważ krótsze piddy były wypełnione spacjami.
Naktibalda

4

Użyłbym pliku blokady, jak wspomniał Marco

#!/bin/bash

# Exit if /tmp/lock.file exists
[ -f /tmp/lock.file ] && exit

# Create lock file, sleep 1 sec and verify lock
echo $$ > /tmp/lock.file
sleep 1
[ "x$(cat /tmp/lock.file)" == "x"$$ ] || exit

# Do stuff
sleep 60

# Remove lock file
rm /tmp/lock.file

1
(Myślę, że zapomniałeś utworzyć plik blokady) Co z warunkami wyścigu ?
Tobias Kienzler,

ops :) Tak, warunki wyścigu są problemem w moim przykładzie, zwykle piszę godzinowe lub codzienne zadania crona, a warunki wyścigu są rzadkie.
nsg

Nie powinny one również mieć znaczenia w moim przypadku, ale należy o tym pamiętać. Może używanie też lsof $0nie jest złe?
Tobias Kienzler,

Możesz zmniejszyć stan wyścigu, pisząc $$w pliku blokady. Następnie sleepprzez krótki czas i przeczytaj ponownie. Jeśli PID jest nadal twój, pomyślnie udało ci się zdobyć blokadę. Nie wymaga absolutnie żadnych dodatkowych narzędzi.
manatwork

1
Nigdy nie używałem lsof do tego celu, to powinno działać. Zauważ, że lsof jest naprawdę wolny w moim systemie (1-2 sekundy) i najprawdopodobniej jest dużo czasu na warunki wyścigowe.
nsg

3

Jeśli chcesz się upewnić, że działa tylko jedna instancja skryptu, spójrz na:

Zablokuj skrypt (przed uruchomieniem równoległym)

W przeciwnym razie możesz sprawdzić pslub wywołać lsof <full-path-of-your-script>, ponieważ nie nazwałbym ich dodatkowymi narzędziami.


Suplement :

właściwie pomyślałem o zrobieniu tego w ten sposób:

for LINE in `lsof -c <your_script> -F p`; do 
    if [ $$ -gt ${LINE#?} ] ; then
        echo "'$0' is already running" 1>&2
        exit 1;
    fi
done

zapewnia to, że działa tylko proces o najniższym poziomie, pidnawet jeśli wykonujesz kilka wystąpień <your_script>jednocześnie.


1
Dziękuję za link, ale czy mógłbyś podać istotne części w swojej odpowiedzi? Powszechną polityką w SE jest zapobieganie gniciu linków ... Ale coś takiego [[(lsof $0 | wc -l) > 2]] && exitmoże być wystarczające, czy też jest to podatne na warunki wyścigowe?
Tobias Kienzler,

Masz rację, zasadniczej części mojej odpowiedzi brakowało, a jedynie zamieszczanie linków jest dość kiepskie. Dodałem własną sugestię do odpowiedzi.
user1146332,

3

Jeszcze jeden sposób, aby upewnić się, że uruchomione jest jedno wystąpienie skryptu bash:

#!/bin/bash

# Check if another instance of script is running
pidof -o %PPID -x $0 >/dev/null && echo "ERROR: Script $0 already running" && exit 1

...

pidof -o %PPID -x $0 pobiera PID istniejącego skryptu, jeśli jest już uruchomiony, lub kończy działanie z kodem błędu 1, jeśli żaden inny skrypt nie jest uruchomiony


3

Chociaż poprosiłeś o rozwiązanie bez dodatkowych narzędzi, to mój ulubiony sposób, używając flock:

#!/bin/sh

[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :

echo "servus!"
sleep 10

Pochodzi z sekcji przykładów man flock, która dalej wyjaśnia:

Jest to przydatny kod podstawowy dla skryptów powłoki. Umieść go na górze skryptu powłoki, który chcesz zablokować, a automatycznie zablokuje się przy pierwszym uruchomieniu. Jeśli env var $ FLOCKER nie jest ustawiony na uruchamiany skrypt powłoki, uruchom flock i weź ekskluzywną nieblokującą blokadę (używając samego skryptu jako pliku blokady) przed ponownym uruchomieniem się z odpowiednimi argumentami. Ustawia również zmienną FLOCKER env var na odpowiednią wartość, aby nie działała ponownie.

Należy wziąć pod uwagę:

  • Wymaga flock , przykładowy skrypt kończy się błędem, jeśli nie można go znaleźć
  • Nie wymaga dodatkowego pliku blokady
  • Może nie działać, jeśli skrypt działa w systemie plików NFS (patrz /server/66919/file-locks-on-an-nfs )

Zobacz także /programming/185451/quick-and-dirty-way-to-ensure-only-one-instance-of-a-shell-script-is-running-at .


1

To jest zmodyfikowana wersja odpowiedzi Anselmo . Chodzi o to, aby utworzyć deskryptor pliku tylko do odczytu przy użyciu samego skryptu bash i użyć flockdo obsługi blokady.

SCRIPT=`realpath $0`     # get absolute path to the script itself
exec 6< "$SCRIPT"        # open bash script using file descriptor 6
flock -n 6 || { echo "ERROR: script is already running" && exit 1; }   # lock file descriptor 6 OR show error message if script is already running

echo "Run your single instance code here"

Główną różnicą w stosunku do wszystkich pozostałych odpowiedzi jest to, że ten kod nie modyfikuje systemu plików, używa bardzo małej powierzchni i nie wymaga czyszczenia, ponieważ deskryptor pliku jest zamykany, gdy tylko skrypt zakończy działanie, niezależnie od stanu wyjścia. Dlatego nie ma znaczenia, czy skrypt zawiedzie, czy się powiedzie.


Powinieneś zawsze cytować wszystkie odwołania do zmiennych powłoki, chyba że masz dobry powód, aby tego nie robić i jesteś pewien, że wiesz, co robisz. Więc powinieneś to robić exec 6< "$SCRIPT".
Scott

@Scott Zmieniłem kod zgodnie z Twoimi sugestiami. Wielkie dzięki.
John Doe,

1

Używam cksum, aby sprawdzić, czy mój skrypt naprawdę działa w pojedynczej instancji, nawet jeśli zmieniam nazwę pliku i ścieżkę pliku .

Nie używam pliku pułapki i blokady, ponieważ jeśli mój serwer nagle przestanie działać, muszę ręcznie usunąć plik blokady po uruchomieniu serwera.

Uwaga: Dla grep ps wymagany jest #! / Bin / bash w pierwszym wierszu

#!/bin/bash

checkinstance(){
   nprog=0
   mysum=$(cksum $0|awk '{print $1}')
   for i in `ps -ef |grep /bin/bash|awk '{print $2}'`;do 
        proc=$(ls -lha /proc/$i/exe 2> /dev/null|grep bash) 
        if [[ $? -eq 0 ]];then 
           cmd=$(strings /proc/$i/cmdline|grep -v bash)
                if [[ $? -eq 0 ]];then 
                   fsum=$(cksum /proc/$i/cwd/$cmd|awk '{print $1}')
                   if [[ $mysum -eq $fsum ]];then
                        nprog=$(($nprog+1))
                   fi
                fi
        fi
   done

   if [[ $nprog -gt 1 ]];then
        echo $0 is already running.
        exit
   fi
}

checkinstance 

#--- run your script bellow 

echo pass
while true;do sleep 1000;done

Lub możesz zakodować cksum wewnątrz skryptu, więc nie musisz się więcej martwić, jeśli chcesz zmienić nazwę pliku, ścieżkę lub treść skryptu .

#!/bin/bash

mysum=1174212411

checkinstance(){
   nprog=0
   for i in `ps -ef |grep /bin/bash|awk '{print $2}'`;do 
        proc=$(ls -lha /proc/$i/exe 2> /dev/null|grep bash) 
        if [[ $? -eq 0 ]];then 
           cmd=$(strings /proc/$i/cmdline|grep -v bash)
                if [[ $? -eq 0 ]];then 
                   fsum=$(grep mysum /proc/$i/cwd/$cmd|head -1|awk -F= '{print $2}')
                   if [[ $mysum -eq $fsum ]];then
                        nprog=$(($nprog+1))
                   fi
                fi
        fi
   done

   if [[ $nprog -gt 1 ]];then
        echo $0 is already running.
        exit
   fi
}

checkinstance

#--- run your script bellow

echo pass
while true;do sleep 1000;done

1
Proszę dokładnie wyjaśnić, w jaki sposób zakodowanie sumy kontrolnej jest dobrym pomysłem.
Scott

nie suma kontrolna na stałe, to jedyny klucz tożsamości do skryptu, gdy uruchomiona zostanie inna instancja, sprawdzi inny proces skryptu powłoki i najpierw przechwyci plik, jeśli klucz tożsamości znajduje się w tym pliku, więc oznacza to, że instancja już działa.
arputra

DOBRZE; proszę edytować swoje odpowiedzi wyjaśnić. W przyszłości nie publikuj wielu 30-liniowych bloków kodu, które wyglądają, jakby były (prawie) identyczne, bez mówienia i wyjaśniania, czym się różnią. I nie mów takich rzeczy, jak „możesz zakodować [sic] cksum w swoim skrypcie” i nie kontynuuj używania nazw zmiennych, mysuma fsumjeśli nie mówisz już o sumie kontrolnej.
Scott

Wygląda interesująco, dzięki! Witamy w unix.stackexchange :)
Tobias Kienzler


0

Mój kod do ciebie

#!/bin/bash

script_file="$(/bin/readlink -f $0)"
lock_file=${script_file////_}

function executing {
  echo "'${script_file}' already executing"
  exit 1
}

(
  flock -n 9 || executing

  sleep 10

) 9> /var/lock/${lock_file}

W oparciu o man flockulepszanie tylko:

  • nazwa pliku blokady, która ma być oparta na pełnej nazwie skryptu
  • wiadomość executing

Tam gdzie umieściłem tutaj sleep 10, możesz umieścić cały główny skrypt.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.