Blocco corretto negli script della shell?


66

A volte devi assicurarti che sia in esecuzione solo un'istanza di uno script di shell allo stesso tempo.

Ad esempio un cron job che viene eseguito tramite crond che non fornisce il blocco da solo (ad es. Il default Solaris crond).

Un modello comune per implementare il blocco è il codice come questo:

#!/bin/sh
LOCK=/var/tmp/mylock
if [ -f $LOCK ]; then            # 'test' -> race begin
  echo Job is already running\!
  exit 6
fi
touch $LOCK                      # 'set'  -> race end
# do some work
rm $LOCK

Naturalmente, tale codice ha una condizione di gara. C'è una finestra temporale in cui l'esecuzione di due istanze può avanzare dopo la riga 3 prima di poter toccare il $LOCKfile.

Per un lavoro cron questo di solito non è un problema perché hai un intervallo di minuti tra due invocazioni.

Ma le cose possono andare storte, ad esempio quando il file di blocco si trova su un server NFS, che si blocca. In tal caso, diversi lavori cron possono essere bloccati sulla riga 3 e messi in coda. Se il server NFS è di nuovo attivo, si avrà un fragoroso branco di lavori in esecuzione paralleli.

Cercando sul web ho trovato lo strumento lockrun che sembra una buona soluzione a quel problema. Con esso esegui uno script che deve essere bloccato in questo modo:

$ lockrun --lockfile=/var/tmp/mylock myscript.sh

Puoi metterlo in un wrapper o usarlo dal tuo crontab.

Utilizza lockf()(POSIX) se disponibile e torna a flock()(BSD). E il lockf()supporto su NFS dovrebbe essere relativamente diffuso.

Ci sono alternative a lockrun?

E gli altri demoni cron? Ci sono crond comuni che supportano il bloccaggio in modo sano? Una rapida occhiata alla pagina man di Vixie Crond (impostazione predefinita sui sistemi Debian / Ubuntu) non mostra nulla sul blocco.

Sarebbe una buona idea includere uno strumento come lockrunin coreutils ?

Secondo me implementa un tema molto simile a timeout, nicee agli amici.


4
Tangenzialmente, e per il beneficio di altri che potrebbero considerare il tuo modello iniziale abbastanza buono (tm), quel codice della shell dovrebbe eventualmente intercettare TERM per rimuovere il suo file di blocco quando killed; e sembra essere una buona pratica archiviare il proprio pid nel file di blocco, anziché semplicemente toccarlo.
Ulrich Schwarz,


@Shawn, non proprio, non menziona crond e NFS.
maxschlepzig,


1
@Ulrich molto tardivamente, la memorizzazione di un PID in un file di blocco NFS aggiunge un valore molto basso. Anche l'aggiunta del nome host non aiuta ancora a controllare un processo live
roaima

Risposte:


45

Ecco un altro modo per eseguire il blocco nello script della shell che può impedire la condizione di competizione descritta in precedenza, in cui due lavori possono entrambi passare alla riga 3. L' noclobberopzione funzionerà in ksh e bash. Non usare set noclobberperché non dovresti creare script in csh / tcsh. ;)

lockfile=/var/tmp/mylock

if ( set -o noclobber; echo "$$" > "$lockfile") 2> /dev/null; then

        trap 'rm -f "$lockfile"; exit $?' INT TERM EXIT

        # do stuff here

        # clean up after yourself, and release your trap
        rm -f "$lockfile"
        trap - INT TERM EXIT
else
        echo "Lock Exists: $lockfile owned by $(cat $lockfile)"
fi

YMMV con blocco su NFS (sai, quando i server NFS non sono raggiungibili), ma in generale è molto più robusto di prima. (10 anni fa)

Se hai lavori cron che fanno la stessa cosa contemporaneamente, da più server, ma hai solo bisogno di 1 istanza per essere effettivamente eseguito, qualcosa del genere potrebbe funzionare per te.

Non ho esperienza con lockrun, ma avere un ambiente di blocco preimpostato prima dello script effettivamente in esecuzione potrebbe aiutare. O potrebbe non farlo. Stai solo impostando il test per il file di lock esterno al tuo script in un wrapper e, in teoria, non potresti semplicemente raggiungere la stessa condizione di gara se due lavori fossero chiamati da lockrun esattamente nello stesso momento, proprio come con 'inside- la soluzione dello script?

Il blocco dei file rispetta comunque il comportamento del sistema e tutti gli script che non controllano l'esistenza del file di blocco prima dell'esecuzione faranno tutto ciò che faranno. Semplicemente inserendo il test del file di blocco e il comportamento corretto, risolverai il 99% dei potenziali problemi, se non il 100%.

Se ti imbatti molto nelle condizioni di gara del file di blocco, potrebbe essere un indicatore di un problema più grande, come non avere i tuoi lavori correttamente programmati, o forse se l'intervallo non è così importante come il completamento del lavoro, forse il tuo lavoro è più adatto per essere demone .


MODIFICA QUI SOTTO - 2016-05-06 (se stai usando KSH88)


Basati sul commento di @Clint Pachl qui sotto, se usi ksh88, usa mkdirinvece di noclobber. Ciò mitiga principalmente una potenziale condizione di razza, ma non la limita del tutto (sebbene il rischio sia minimo). Per maggiori informazioni leggi il link che Clint ha pubblicato di seguito .

lockdir=/var/tmp/mylock
pidfile=/var/tmp/mylock/pid

if ( mkdir ${lockdir} ) 2> /dev/null; then
        echo $$ > $pidfile
        trap 'rm -rf "$lockdir"; exit $?' INT TERM EXIT
        # do stuff here

        # clean up after yourself, and release your trap
        rm -rf "$lockdir"
        trap - INT TERM EXIT
else
        echo "Lock Exists: $lockdir owned by $(cat $pidfile)"
fi

E, come ulteriore vantaggio, se devi creare file tmp nel tuo script, puoi usare la lockdirdirectory per loro, sapendo che verranno ripuliti quando lo script esce.

Per bash più moderni, il metodo noclobber in alto dovrebbe essere adatto.


1
No, con lockrun non hai problemi - quando si blocca un server NFS, tutte le chiamate lockrun si bloccheranno (almeno) nella lockf()chiamata di sistema - quando viene eseguito il backup tutti i processi vengono ripresi ma solo un processo vincerà il blocco. Nessuna condizione di gara. Non mi imbatto molto in tali problemi con i cronjob - il contrario è il caso - ma questo è un problema quando ti colpisce, ha il potenziale per creare molto dolore.
maxschlepzig,

1
Ho accettato questa risposta perché il metodo è sicuro e finora il più elegante. Suggerisco una piccola variante: set -o noclobber && echo "$$" > "$lockfile"ottenere un fallback sicuro quando la shell non supporta l'opzione noclobber.
maxschlepzig,

3
Buona risposta, ma dovresti anche "uccidere -0" il valore nel file di blocco per assicurarti che il processo che ha creato il blocco esista ancora.
Nigel Horne,

1
L' noclobberopzione potrebbe essere soggetta alle condizioni di gara. Vedi mywiki.wooledge.org/BashFAQ/045 per alcuni spunti di riflessione.
Clint Pachl,

2
Nota: l'uso di noclobber(o -C) in ksh88 non funziona perché ksh88 non lo utilizza O_EXCLper noclobber. Se stai utilizzando una shell più recente potresti essere OK ...
jrw32982 supporta Monica il

14

Preferisco usare hard link.

lockfile=/var/lock/mylock
tmpfile=${lockfile}.$$
echo $$ > $tmpfile
if ln $tmpfile $lockfile 2>&-; then
    echo locked
else
    echo locked by $(<$lockfile)
    rm $tmpfile
    exit
fi
trap "rm ${tmpfile} ${lockfile}" 0 1 2 3 15
# do what you need to

I collegamenti fisici sono atomici su NFS e per lo più anche mkdir . Usando mkdir(2)o link(2)sono più o meno gli stessi, a livello pratico; Preferisco solo usare i collegamenti fisici perché più implementazioni di NFS hanno consentito collegamenti fisici atomici rispetto a quelli atomici mkdir. Con le versioni moderne di NFS, non dovresti preoccuparti di usare nessuno dei due.


12

Capisco che mkdirè atomico, quindi forse:

lockdir=/var/tmp/myapp
if mkdir $lockdir; then
  # this is a new instance, store the pid
  echo $$ > $lockdir/PID
else
  echo Job is already running, pid $(<$lockdir/PID) >&2
  exit 6
fi

# then set traps to cleanup upon script termination 
# ref http://www.shelldorado.com/goodcoding/tempfiles.html
trap 'rm -r "$lockdir" >/dev/null 2>&1' 0
trap "exit 2" 1 2 3 13 15

Ok, ma non sono riuscito a trovare informazioni se mkdir()su NFS (> = 3) è standardizzato per essere atomico.
maxschlepzig,

2
@maxschlepzig RFC 1813 non richiede esplicitamente mkdirdi essere atomico (lo fa per rename). In pratica, è noto che alcune implementazioni non lo sono. Correlati: un thread interessante, incluso un contributo dell'autore dell'arco GNU .
Gilles 'SO- smetti di essere malvagio' il

8

Un modo semplice è quello di utilizzare l' lockfilearrivo di solito con il procmailpacchetto.

LOCKFILE="/tmp/mylockfile.lock"
# try once to get the lock else exit
lockfile -r 0 "$LOCKFILE" || exit 0

# here the actual job

rm -f "$LOCKFILE"

5

semche fa parte degli parallelstrumenti GNU potrebbe essere quello che stai cercando:

sem [--fg] [--id <id>] [--semaphoretimeout <secs>] [-j <num>] [--wait] command

Come in:

sem --id my_semaphore --fg "echo 1 ; date ; sleep 3" &
sem --id my_semaphore --fg "echo 2 ; date ; sleep 3" &
sem --id my_semaphore --fg "echo 3 ; date ; sleep 3" &

Emissione:

1
Thu 10 Nov 00:26:21 UTC 2016
2
Thu 10 Nov 00:26:24 UTC 2016
3
Thu 10 Nov 00:26:28 UTC 2016

Si noti che l'ordine non è garantito. Inoltre, l'output non viene visualizzato fino al termine (irritante!). Ma anche così, è il modo più conciso che conosco per proteggermi dall'esecuzione simultanea, senza preoccuparmi di file di lock, tentativi e pulizia.


Il bloccaggio offerto dalla semmaniglia viene abbattuto a metà dell'esecuzione?
roaima,

2

Io uso dtach.

$ dtach -n /tmp/socket long_running_task ; echo $?
0
$ dtach -n /tmp/socket long_running_task ; echo $?
dtach: /tmp/socket: Address already in use
1

1

Uso lo strumento da riga di comando "flock" per gestire i blocchi nei miei script bash, come descritto qui e qui . Ho usato questo semplice metodo dalla manpage flock, per eseguire alcuni comandi in una subshell ...

   (
     flock -n 9
     # ... commands executed under lock ...
   ) 9>/var/lock/mylockfile

In questo esempio, non riesce con il codice di uscita 1 se non è in grado di acquisire il file di blocco. Ma il flock può anche essere usato in modi che non richiedono l'esecuzione di comandi in una sotto-shell :-)


3
La flock()chiamata di sistema non funziona su NFS .
maxschlepzig,

1
BSD ha uno strumento simile, "lockf".
dubiousjim,

2
@dubiousjim, BSD lockf chiama anche flock()ed è quindi problematico su NFS. Btw, nel frattempo, flock () su Linux ora ricade su fcntl()quando il file si trova su un mount NFS, quindi, in un ambiente NFS solo Linux flock()ora funziona su NFS.
maxschlepzig,

1

Non usare un file.

Se il tuo script viene eseguito in questo modo, ad esempio:

bash my_script

Puoi rilevare se è in esecuzione utilizzando:

running_proc=$(ps -C bash -o pid=,cmd= | grep my_script);
if [[ "$running_proc" != "$$ bash my_script" ]]; do 
  echo Already locked
  exit 6
fi

Hm, il codice di controllo ps viene eseguito dall'interno my_script? Nel caso in cui sia in esecuzione un'altra istanza, non running_proccontiene due righe corrispondenti? Mi piace l'idea, ma ovviamente - otterrai risultati falsi quando un altro utente esegue uno script con lo stesso nome ...
maxschlepzig

3
Include anche una condizione di competizione: se 2 istanze eseguono la prima riga in parallelo, nessuna ottiene il 'lock' ed entrambe escono con lo stato 6. Questa sarebbe una specie di fame reciproca rotonda . A proposito, non sono sicuro del motivo per cui usi $!invece che $$nel tuo esempio.
maxschlepzig,

@maxschlepzig infatti mi dispiace per i $ errati! vs. $$
frogstarr78,

@maxschlepzig per gestire più utenti che eseguono lo script aggiungi euser = all'argomento -o.
frogstarr78,

@maxschlepzig per prevenire più righe puoi anche modificare gli argomenti in grep o "filtri" aggiuntivi (ad es grep -v $$.). Fondamentalmente stavo cercando di fornire un approccio diverso al problema.
frogstarr78,

1

Per un utilizzo effettivo, è necessario utilizzare la risposta più votata .

Tuttavia, voglio discutere alcuni diversi approcci rotti e semi-lavorabili usando pse le molte avvertenze che hanno, dal momento che continuo a vedere le persone li usano.

Questa risposta è davvero la risposta a "Perché non usare pse grepgestire il blocco nella shell?"

Approccio rotto n. 1

In primo luogo, un approccio fornito in un'altra risposta che ha alcuni voti nonostante il fatto che non funzioni (e non potrebbe mai) funzionare e chiaramente non è mai stato testato:

running_proc=$(ps -C bash -o pid=,cmd= | grep my_script);
if [[ "$running_proc" != "$$ bash my_script" ]]; do 
  echo Already locked
  exit 6
fi

Ripariamo gli errori di sintassi e gli psargomenti rotti e otteniamo:

running_proc=$(ps -C bash -o pid,cmd | grep "$0");
echo "$running_proc"
if [[ "$running_proc" != "$$ bash $0" ]]; then
  echo Already locked
  exit 6
fi

Questo script uscirà sempre 6, ogni volta, indipendentemente da come lo si esegue.

Se lo esegui ./myscript, l' psoutput sarà 12345 -bash, il che non corrisponde alla stringa richiesta 12345 bash ./myscript, quindi fallirà.

Se lo esegui bash myscript, le cose diventano più interessanti. Il processo bash richiede l'esecuzione della pipeline e la shell figlio esegue pse grep. Sia la shell originale che la shell figlio verranno visualizzate psnell'output, in questo modo:

25793 bash myscript
25795 bash myscript

Non è l'output previsto $$ bash $0, quindi lo script verrà chiuso.

Approccio rotto n. 2

Ora, in tutta onestà per l'utente che ha scritto l'approccio n. 1, ho fatto qualcosa di simile da solo quando ho provato questo:

if otherpids="$(pgrep -f "$0" | grep -vFx "$$")" ; then
  echo >&2 "There are other copies of the script running; exiting."
  ps >&2 -fq "${otherpids//$'\n'/ }" # -q takes about a tenth the time as -p
  exit 1
fi

Questo funziona quasi . Ma il fatto di biforcarsi per eseguire il tubo lo butta via. Quindi anche questo uscirà sempre.

Approccio n. 3 inaffidabile

pids_this_script="$(pgrep -f "$0")"
if not_this_process="$(echo "$pids_this_script" | grep -vFx "$$")"; then
  echo >&2 "There are other copies of this script running; exiting."
  ps -fq "${not_this_process//$'\n'/ }"
  exit 1
fi

Questa versione evita il problema di biforcazione della pipeline nell'approccio n. 2 ottenendo prima tutti i PID che hanno lo script corrente nei loro argomenti della riga di comando, quindi filtrando tale pidlist, separatamente, per omettere il PID dello script corrente.

Questo potrebbe funzionare ... a condizione che nessun altro processo abbia una riga di comando corrispondente a $0, e se lo script viene sempre chiamato allo stesso modo (ad es. Se viene chiamato con un percorso relativo e quindi un percorso assoluto, quest'ultima istanza non noterà la prima ).

Approccio n. 4 inaffidabile

Quindi cosa succede se saltiamo il controllo dell'intera riga di comando, dal momento che potrebbe non indicare uno script effettivamente in esecuzione, e controlliamo lsofinvece di trovare tutti i processi che hanno questo script aperto?

Bene, sì, questo approccio in realtà non è poi così male:

if otherpids="$(lsof -t "$0" | grep -vFx "$$")"; then
  echo >&2 "Error: There are other processes that have this script open - most likely other copies of the script running.  Exiting to avoid conflicts."
  ps >&2 -fq "${otherpids//$'\n'/ }"
  exit 1
fi

Naturalmente, se è in esecuzione una copia dello script, la nuova istanza verrà avviata correttamente e avrai due copie in esecuzione.

O se lo script in esecuzione viene modificato (ad es. Con Vim o con a git checkout), la "nuova" versione dello script si avvierà senza problemi, poiché sia ​​Vim sia il git checkoutrisultato in un nuovo file (un nuovo inode) al posto del vecchio.

Tuttavia, se lo script non viene mai modificato e mai copiato, questa versione è abbastanza buona. Non ci sono condizioni di competizione perché il file di script deve essere già aperto prima di poter raggiungere il controllo.

Possono esserci ancora falsi positivi se un altro processo ha il file di script aperto, ma nota che anche se è aperto per la modifica in Vim, vim in realtà non tiene aperto il file di script, quindi non genererà falsi positivi.

Ma ricorda, non usare questo approccio se lo script potrebbe essere modificato o copiato poiché otterrai falsi negativi, cioè più istanze in esecuzione contemporaneamente - quindi il fatto che la modifica con Vim non dia falsi positivi non dovrebbe importare a te. Ho parlato, però, perché l'approccio # 3 non dare falsi positivi (cioè si rifiuta di iniziare) se avete lo script aperto con Vim.

Quindi cosa fare, allora?

La risposta più votata a questa domanda offre un buon approccio solido.

Forse puoi scriverne uno migliore ... ma se non capisci tutti i problemi e le avvertenze con tutti gli approcci di cui sopra, non è probabile che tu scriva un metodo di blocco che li eviti tutti.



0

Ecco qualcosa che a volte aggiungo su un server per gestire facilmente le condizioni di gara per qualsiasi lavoro sulla macchina. È simile al post di Tim Kennedy, ma in questo modo ottieni la gestione della gara aggiungendo solo una riga a ogni script bash che ne ha bisogno.

Inserisci il contenuto di seguito in eg / opt / racechecker / racechecker:

ZPROGRAMNAME=$(readlink -f $0)
EZPROGRAMNAME=`echo $ZPROGRAMNAME | sed 's/\//_/g'`
EZMAIL="/usr/bin/mail"
EZCAT="/bin/cat"

if  [ -n "$EZPROGRAMNAME" ] ;then
        EZPIDFILE=/tmp/$EZPROGRAMNAME.pid
        if [ -e "$EZPIDFILE" ] ;then
                EZPID=$($EZCAT $EZPIDFILE)
                echo "" | $EZMAIL -s "$ZPROGRAMNAME already running with pid $EZPID"  alarms@someemail.com >>/dev/null
                exit -1
        fi
        echo $$ >> $EZPIDFILE
        function finish {
          rm  $EZPIDFILE
        }
        trap finish EXIT
fi

Ecco come usarlo. Nota la riga dopo lo shebang:

     #/bin/bash
     . /opt/racechecker/racechecker
     echo "script are running"
     sleep 120

Il modo in cui funziona è che capisce il nome del file bashscript principale e crea un file pid sotto "/ tmp". Aggiunge anche un ascoltatore al segnale di fine. L'ascoltatore rimuoverà il file pid quando lo script principale sta completando correttamente.

Invece se esiste un pidfile all'avvio di un'istanza, verrà eseguita l'istruzione if contenente il codice all'interno della seconda istruzione if. In questo caso ho deciso di lanciare una mail di allarme quando ciò accade.

Cosa succede se lo script si arresta in modo anomalo

Un ulteriore esercizio sarebbe quello di gestire gli arresti anomali. Idealmente il pidfile dovrebbe essere rimosso anche se lo script principale si arresta in modo anomalo per qualsiasi motivo, questo non è possibile nella mia versione precedente. Ciò significa che se lo script si arresta in modo anomalo, il pidfile dovrebbe essere rimosso manualmente per ripristinare la funzionalità.

In caso di crash del sistema

È una buona idea archiviare il pidfile / lockfile in ad esempio / tmp. In questo modo gli script continueranno sicuramente a essere eseguiti dopo un crash del sistema poiché i file pid verranno sempre eliminati all'avvio.


A differenza della risposta di Tim Kennedy, la tua sceneggiatura contiene una condizione di gara. Questo perché la verifica della presenza del PIDFILE e della sua creazione condizionale non viene eseguita in un'operazione atomica.
maxschlepzig,

+1 su quello! Lo prenderò in considerazione e modificherò la mia sceneggiatura.
ziggestardust,

-2

Controlla il mio script ...

Potresti ADORARLO ....

[rambabu@Server01 ~]$ sh Prevent_cron-OR-Script_against_parallel_run.sh
Parallel RUN Enabled
Now running
Task completed in Parallel RUN...
[rambabu@Server01 ~]$ cat Prevent_cron-OR-Script_against_parallel_run.sh
#!/bin/bash
#Created by RambabuKella
#Date : 12-12-2013

#LOCK file name
Parallel_RUN="yes"
#Parallel_RUN="no"
PS_GREP=0
LOCK=/var/tmp/mylock_`whoami`_"$0"
#Checking for the process
PS_GREP=`ps -ef |grep "sh $0" |grep -v grep|wc -l`
if [ "$Parallel_RUN" == "no" ] ;then
echo "Parallel RUN Disabled"

 if [ -f $LOCK ] || [ $PS_GREP -gt 2   ] ;then
        echo -e "\nJob is already running OR LOCK file exists. "
        echo -e "\nDetail are : "
        ps -ef |grep  "$0" |grep -v grep
        cat "$LOCK"
  exit 6
 fi
echo -e "LOCK file \" $LOCK \" created on : `date +%F-%H-%M` ." &> $LOCK
# do some work
echo "Now running"
echo "Task completed on with single RUN ..."
#done

rm -v $LOCK 2>/dev/null
exit 0
else

echo "Parallel RUN Enabled"

# do some work
echo "Now running"
echo "Task completed in Parallel RUN..."
#done

exit 0
fi
echo "some thing wrong"
exit 2
[rambabu@Server01 ~]$

-3

Offro la seguente soluzione, in uno script chiamato "flocktest"

#!/bin/bash
export LOGFILE=`basename $0`.logfile
logit () {
echo "$1" >>$LOGFILE
}
PROGPATH=$0
(
flock -x -n 257
(($?)) && logit "'$PROGPATH' is already running!" && exit 0
logit "'$PROGPATH', proc($$): sleeping 30 seconds"
sleep 30
)257<$PROGPATH
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.