Modo rapido e sporco per garantire che sia in esecuzione solo un'istanza di uno script di shell alla volta


Risposte:


109

Ecco un'implementazione che utilizza un file di blocco e fa eco a un PID in esso. Questo serve come protezione se il processo viene interrotto prima di rimuovere il pidfile :

LOCKFILE=/tmp/lock.txt
if [ -e ${LOCKFILE} ] && kill -0 `cat ${LOCKFILE}`; then
    echo "already running"
    exit
fi

# make sure the lockfile is removed when we exit and then claim it
trap "rm -f ${LOCKFILE}; exit" INT TERM EXIT
echo $$ > ${LOCKFILE}

# do stuff
sleep 1000

rm -f ${LOCKFILE}

Il trucco qui è quello kill -0che non fornisce alcun segnale ma controlla solo se esiste un processo con il PID dato. Inoltre, la chiamata a trapassicurerà che il file di blocco venga rimosso anche quando il processo viene interrotto (tranne kill -9).


73
Come già accennato in un commento su un'altra risposta, questo ha un difetto fatale: se l'altro script si avvia tra il controllo e l'eco, sei un brindisi.
Paul Tomblin,

1
Il trucco del collegamento simbolico è accurato, ma se il proprietario del file di blocco viene ucciso -9 o il sistema si arresta in modo anomalo, c'è ancora una condizione di competizione per leggere il collegamento simbolico, notare che il proprietario non c'è più, quindi eliminarlo. Sto rispettando la mia soluzione.
bmdhacks,

9
Atomic check and create è disponibile nella shell usando flock (1) o lockfile (1). Vedi altre risposte
dmckee --- ex gattino moderatore,

3
Vedi la mia risposta per un modo portatile di fare un controllo atomico e creare senza dover fare affidamento su utility come flock o lockfile.
lhunath,

2
Questo non è atomico ed è quindi inutile. È necessario un meccanismo atomico per test e set.
K Richard Pixley,

214

Utilizzare flock(1)per rendere un blocco con ambito esclusivo un descrittore su file. In questo modo è anche possibile sincronizzare diverse parti dello script.

#!/bin/bash

(
  # Wait for lock on /var/lock/.myscript.exclusivelock (fd 200) for 10 seconds
  flock -x -w 10 200 || exit 1

  # Do stuff

) 200>/var/lock/.myscript.exclusivelock

Ciò garantisce che il codice tra (e )sia eseguito solo da un processo alla volta e che il processo non attenda troppo a lungo un blocco.

Avvertenza: questo particolare comando fa parte di util-linux. Se si esegue un sistema operativo diverso da Linux, potrebbe essere o non essere disponibile.


11
Qual è il 200? Dice "fd" nel manul, ma non so cosa significhi.
frizzante

4
@chovy "descrittore di file", un handle intero che designa un file aperto.
Alex B,

6
Se qualcun altro si sta chiedendo: la sintassi ( command A ) command Binvoca una subshell per command A. Documentato su tldp.org/LDP/abs/html/subshells.html . Non sono ancora sicuro dei tempi di invocazione della subshell e del comando B.
Dr. Jan-Philip Gehrcke,

1
Penso che il codice all'interno della sotto-shell dovrebbe essere più simile: if flock -x -w 10 200; then ...Do stuff...; else echo "Failed to lock file" 1>&2; fiquindi se si verifica il timeout (qualche altro processo ha il file bloccato), questo script non va avanti e modifica il file. Probabilmente ... il contro-argomento è "ma se ci sono voluti 10 secondi e il blocco non è ancora disponibile, non sarà mai disponibile", presumibilmente perché il processo che tiene il blocco non sta terminando (forse è in esecuzione sotto un debugger?).
Jonathan Leffler,

1
Il file reindirizzato è solo una cartella di destinazione su cui attivare il blocco, non vi sono dati significativi che vi entrano. La exitda parte all'interno ( ). Al termine del sottoprocesso, il blocco viene automaticamente rilasciato, poiché non è presente alcun processo che lo trattiene.
clacke,

158

Tutti gli approcci che verificano l'esistenza di "file di blocco" sono imperfetti.

Perché? Perché non c'è modo di controllare se esiste un file e crearlo in una singola azione atomica. A causa di ciò; c'è una condizione di gara che SARÀ rendere i vostri tentativi di rottura reciproca esclusione.

Invece, devi usare mkdir. mkdircrea una directory se non esiste ancora e, in caso affermativo, imposta un codice di uscita. Ancora più importante, fa tutto questo in una singola azione atomica rendendolo perfetto per questo scenario.

if ! mkdir /tmp/myscript.lock 2>/dev/null; then
    echo "Myscript is already running." >&2
    exit 1
fi

Per tutti i dettagli, vedi l'eccellente BashFAQ: http://mywiki.wooledge.org/BashFAQ/045

Se si desidera occuparsi dei blocchi non funzionanti, il fusore (1) è utile. L'unico aspetto negativo qui è che l'operazione richiede circa un secondo, quindi non è istantanea.

Ecco una funzione che ho scritto una volta che risolve il problema utilizzando il fusore:

#       mutex file
#
# Open a mutual exclusion lock on the file, unless another process already owns one.
#
# If the file is already locked by another process, the operation fails.
# This function defines a lock on a file as having a file descriptor open to the file.
# This function uses FD 9 to open a lock on the file.  To release the lock, close FD 9:
# exec 9>&-
#
mutex() {
    local file=$1 pid pids 

    exec 9>>"$file"
    { pids=$(fuser -f "$file"); } 2>&- 9>&- 
    for pid in $pids; do
        [[ $pid = $$ ]] && continue

        exec 9>&- 
        return 1 # Locked by a pid.
    done 
}

Puoi usarlo in uno script in questo modo:

mutex /var/run/myscript.lock || { echo "Already running." >&2; exit 1; }

Se non ti interessa la portabilità (queste soluzioni dovrebbero funzionare praticamente su qualsiasi box UNIX), il fuser di Linux (1) offre alcune opzioni aggiuntive e c'è anche flock (1) .


1
È possibile combinare la if ! mkdirparte con il controllo se il processo con il PID memorizzato (all'avvio riuscito) all'interno del lockdir è effettivamente in esecuzione e identico allo script per la protezione degli staleni. Ciò proteggerebbe anche dal riutilizzo del PID dopo un riavvio e non richiederebbe nemmeno fuser.
Tobias Kienzler,

4
È certamente vero che mkdirnon è definito come un'operazione atomica e come tale "effetto collaterale" è un dettaglio di implementazione del file system. Gli credo pienamente se dice che NFS non lo implementa in modo atomico. Anche se non sospetto che la tua /tmpsarà una condivisione NFS e sarà probabilmente fornita da un fs che implementa mkdiratomicamente.
lhunath,

5
Ma c'è un modo per verificare l'esistenza di un file normale e crearlo atomicamente se non lo fa: usando lnper creare un collegamento reale da un altro file. Se hai strani filesystem che non lo garantiscono, puoi in seguito controllare l'inode del nuovo file per vedere se è lo stesso del file originale.
Juan Cespedes,

4
V'è 'un modo per verificare se esiste un file e di creare in una singola azione atomica' - è open(... O_CREAT|O_EXCL). Hai solo bisogno di un programma utente adatto per farlo, come lockfile-create(in lockfile-progs) o dotlockfile(in liblockfile-bin). E assicurati di ripulire correttamente (ad es. trap EXIT) O di verificare i blocchi stantii (ad es. Con --use-pid).
Toby Speight,

5
"Tutti gli approcci che testano l'esistenza di" file di blocco "sono imperfetti. Perché? Perché non c'è modo di controllare se esiste un file e crearlo in una singola azione atomica." - Per renderlo atomico, deve essere fatto in il livello del kernel - ed è fatto a livello del kernel con flock (1) linux.die.net/man/1/flock che appare dalla data di copyright di man circa dal 2006. Quindi ho fatto un downvote (- 1), niente di personale, solo avere una forte convinzione che l'uso degli strumenti implementati dal kernel forniti dagli sviluppatori del kernel sia corretto.
Craig Hicks,

42

C'è un wrapper attorno alla chiamata di sistema flock (2) chiamato, senza fantasia, flock (1). Questo rende relativamente facile ottenere in modo affidabile blocchi esclusivi senza preoccuparsi della pulizia, ecc. Ci sono esempi nella pagina man su come usarlo in uno script di shell.


3
La flock()chiamata di sistema non è POSIX e non funziona per i file sui montaggi NFS.
maxschlepzig,

17
In esecuzione da un lavoro Cron che utilizzo flock -x -n %lock file% -c "%command%"per assicurarmi che venga eseguita solo un'istanza.
Ryall,

Aww, invece del gregge privo di fantasia (1) avrebbero dovuto scegliere qualcosa come flock (U). ... ha una certa familiarità con esso. . Sembra che ne abbia sentito parlare prima o due.
Kent Kruckeberg,

È da notare che la documentazione flock (2) specifica l'uso solo con i file, ma la documentazione flock (1) specifica l'uso con file o directory. La documentazione flock (1) non è esplicita su come indicare la differenza durante la creazione, ma suppongo che sia fatta aggiungendo un "/" finale. Comunque, se flock (1) può gestire directory ma flock (2) non può, allora flock (1) non viene implementato solo su flock (2).
Craig Hicks,

27

Hai bisogno di un'operazione atomica, come il gregge, altrimenti alla fine questo fallirà.

Ma cosa fare se il gregge non è disponibile. Bene c'è mkdir. Anche questa è un'operazione atomica. Solo un processo porterà a un mkdir di successo, tutti gli altri falliranno.

Quindi il codice è:

if mkdir /var/lock/.myscript.exclusivelock
then
  # do stuff
  :
  rmdir /var/lock/.myscript.exclusivelock
fi

Devi prenderti cura dei blocchi stantii altrimenti dopo un crash il tuo script non verrà mai più eseguito.


1
Esegui alcune volte contemporaneamente (come "./a.sh & ./a.sh & ./a.sh & ./a.sh & ./a.sh & ./a.sh & ./a.sh & ") e lo script trapelerà alcune volte.
Nippysaurus,

7
@Nippysaurus: questo metodo di blocco non perde. Quello che hai visto è stato lo script iniziale terminato prima che tutte le copie fossero avviate, quindi un altro è stato in grado (correttamente) di ottenere il blocco. Per evitare questo falso positivo, aggiungi un sleep 10prima rmdire prova a ricominciare a cascata: nulla "perde".
Sir Athos,

Altre fonti affermano che mkdir non è atomico su alcuni filesystem come NFS. E tra l'altro ho visto occasioni in cui su mkdir ricorsivo simultaneo NFS porta a errori a volte con i lavori di matrice jenkins. Quindi sono abbastanza sicuro che sia così. Ma mkdir è piuttosto carino per i casi d'uso meno impegnativi IMO.
Akostadinov,

Puoi usare l'opzione noclobber di Bash con normali file.
Palec,

26

Per rendere affidabile il bloccaggio è necessaria un'operazione atomica. Molte delle proposte di cui sopra non sono atomiche. L'utilità lockfile proposta (1) sembra promettente, come menzionato nella pagina man, che è "resistente a NFS". Se il tuo sistema operativo non supporta lockfile (1) e la tua soluzione deve funzionare su NFS, non hai molte opzioni ....

NFSv2 ha due operazioni atomiche:

  • link simbolico
  • rinominare

Con NFSv3 anche la chiamata di creazione è atomica.

Le operazioni di directory NON sono atomiche in NFSv2 e NFSv3 (fare riferimento al libro "NFS Illustrated" di Brent Callaghan, ISBN 0-201-32570-5; Brent è un veterano di NFS a Sun).

Sapendo questo, puoi implementare spin-lock per file e directory (nella shell, non in PHP):

blocca dir corrente:

while ! ln -s . lock; do :; done

blocca un file:

while ! ln -s ${f} ${f}.lock; do :; done

sblocca dir corrente (presupposto, il processo in esecuzione ha davvero acquisito il blocco):

mv lock deleteme && rm deleteme

sbloccare un file (presupposto, il processo in esecuzione ha davvero acquisito il blocco):

mv ${f}.lock ${f}.deleteme && rm ${f}.deleteme

Inoltre, Remove non è atomico, quindi prima la ridenominazione (che è atomica) e quindi la rimozione.

Per le chiamate symlink e rename, entrambi i nomi file devono risiedere sullo stesso filesystem. La mia proposta: utilizzare solo nomi di file semplici (senza percorsi) e inserire file e bloccare nella stessa directory.


Quali pagine di NFS Illustrated supportano l'affermazione che mkdir non è atomico su NFS?
maxschlepzig,

Grazie per questa tecnica. Un'implementazione di shell mutex è disponibile nella mia nuova libreria di shell: github.com/Offirmo/offirmo-shell-lib , vedere "mutex". Utilizza lockfilese disponibile, o fallback a questo symlinkmetodo in caso contrario.
Offirmo,

Bello. Sfortunatamente questo metodo non fornisce un modo per eliminare automaticamente i blocchi obsoleti.
Richard Hansen,

Per lo sblocco a due stadi ( mv, rm), dovrebbe rm -fessere usato, piuttosto che rmnel caso in cui due processi P1, P2 stiano correndo? Ad esempio, P1 inizia a sbloccare con mv, quindi P2 blocca, quindi P2 sblocca (entrambi mve rm), infine P1 tenta rme fallisce.
Matt Wallis,

1
@MattWallis L'ultimo problema potrebbe essere facilmente mitigato includendo $$nel ${f}.deletemenome file.
Stefan Majewsky,

23

Un'altra opzione è quella di utilizzare l' noclobberopzione shell eseguendo set -C. Quindi >fallirà se il file esiste già.

In breve:

set -C
lockfile="/tmp/locktest.lock"
if echo "$$" > "$lockfile"; then
    echo "Successfully acquired lock"
    # do work
    rm "$lockfile"    # XXX or via trap - see below
else
    echo "Cannot acquire lock - already locked by $(cat "$lockfile")"
fi

Questo fa sì che la shell chiami:

open(pathname, O_CREAT|O_EXCL)

che crea atomicamente il file o fallisce se il file esiste già.


Secondo un commento su BashFAQ 045 , questo potrebbe non riuscire ksh88, ma funziona in tutte le mie shell:

$ strace -e trace=creat,open -f /bin/bash /home/mikel/bin/testopen 2>&1 | grep -F testopen.lock
open("/tmp/testopen.lock", O_WRONLY|O_CREAT|O_EXCL|O_LARGEFILE, 0666) = 3

$ strace -e trace=creat,open -f /bin/zsh /home/mikel/bin/testopen 2>&1 | grep -F testopen.lock
open("/tmp/testopen.lock", O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY|O_LARGEFILE, 0666) = 3

$ strace -e trace=creat,open -f /bin/pdksh /home/mikel/bin/testopen 2>&1 | grep -F testopen.lock
open("/tmp/testopen.lock", O_WRONLY|O_CREAT|O_EXCL|O_TRUNC|O_LARGEFILE, 0666) = 3

$ strace -e trace=creat,open -f /bin/dash /home/mikel/bin/testopen 2>&1 | grep -F testopen.lock
open("/tmp/testopen.lock", O_WRONLY|O_CREAT|O_EXCL|O_LARGEFILE, 0666) = 3

Interessante che pdkshaggiunge la O_TRUNCbandiera, ma ovviamente è ridondante:
o stai creando un file vuoto o non stai facendo nulla.


Come fai il rm dipende da come desideri che vengano gestite le uscite impure.

Elimina all'uscita pulita

Le nuove esecuzioni falliscono fino a quando il problema che ha causato l'uscita impura è stato risolto e il file di blocco è stato rimosso manualmente.

# acquire lock
# do work (code here may call exit, etc.)
rm "$lockfile"

Elimina su qualsiasi uscita

Le nuove esecuzioni hanno esito positivo a condizione che lo script non sia già in esecuzione.

trap 'rm "$lockfile"' EXIT

Approccio molto nuovo ... questo sembra essere un modo per realizzare l'atomicità usando un file di blocco piuttosto che una directory di blocco.
Matt Caldwell,

Bel approccio. :-) Sulla trappola EXIT, dovrebbe limitare quale processo può ripulire il file di blocco. Ad esempio: trap 'if [[$ (cat "$ lockfile") == "$$"]]; quindi rm "$ lockfile"; fi 'EXIT
Kevin Seifert

1
I file di blocco non sono atomici su NFS. ecco perché le persone sono passate all'utilizzo delle directory di blocco.
K Richard Pixley,

20

Puoi usarlo GNU Parallelperché funziona come mutex quando viene chiamato come sem. Quindi, in termini concreti, puoi usare:

sem --id SCRIPTSINGLETON yourScript

Se vuoi anche un timeout, usa:

sem --id SCRIPTSINGLETON --semaphoretimeout -10 yourScript

Il timeout di <0 significa uscire senza eseguire lo script se il semaforo non viene rilasciato entro il timeout, il timeout di> 0 significa eseguire lo script comunque.

Nota che dovresti dargli un nome (con --id) altrimenti sarà impostato sul terminale di controllo.

GNU Parallel è un'installazione molto semplice sulla maggior parte delle piattaforme Linux / OSX / Unix - è solo uno script Perl.


Le persone troppo cattive sono riluttanti a votare risposte inutili: questo porta a seppellire nuove risposte pertinenti in un mucchio di spazzatura.
Dmitry Grigoryev,

4
Abbiamo solo bisogno di molti voti. Questa è una risposta così ordinata e poco conosciuta. (Anche se essere un PO pedante voleva essere veloce e sporco mentre questo è veloce e pulito!) Maggiori informazioni sulla semdomanda correlata unix.stackexchange.com/a/322200/199525 .
Parzialmente nuvoloso

16

Per gli script di shell, tendo ad andare mkdiroltre flockperché rende le serrature più portatili.

In entrambi i casi, l'utilizzo set -enon è sufficiente. Questo esce dallo script solo se un comando non riesce. Le tue serrature rimarranno comunque indietro.

Per una corretta pulizia del blocco, dovresti davvero impostare le tue trappole su qualcosa come questo codice psuedo (sollevato, semplificato e non testato ma da script usati attivamente):

#=======================================================================
# Predefined Global Variables
#=======================================================================

TMPDIR=/tmp/myapp
[[ ! -d $TMP_DIR ]] \
    && mkdir -p $TMP_DIR \
    && chmod 700 $TMPDIR

LOCK_DIR=$TMP_DIR/lock

#=======================================================================
# Functions
#=======================================================================

function mklock {
    __lockdir="$LOCK_DIR/$(date +%s.%N).$$" # Private Global. Use Epoch.Nano.PID

    # If it can create $LOCK_DIR then no other instance is running
    if $(mkdir $LOCK_DIR)
    then
        mkdir $__lockdir  # create this instance's specific lock in queue
        LOCK_EXISTS=true  # Global
    else
        echo "FATAL: Lock already exists. Another copy is running or manually lock clean up required."
        exit 1001  # Or work out some sleep_while_execution_lock elsewhere
    fi
}

function rmlock {
    [[ ! -d $__lockdir ]] \
        && echo "WARNING: Lock is missing. $__lockdir does not exist" \
        || rmdir $__lockdir
}

#-----------------------------------------------------------------------
# Private Signal Traps Functions {{{2
#
# DANGER: SIGKILL cannot be trapped. So, try not to `kill -9 PID` or 
#         there will be *NO CLEAN UP*. You'll have to manually remove 
#         any locks in place.
#-----------------------------------------------------------------------
function __sig_exit {

    # Place your clean up logic here 

    # Remove the LOCK
    [[ -n $LOCK_EXISTS ]] && rmlock
}

function __sig_int {
    echo "WARNING: SIGINT caught"    
    exit 1002
}

function __sig_quit {
    echo "SIGQUIT caught"
    exit 1003
}

function __sig_term {
    echo "WARNING: SIGTERM caught"    
    exit 1015
}

#=======================================================================
# Main
#=======================================================================

# Set TRAPs
trap __sig_exit EXIT    # SIGEXIT
trap __sig_int INT      # SIGINT
trap __sig_quit QUIT    # SIGQUIT
trap __sig_term TERM    # SIGTERM

mklock

# CODE

exit # No need for cleanup code here being in the __sig_exit trap function

Ecco cosa accadrà. Tutte le trappole produrranno un'uscita, quindi la funzione __sig_exitavverrà sempre (salvo un SIGKILL) che pulisce i tuoi blocchi.

Nota: i miei valori di uscita non sono valori bassi. Perché? Vari sistemi di elaborazione batch fanno o hanno aspettative sui numeri da 0 a 31. Impostandoli su qualcos'altro, posso fare in modo che i miei script e flussi batch reagiscano di conseguenza al precedente lavoro batch o script.


2
La tua sceneggiatura è troppo prolissa, penso che potrebbe essere stata molto più breve, ma nel complesso sì, devi fare delle trappole per farlo correttamente. Inoltre aggiungerei SIGHUP.
Mojuba,

Funziona bene, tranne che sembra controllare $ LOCK_DIR mentre rimuove $ __ lockdir. Forse dovrei suggerire quando si rimuove il blocco si farebbe rm -r $ LOCK_DIR?
Bevada,

Grazie per il suggerimento Quanto sopra è stato rimosso dal codice e inserito in modo psuedo, quindi dovrà essere ottimizzato in base all'uso della gente. Tuttavia, ho deliberatamente scelto rmdir nel mio caso poiché rmdir rimuove in modo sicuro le directory solo se sono vuote. Se le persone stanno inserendo risorse come file PID, ecc., Dovrebbero modificare la loro pulizia del blocco in modo più aggressivo rm -r $LOCK_DIRo addirittura forzarlo come necessario (come ho fatto anche io in casi speciali come il possesso di file scratch relativi). Saluti.
Mark Stinson,

Hai provato exit 1002?
Gilles Quenot,

13

Davvero veloce e davvero sporco? Questo one-liner nella parte superiore della tua sceneggiatura funzionerà:

[[ $(pgrep -c "`basename \"$0\"`") -gt 1 ]] && exit

Naturalmente, assicurati solo che il nome del tuo script sia univoco. :)


Come posso simularlo per testarlo? C'è un modo per avviare uno script due volte in una riga e forse ricevere un avviso, se è già in esecuzione?
rubo77,

2
Questo non funziona affatto! Perché controllare -gt 2? grep non si trova sempre nel risultato di ps!
rubo77,

pgrepnon è in POSIX. Se vuoi che funzioni in modo portabile, hai bisogno di POSIX psed elaborare il suo output.
Palec,

Su OSX -cnon esiste, dovrai usare | wc -l. Informazioni sul confronto dei numeri: -gt 1è selezionato poiché la prima istanza si vede da sola.
Benjamin Peter,

6

Ecco un approccio che combina il blocco della directory atomica con un controllo per il blocco non aggiornato tramite PID e il riavvio se obsoleto. Inoltre, questo non si basa su alcun bashismo.

#!/bin/dash

SCRIPTNAME=$(basename $0)
LOCKDIR="/var/lock/${SCRIPTNAME}"
PIDFILE="${LOCKDIR}/pid"

if ! mkdir $LOCKDIR 2>/dev/null
then
    # lock failed, but check for stale one by checking if the PID is really existing
    PID=$(cat $PIDFILE)
    if ! kill -0 $PID 2>/dev/null
    then
       echo "Removing stale lock of nonexistent PID ${PID}" >&2
       rm -rf $LOCKDIR
       echo "Restarting myself (${SCRIPTNAME})" >&2
       exec "$0" "$@"
    fi
    echo "$SCRIPTNAME is already running, bailing out" >&2
    exit 1
else
    # lock successfully acquired, save PID
    echo $$ > $PIDFILE
fi

trap "rm -rf ${LOCKDIR}" QUIT INT TERM EXIT


echo hello

sleep 30s

echo bye

5

Creare un file di blocco in una posizione nota e verificare l'esistenza all'avvio dello script? Inserire il PID nel file potrebbe essere utile se qualcuno sta tentando di rintracciare un'istanza errata che impedisce l'esecuzione dello script.


5

Questo esempio è spiegato nel gregge man, ma ha bisogno di alcuni miglioramenti, perché dovremmo gestire bug e codici di uscita:

   #!/bin/bash
   #set -e this is useful only for very stupid scripts because script fails when anything command exits with status more than 0 !! without possibility for capture exit codes. not all commands exits >0 are failed.

( #start subprocess
  # Wait for lock on /var/lock/.myscript.exclusivelock (fd 200) for 10 seconds
  flock -x -w 10 200
  if [ "$?" != "0" ]; then echo Cannot lock!; exit 1; fi
  echo $$>>/var/lock/.myscript.exclusivelock #for backward lockdir compatibility, notice this command is executed AFTER command bottom  ) 200>/var/lock/.myscript.exclusivelock.
  # Do stuff
  # you can properly manage exit codes with multiple command and process algorithm.
  # I suggest throw this all to external procedure than can properly handle exit X commands

) 200>/var/lock/.myscript.exclusivelock   #exit subprocess

FLOCKEXIT=$?  #save exitcode status
    #do some finish commands

exit $FLOCKEXIT   #return properly exitcode, may be usefull inside external scripts

Puoi usare un altro metodo, elencare i processi che ho usato in passato. Ma questo è più complicato del metodo sopra. Dovresti elencare i processi per ps, filtrare per nome, filtro aggiuntivo grep -v grep per rimuovere il parassita e infine contarlo per grep -c. e confronta con il numero. È complicato e incerto


1
Puoi usare ln -s, perché questo può creare un collegamento simbolico solo quando non esiste alcun file o collegamento simbolico, lo stesso di mkdir. molti processi di sistema utilizzavano collegamenti simbolici in passato, ad esempio init o inetd. synlink mantiene l'ID di processo, ma in realtà non punta a nulla. per anni questo comportamento è stato cambiato. processi utilizza greggi e semafori.
Znik

5

Le risposte esistenti pubblicate si basano sull'utilità CLI flocko non proteggono correttamente il file di blocco. L'utility flock non è disponibile su tutti i sistemi non Linux (es. FreeBSD) e non funziona correttamente su NFS.

All'inizio dell'amministrazione e dello sviluppo del sistema, mi è stato detto che un metodo sicuro e relativamente portatile per creare un file di blocco era quello di creare un file temporaneo usando mkemp(3)omkemp(1) , scrivere informazioni identificative nel file temporaneo (cioè PID), quindi hard link il file temporaneo al file di blocco. Se il collegamento ha avuto esito positivo, il blocco è stato ottenuto correttamente.

Quando si utilizzano i blocchi negli script della shell, in genere inserisco una obtain_lock()funzione in un profilo condiviso e quindi la fonte dagli script. Di seguito è riportato un esempio della mia funzione di blocco:

obtain_lock()
{
  LOCK="${1}"
  LOCKDIR="$(dirname "${LOCK}")"
  LOCKFILE="$(basename "${LOCK}")"

  # create temp lock file
  TMPLOCK=$(mktemp -p "${LOCKDIR}" "${LOCKFILE}XXXXXX" 2> /dev/null)
  if test "x${TMPLOCK}" == "x";then
     echo "unable to create temporary file with mktemp" 1>&2
     return 1
  fi
  echo "$$" > "${TMPLOCK}"

  # attempt to obtain lock file
  ln "${TMPLOCK}" "${LOCK}" 2> /dev/null
  if test $? -ne 0;then
     rm -f "${TMPLOCK}"
     echo "unable to obtain lockfile" 1>&2
     if test -f "${LOCK}";then
        echo "current lock information held by: $(cat "${LOCK}")" 1>&2
     fi
     return 2
  fi
  rm -f "${TMPLOCK}"

  return 0;
};

Di seguito è riportato un esempio di come utilizzare la funzione di blocco:

#!/bin/sh

. /path/to/locking/profile.sh
PROG_LOCKFILE="/tmp/myprog.lock"

clean_up()
{
  rm -f "${PROG_LOCKFILE}"
}

obtain_lock "${PROG_LOCKFILE}"
if test $? -ne 0;then
   exit 1
fi
trap clean_up SIGHUP SIGINT SIGTERM

# bulk of script

clean_up
exit 0
# end of script

Ricordati di chiamare clean_up in qualsiasi punto di uscita nel tuo script.

Ho usato quanto sopra in entrambi gli ambienti Linux e FreeBSD.


4

Quando si prende di mira una macchina Debian trovo che il lockfile-progspacchetto sia una buona soluzione. procmailviene fornito anche con uno lockfilestrumento. Tuttavia a volte sono bloccato con nessuno di questi.

Ecco la mia soluzione che utilizza mkdirper atomicità e un file PID per rilevare blocchi non aggiornati. Questo codice è attualmente in produzione su una configurazione Cygwin e funziona bene.

Per usarlo chiama semplicemente exclusive_lock_requirequando hai bisogno di avere accesso esclusivo a qualcosa. Un parametro facoltativo per il nome del blocco consente di condividere i blocchi tra diversi script. Ci sono anche due funzioni di livello inferiore ( exclusive_lock_trye exclusive_lock_retry) se hai bisogno di qualcosa di più complesso.

function exclusive_lock_try() # [lockname]
{

    local LOCK_NAME="${1:-`basename $0`}"

    LOCK_DIR="/tmp/.${LOCK_NAME}.lock"
    local LOCK_PID_FILE="${LOCK_DIR}/${LOCK_NAME}.pid"

    if [ -e "$LOCK_DIR" ]
    then
        local LOCK_PID="`cat "$LOCK_PID_FILE" 2> /dev/null`"
        if [ ! -z "$LOCK_PID" ] && kill -0 "$LOCK_PID" 2> /dev/null
        then
            # locked by non-dead process
            echo "\"$LOCK_NAME\" lock currently held by PID $LOCK_PID"
            return 1
        else
            # orphaned lock, take it over
            ( echo $$ > "$LOCK_PID_FILE" ) 2> /dev/null && local LOCK_PID="$$"
        fi
    fi
    if [ "`trap -p EXIT`" != "" ]
    then
        # already have an EXIT trap
        echo "Cannot get lock, already have an EXIT trap"
        return 1
    fi
    if [ "$LOCK_PID" != "$$" ] &&
        ! ( umask 077 && mkdir "$LOCK_DIR" && umask 177 && echo $$ > "$LOCK_PID_FILE" ) 2> /dev/null
    then
        local LOCK_PID="`cat "$LOCK_PID_FILE" 2> /dev/null`"
        # unable to acquire lock, new process got in first
        echo "\"$LOCK_NAME\" lock currently held by PID $LOCK_PID"
        return 1
    fi
    trap "/bin/rm -rf \"$LOCK_DIR\"; exit;" EXIT

    return 0 # got lock

}

function exclusive_lock_retry() # [lockname] [retries] [delay]
{

    local LOCK_NAME="$1"
    local MAX_TRIES="${2:-5}"
    local DELAY="${3:-2}"

    local TRIES=0
    local LOCK_RETVAL

    while [ "$TRIES" -lt "$MAX_TRIES" ]
    do

        if [ "$TRIES" -gt 0 ]
        then
            sleep "$DELAY"
        fi
        local TRIES=$(( $TRIES + 1 ))

        if [ "$TRIES" -lt "$MAX_TRIES" ]
        then
            exclusive_lock_try "$LOCK_NAME" > /dev/null
        else
            exclusive_lock_try "$LOCK_NAME"
        fi
        LOCK_RETVAL="${PIPESTATUS[0]}"

        if [ "$LOCK_RETVAL" -eq 0 ]
        then
            return 0
        fi

    done

    return "$LOCK_RETVAL"

}

function exclusive_lock_require() # [lockname] [retries] [delay]
{
    if ! exclusive_lock_retry "$@"
    then
        exit 1
    fi
}

Grazie, l'ho provato su Cygwin e ha superato test semplici.
ndemou,

4

Se i limiti del flock, che sono già stati descritti altrove in questo thread, non sono un problema per te, allora dovrebbe funzionare:

#!/bin/bash

{
    # exit if we are unable to obtain a lock; this would happen if 
    # the script is already running elsewhere
    # note: -x (exclusive) is the default
    flock -n 100 || exit

    # put commands to run here
    sleep 100
} 100>/tmp/myjob.lock 

3
Ho solo pensato che -x (blocco scrittura) è già impostato di default.
Keldon Alleyne,

e lo -nfarà exit 1immediatamente se non riesce a ottenere il blocco
Anentropic

Grazie @KeldonAlleyne, ho aggiornato il codice per rimuovere "-x" poiché è predefinito.
presto8

3

Alcuni unix hanno lockfileche è molto simile al già citato flock.

Dalla manpage:

lockfile può essere utilizzato per creare uno o più file semaforo. Se il file di blocco non è in grado di creare tutti i file specificati (nell'ordine specificato), attende lo sleeptime (il valore predefinito è 8) secondi e riprova l'ultimo file non riuscito. È possibile specificare il numero di tentativi da eseguire fino alla restituzione dell'errore. Se il numero di tentativi è -1 (impostazione predefinita, ovvero -r-1) il file di blocco riproverà per sempre.


come otteniamo l' lockfileutilità ??
Offirmo,

lockfileè distribuito con procmail. Inoltre esiste un'alternativa dotlockfileal liblockfilepacchetto. Entrambi affermano di lavorare in modo affidabile su NFS.
Mr. Deathless,

3

In realtà anche se la risposta di bmdhacks è quasi buona, c'è una leggera possibilità che il secondo script venga eseguito dopo aver prima controllato il file di blocco e prima che lo scrivesse. Quindi entrambi scriveranno il file di blocco ed entrambi saranno in esecuzione. Ecco come farlo funzionare di sicuro:

lockfile=/var/lock/myscript.lock

if ( set -o noclobber; echo "$$" > "$lockfile") 2> /dev/null ; then
  trap 'rm -f "$lockfile"; exit $?' INT TERM EXIT
else
  # or you can decide to skip the "else" part if you want
  echo "Another instance is already running!"
fi

Il noclobber opzione farà in modo che il comando di reindirizzamento fallisca se il file esiste già. Quindi il comando di reindirizzamento è in realtà atomico: scrivi e controlla il file con un solo comando. Non è necessario rimuovere il file di blocco alla fine del file: verrà rimosso dalla trap. Spero che questo aiuti alle persone che lo leggeranno in seguito.

PS Non ho visto che Mikel ha già risposto correttamente alla domanda, anche se non ha incluso il comando trap per ridurre la possibilità che il file di blocco venga lasciato dopo aver interrotto lo script con Ctrl-C per esempio. Quindi questa è la soluzione completa


3

Volevo eliminare i file di lock, i lockdir, i programmi di blocco speciali e anche pidofdal momento che non si trovano su tutte le installazioni di Linux. Volevo anche avere il codice più semplice possibile (o almeno il minor numero di righe possibile). ifDichiarazione più semplice , in una riga:

if [[ $(ps axf | awk -v pid=$$ '$1!=pid && $6~/'$(basename $0)'/{print $1}') ]]; then echo "Already running"; exit; fi

1
Questo è sensibile all'output di "ps", sulla mia macchina (Ubuntu 14.04, / bin / ps dalla versione 3.3.9 di procps-ng) il comando "ps axf" stampa i caratteri dell'albero ASCII che interrompono i numeri di campo. Questo ha funzionato per me: /bin/ps -a --format pid,cmd | awk -v pid=$$ '/'$(basename $0)'/ { if ($1!=pid) print $1; }'
qneill

2

Uso un approccio semplice che gestisce i file di blocco non aggiornati.

Si noti che alcune delle soluzioni precedenti che memorizzano il pid, ignorano il fatto che il pid può avvolgersi. Quindi, solo verificare se esiste un processo valido con il pid memorizzato non è sufficiente, specialmente per gli script a esecuzione prolungata.

Uso noclobber per assicurarmi che solo uno script possa aprire e scrivere contemporaneamente nel file di blocco. Inoltre, memorizzo abbastanza informazioni per identificare in modo univoco un processo nel file di blocco. Definisco l'insieme di dati per identificare in modo univoco un processo che deve essere pid, ppid, lstart.

Quando viene avviato un nuovo script, se non riesce a creare il file di blocco, verifica che il processo che ha creato il file di blocco sia ancora attivo. In caso contrario, supponiamo che il processo originale sia deceduto in modo ingrato e abbia lasciato un file di blocco obsoleto. Il nuovo script prende quindi la proprietà del file di blocco e tutto va bene di nuovo nel mondo.

Dovrebbe funzionare con più shell su più piattaforme. Veloce, portatile e semplice.

#!/usr/bin/env sh
# Author: rouble

LOCKFILE=/var/tmp/lockfile #customize this line

trap release INT TERM EXIT

# Creates a lockfile. Sets global variable $ACQUIRED to true on success.
# 
# Returns 0 if it is successfully able to create lockfile.
acquire () {
    set -C #Shell noclobber option. If file exists, > will fail.
    UUID=`ps -eo pid,ppid,lstart $$ | tail -1`
    if (echo "$UUID" > "$LOCKFILE") 2>/dev/null; then
        ACQUIRED="TRUE"
        return 0
    else
        if [ -e $LOCKFILE ]; then 
            # We may be dealing with a stale lock file.
            # Bring out the magnifying glass. 
            CURRENT_UUID_FROM_LOCKFILE=`cat $LOCKFILE`
            CURRENT_PID_FROM_LOCKFILE=`cat $LOCKFILE | cut -f 1 -d " "`
            CURRENT_UUID_FROM_PS=`ps -eo pid,ppid,lstart $CURRENT_PID_FROM_LOCKFILE | tail -1`
            if [ "$CURRENT_UUID_FROM_LOCKFILE" == "$CURRENT_UUID_FROM_PS" ]; then 
                echo "Script already running with following identification: $CURRENT_UUID_FROM_LOCKFILE" >&2
                return 1
            else
                # The process that created this lock file died an ungraceful death. 
                # Take ownership of the lock file.
                echo "The process $CURRENT_UUID_FROM_LOCKFILE is no longer around. Taking ownership of $LOCKFILE"
                release "FORCE"
                if (echo "$UUID" > "$LOCKFILE") 2>/dev/null; then
                    ACQUIRED="TRUE"
                    return 0
                else
                    echo "Cannot write to $LOCKFILE. Error." >&2
                    return 1
                fi
            fi
        else
            echo "Do you have write permissons to $LOCKFILE ?" >&2
            return 1
        fi
    fi
}

# Removes the lock file only if this script created it ($ACQUIRED is set), 
# OR, if we are removing a stale lock file (first parameter is "FORCE") 
release () {
    #Destroy lock file. Take no prisoners.
    if [ "$ACQUIRED" ] || [ "$1" == "FORCE" ]; then
        rm -f $LOCKFILE
    fi
}

# Test code
# int main( int argc, const char* argv[] )
echo "Acquring lock."
acquire
if [ $? -eq 0 ]; then 
    echo "Acquired lock."
    read -p "Press [Enter] key to release lock..."
    release
    echo "Released lock."
else
    echo "Unable to acquire lock."
fi

Ti ho dato +1 per una soluzione diversa. Althoug non funziona né in AIX (> ps -eo pid, ppid, lstart $$ | tail -1 ps: elenco non valido con -o.) Non HP-UX (> ps -eo pid, ppid, lstart $$ | tail -1 ps: opzione illegale - o). Grazie.
Tagar,

2

Aggiungi questa riga all'inizio del tuo script

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

È un codice boilerplate da man flock.

Se si desidera più registrazione, utilizzare questo

[ "${FLOCKER}" != "$0" ] && { echo "Trying to start build from queue... "; exec bash -c "FLOCKER='$0' flock -E $E_LOCKED -en '$0' '$0' '$@' || if [ \"\$?\" -eq $E_LOCKED ]; then echo 'Locked.'; fi"; } || echo "Lock is free. Completing."

Questo imposta e controlla i blocchi usando l' flockutilità. Questo codice rileva se è stato eseguito per la prima volta controllando la variabile FLOCKER, se non è impostato sul nome dello script, quindi tenta di riavviare lo script in modo ricorsivo utilizzando flock e con la variabile FLOCKER inizializzata, se FLOCKER è impostato correttamente, quindi flock su iterazione precedente riuscito ed è OK procedere. Se il blocco è occupato, non riesce con il codice di uscita configurabile.

Sembra non funzionare su Debian 7, ma sembra funzionare di nuovo con il pacchetto sperimentale util-linux 2.25. Scrive "flock: ... File di testo occupato". Potrebbe essere ignorato disabilitando il permesso di scrittura sul tuo script.


1

PID e lockfile sono sicuramente i più affidabili. Quando si tenta di eseguire il programma, è possibile verificare il file di blocco che e se esiste, può utilizzare psper vedere se il processo è ancora in esecuzione. In caso contrario, lo script può iniziare, aggiornando il PID nel file di blocco.


1

Trovo che la soluzione di bmdhack sia la più pratica, almeno per il mio caso d'uso. L'uso di flock e lockfile si basa sulla rimozione del lockfile usando rm al termine dello script, cosa che non può essere sempre garantita (es. Kill -9).

Vorrei cambiare una cosa minore sulla soluzione di bmdhack: è utile rimuovere il file di blocco, senza dichiarare che ciò non è necessario per il funzionamento sicuro di questo semaforo. Il suo uso di kill -0 assicura che un vecchio file di lock per un processo morto verrà semplicemente ignorato / sovrascritto.

La mia soluzione semplificata è quindi semplicemente aggiungere quanto segue all'inizio del singleton:

## Test the lock
LOCKFILE=/tmp/singleton.lock 
if [ -e ${LOCKFILE} ] && kill -0 `cat ${LOCKFILE}`; then
    echo "Script already running. bye!"
    exit 
fi

## Set the lock 
echo $$ > ${LOCKFILE}

Ovviamente, questo script ha ancora il difetto che i processi che possono iniziare contemporaneamente hanno un rischio di gara, poiché il test di blocco e le operazioni impostate non sono una singola azione atomica. Ma la soluzione proposta da parte di lhunath di usare mkdir ha il difetto che uno script ucciso potrebbe lasciare dietro la directory, impedendo così l'esecuzione di altre istanze.


1

I semaforici usi di utilità flock(come discusso in precedenza, ad esempio mediante presto8) per implementare un semaforo conteggio . Consente qualsiasi numero specifico di processi simultanei desiderati. Lo usiamo per limitare il livello di concorrenza di vari processi di lavoro in coda.

È come sem ma molto più leggero. (Informativa completa: l'ho scritto dopo aver scoperto che il sem era troppo pesante per le nostre esigenze e non era disponibile una semplice utility di conteggio dei semafori.)


1

Un esempio con flock (1) ma senza subshell. Il file flock () ed / tmp / foo non viene mai rimosso, ma ciò non importa in quanto viene flock () e un-flock () ed.

#!/bin/bash

exec 9<> /tmp/foo
flock -n 9
RET=$?
if [[ $RET -ne 0 ]] ; then
    echo "lock failed, exiting"
    exit
fi

#Now we are inside the "critical section"
echo "inside lock"
sleep 5
exec 9>&- #close fd 9, and release lock

#The part below is outside the critical section (the lock)
echo "lock released"
sleep 5

1

Ha già risposto un milione di volte, ma in un altro modo, senza la necessità di dipendenze esterne:

LOCK_FILE="/var/lock/$(basename "$0").pid"
trap "rm -f ${LOCK_FILE}; exit" INT TERM EXIT
if [[ -f $LOCK_FILE && -d /proc/`cat $LOCK_FILE` ]]; then
   // Process already exists
   exit 1
fi
echo $$ > $LOCK_FILE

Ogni volta che scrive l'attuale PID ($$) nel file di blocco e all'avvio dello script verifica se un processo è in esecuzione con l'ultimo PID.


1
Senza la chiamata trap (o almeno una pulizia verso la fine per il caso normale), si ha il bug falso positivo in cui il file di blocco viene lasciato in giro dopo l'ultima esecuzione e il PID è stato riutilizzato da un altro processo in seguito. (E nel peggiore dei casi, è stato dotato di un lungo processo come apache ....)
Philippe Chaintreuil,

1
Sono d'accordo, il mio approccio è imperfetto, ha bisogno di una trappola. Ho aggiornato la mia soluzione. Preferisco ancora non avere dipendenze esterne.
Filidor Wiese,

1

L'uso del blocco del processo è molto più forte e si occupa anche delle uscite ingrate. lock_file rimane aperto fino a quando il processo è in esecuzione. Verrà chiuso (tramite shell) una volta che il processo esiste (anche se viene ucciso). Ho trovato questo molto efficiente:

lock_file=/tmp/`basename $0`.lock

if fuser $lock_file > /dev/null 2>&1; then
    echo "WARNING: Other instance of $(basename $0) running."
    exit 1
fi
exec 3> $lock_file 

1

Uso oneliner all'inizio del copione:

#!/bin/bash

if [[ $(pgrep -afc "$(basename "$0")") -gt "1" ]]; then echo "Another instance of "$0" has already been started!" && exit; fi
.
the_beginning_of_actual_script

È bene vedere la presenza del processo nella memoria (indipendentemente dallo stato del processo); ma fa il lavoro per me.


0

Il percorso del gregge è la strada da percorrere. Pensa a cosa succede quando la sceneggiatura muore improvvisamente. Nel caso flock hai appena perso il gregge, ma questo non è un problema. Inoltre, nota che un trucco malvagio è prendere un gregge sulla sceneggiatura stessa ... ma questo ovviamente ti consente di correre a tutta velocità in problemi di autorizzazione.


0

Veloce e sporco?

#!/bin/sh

if [ -f sometempfile ]
  echo "Already running... will now terminate."
  exit
else
  touch sometempfile
fi

..do what you want here..

rm sometempfile

7
Questo può essere o meno un problema, a seconda di come viene utilizzato, ma esiste una condizione di competizione tra il test per il blocco e la sua creazione, in modo che due script possano essere avviati contemporaneamente. Se uno termina per primo, l'altro rimarrà in esecuzione senza file di blocco.
TimB

3
C News, che mi ha insegnato molto sullo scripting della shell portatile, era solito creare un file lock. $$ e quindi tentare di collegarlo con "lock" - se il collegamento ha avuto successo, hai avuto il blocco, altrimenti hai rimosso il blocco. $$ ed è uscito.
Paul Tomblin,

Questo è davvero un buon modo per farlo, tranne per il fatto che hai ancora la necessità di rimuovere manualmente il file di blocco se qualcosa va storto e il file di blocco non viene eliminato.
Matthew Scharley,

2
Veloce e sporco, è quello che ha chiesto :)
Aupajo,
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.