Come assicurarsi che venga eseguita solo un'istanza di uno script bash?


26

Una soluzione che non richiede strumenti aggiuntivi sarebbe preferita.


Che dire di un file di blocco?
Marco,

@Marco Ho trovato questa risposta SO usando quella, ma come affermato in un commento , questo può creare una condizione di gara
Tobias Kienzler,

3
Questo è BashFAQ 45 .
jw013,

@ jw013 grazie! Quindi forse qualcosa del genere ln -s my.pid .lockrivendicherà il blocco (seguito da echo $$ > my.pid) e in caso di errore può verificare se il PID archiviato .lockè davvero un'istanza attiva dello script
Tobias Kienzler,

Risposte:


18

Quasi come la risposta di nsg: usa una directory di blocco . La creazione di directory è atomica su Linux, Unix e * BSD e molti altri sistemi operativi.

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

È possibile inserire il PID del blocco sh in un file nella directory di blocco per scopi di debug, ma non cadere nella trappola di pensare che è possibile controllare quel PID per vedere se il processo di blocco viene ancora eseguito. Molte condizioni di gara si trovano su quel percorso.


1
Prenderei in considerazione l'utilizzo del PID memorizzato per verificare se l'istanza di blocco è ancora attiva. Tuttavia, ecco un'affermazione che mkdirnon è atomica su NFS (il che non è il mio caso, ma immagino che si debba dire che, se fosse vero)
Tobias Kienzler,

Sì, utilizzare il PID memorizzato per vedere se il processo di blocco viene ancora eseguito, ma non tentare di fare altro che registrare un messaggio. Il lavoro di controllo del pid memorizzato, creazione di un nuovo file PID, ecc., Lascia una grande finestra per le gare.
Bruce Ediger,

Ok, come ha affermato Ihunath, molto probabilmente sarebbe il lockdir in /tmpcui di solito non è condiviso NFS, quindi dovrebbe andare bene.
Tobias Kienzler,

Vorrei utilizzare rm -rfper rimuovere la directory di blocco. rmdirfallirà se qualcuno (non necessariamente tu) è riuscito ad aggiungere un file alla directory.
Chepner,

18

Per aggiungere alla risposta di Bruce Ediger e ispirato a questa risposta , dovresti anche aggiungere più intelligenza alla pulizia per evitare la fine dello script:

#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

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

6

Questo potrebbe essere troppo semplicistico, per favore correggimi se sbaglio. Non è psabbastanza semplice ?

#!/bin/bash 

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

# do stuff below this comment

1
Bello e / o geniale. :)
Spooky

1
Ho usato questa condizione per una settimana e in 2 occasioni non ha impedito l'avvio di un nuovo processo. Ho capito qual è il problema: new pid è una sottostringa di quella vecchia e viene nascosta da grep -v $$. esempi reali: vecchio - 14532, nuovo - 1453, vecchio - 28858, nuovo - 858.
Naktibalda,

L'ho corretto cambiando grep -v $$ingrep -v "^${$} "
Naktibalda il

@Naktibalda buona cattura, grazie! Puoi anche risolverlo con grep -wv "^$$"(vedi modifica).
terdon

Grazie per l'aggiornamento. Il mio schema a volte falliva perché i pidi più corti venivano lasciati imbottiti di spazi.
Naktibalda

4

Vorrei usare un file di blocco, come menzionato da 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
(Penso che ti sia dimenticato di creare il file di blocco) E le condizioni di gara ?
Tobias Kienzler,

op :) Sì, le condizioni di gara sono un problema nel mio esempio, di solito scrivo cron job orarie o giornaliere e le condizioni di gara sono rare.
nsg

Non dovrebbero essere rilevanti neppure nel mio caso, ma è qualcosa che bisogna tenere a mente. Forse usare lsof $0non è male, neanche?
Tobias Kienzler,

Puoi ridurre le condizioni di gara scrivendo il tuo $$nel file di blocco. Quindi sleepper un breve intervallo e rileggilo. Se il PID è ancora tuo, hai acquisito correttamente il blocco. Non necessita assolutamente di strumenti aggiuntivi.
arte

1
Non ho mai usato lsof per questo scopo, questo dovrebbe funzionare. Nota che lsof è molto lento nel mio sistema (1-2 sec) e molto probabilmente c'è molto tempo per le condizioni di gara.
ng

3

Se vuoi assicurarti che sia in esecuzione solo un'istanza del tuo script, dai un'occhiata a:

Blocca il tuo script (contro la corsa parallela)

Altrimenti puoi controllare pso invocare lsof <full-path-of-your-script>, dal momento che non li chiamerei strumenti aggiuntivi.


Supplemento :

in realtà ho pensato di farlo in questo modo:

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

questo garantisce che solo il processo con il minimo pidcontinui a funzionare anche se si eseguono fork-and-exec diverse istanze <your_script>contemporaneamente.


1
Grazie per il link, ma potresti includere le parti essenziali nella tua risposta? È politica comune a SE prevenire il marciume dei link ... Ma qualcosa del genere [[(lsof $0 | wc -l) > 2]] && exitpotrebbe effettivamente essere sufficiente, o è anche incline alle condizioni di gara?
Tobias Kienzler,

Hai ragione, la parte essenziale della mia risposta mancava e solo pubblicare link è piuttosto zoppo. Ho aggiunto il mio suggerimento alla risposta.
user1146332

3

Un altro modo per assicurarsi che venga eseguita una singola istanza di script 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 ottiene il PID dello script esistente se è già in esecuzione o esce con il codice di errore 1 se nessun altro script è in esecuzione


3

Sebbene tu abbia chiesto una soluzione senza strumenti aggiuntivi, questo è il mio modo preferito usando flock:

#!/bin/sh

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

echo "servus!"
sleep 10

Questo deriva dalla sezione degli esempi di man flock, che spiega ulteriormente:

Questo è utile codice boilerplate per gli script di shell. Inseriscilo nella parte superiore dello script della shell che vuoi bloccare e si bloccherà automaticamente alla prima esecuzione. Se env var $ FLOCKER non è impostato sullo script della shell in esecuzione, eseguire flock e acquisire un blocco esclusivo non bloccante (utilizzando lo script stesso come file di blocco) prima di rieseguire se stesso con gli argomenti corretti. Inoltre imposta FLOCKER env var sul valore corretto in modo che non venga eseguito di nuovo.

Punti da considerare:

  • Richiede flock, lo script di esempio termina con un errore se non può essere trovato
  • Non è necessario alcun file di blocco aggiuntivo
  • Potrebbe non funzionare se lo script è su NFS (consultare /server/66919/file-locks-on-an-nfs )

Vedi anche /programming/185451/quick-and-dirty-way-to-ensure-only-one-instance-of-a-shell-script-is-running-at .


1

Questa è una versione modificata della risposta di Anselmo . L'idea è di creare un descrittore di file di sola lettura utilizzando lo script bash stesso e utilizzarlo flockper gestire il blocco.

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"

La principale differenza rispetto a tutte le altre risposte è che questo codice non modifica il filesystem, utilizza un footprint molto basso e non necessita di alcuna pulizia poiché il descrittore di file viene chiuso non appena lo script termina indipendentemente dallo stato di uscita. Quindi non importa se lo script fallisce o ha esito positivo.


Dovresti sempre citare tutti i riferimenti alle variabili della shell a meno che tu non abbia una buona ragione per non farlo e sei sicuro di sapere cosa stai facendo. Quindi dovresti fare exec 6< "$SCRIPT".
Scott,

@Scott Ho modificato il codice in base ai tuoi suggerimenti. Grazie molto.
John Doe,

1

Sto usando cksum per verificare che il mio script stia davvero eseguendo una singola istanza, anche se cambio il nome del file e il percorso del file .

Non sto usando il file trap & lock, perché se il mio server improvvisamente si spegne, devo rimuovere manualmente il file di blocco dopo che il server è andato su.

Nota: #! / Bin / bash in prima riga è richiesto per grep ps

#!/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

Oppure puoi cksum hardcoded all'interno del tuo script, quindi non ti preoccupare di nuovo se vuoi cambiare nome file, percorso o contenuto del tuo script .

#!/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
Spiega esattamente come codificare il checksum è una buona idea.
Scott,

non un checksum hardcoding, crea solo la chiave di identità dello script, quando verrà eseguita un'altra istanza, controllerà altri processi di script di shell e eseguirà prima la cat del file, se la chiave di identità si trova su quel file, quindi significa che l'istanza è già in esecuzione.
arputra,

OK; per favore modifica la tua risposta per spiegarlo. E, in futuro, ti preghiamo di non pubblicare più blocchi di codice lunghi 30 righe che sembrano (quasi) identici senza dire e spiegare come sono diversi. E non dire cose come "puoi scrivere [sic] cksum all'interno del tuo script", e non continuare a usare nomi di variabili mysume fsum, quando non parli più di un checksum.
Scott,

Sembra interessante, grazie! E benvenuto su unix.stackexchange :)
Tobias Kienzler,


0

Il mio codice per te

#!/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}

Basato su man flock, migliorando solo:

  • il nome del file di blocco, in base al nome completo dello script
  • il messaggio executing

Dove ho messo qui il sleep 10, puoi mettere tutto lo script principale.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.