Suposições:
- Todas as instâncias de script estão sendo executadas na mesma máquina.
- Existe um diretório conhecido onde seu script pode escrever e que nenhum outro programa usará. Este diretório está em um sistema de arquivos "comum" (em particular, não NFS).
Você pode usar operações atômicas, como criação de arquivos ( set -C; (: >foo) 2>/dev/null
), renomeação ( mv
) e exclusão ( rm
) para lidar com bloqueio.
Para notificar um processo, você pode enviar um sinal; no entanto, é problemático localizar os processos de destino: se você armazenar IDs de processo em algum lugar, não pode ter certeza de que os IDs ainda são válidos, eles podem ter sido reutilizados por um processo não relacionado. Uma maneira de sincronizar dois processos é escrever um byte em um pipe; o leitor irá bloquear até que um escritor apareça e vice-versa.
Primeiro, configure o diretório. Crie um arquivo chamado lock
e um pipe nomeado chamado pipe
.
if ! [ -d /script-locking-directory ]; then
# The directory doesn't exist, create and populate it
{
mkdir /script-locking-directory-$$ &&
mkfifo /script-locking-directory-$$/pipe &&
touch /script-locking-directory-$$/lock &&
mv /script-locking-directory-$$ /script-locking-directory
} || {
# An error happened, so clean up
err=$?
rm -r /script-locking-directory-$$
# Exit, unless another instance of the script created the directory
# at the same time as us
if ! [ -d /script-locking-directory ]; then exit $?; fi
}
fi
Implementaremos o bloqueio renomeando o arquivo lock
. Além disso, usaremos um esquema simples para notificar todos os garçons sobre o bloqueio: faça um eco de um byte para um canal e peça a todos os garçons que esperem lendo esse tubo. Aqui está um esquema simples.
take_lock () {
while ! mv lock lock.held 2>/dev/null; do
read <pipe # wait for a write on the pipe
done
}
release_lock () {
mv lock.held lock
read <pipe & # make sure there is a reader on the pipe so we don't block
echo >pipe # notify all readers
}
Este esquema acorda todos os garçons, o que pode ser ineficiente, mas isso não será um problema a menos que haja muita disputa (ou seja, muitos garçons ao mesmo tempo).
O maior problema com o código acima é que, se um porta-trava morre, a trava não será liberada. Como podemos detectar essa situação? Não podemos simplesmente procurar um processo chamado de bloqueio, devido à reutilização do PID. O que podemos fazer é abrir o arquivo de bloqueio no script e verificar se o arquivo de bloqueio está aberto quando uma nova instância de script for iniciada.
break_lock () {
if ! [ -e "lock.held" ]; then return 1; fi
if [ -n "$(fuser lock.held)" ]; then return 1; fi
# If we get this far, the lock holder died
if mv lock.held lock.breaking.$$ 2>/dev/null; then
# Check that someone else didn't break the lock and take it just now
if [ -n "$(fuser lock.breaking.$$)" ]; then
mv lock.breaking.$$ lock.held
return 0
fi
mv lock.breaking.$$ lock
fi
return 0 # whether we did break a lock or not, try taking it again
}
take_lock () {
while ! mv lock lock.taking.$$ 2>/dev/null; do
if break_lock; then continue; fi
read <pipe # wait for a write on the pipe
done
exec 9<lock.taking.$$
mv lock.taking.$$ lock.held
}
release_lock () {
# lock.held might not exist if someone else is trying to break our lock.
# So we try in a loop.
while ! mv lock.held lock.releasing.$$ 2>/dev/null; do :; done
exec 9<&-
mv lock.releasing.$$ lock
read <pipe & # make sure there is a reader on the pipe so we don't block
echo >pipe # notify all readers
}
Essa implementação ainda pode travar se o detentor de bloqueio morrer enquanto outra instância estiver dentro de take_lock
: o bloqueio permanecerá suspenso até que uma terceira instância seja iniciada. Eu também presumo que um script não irá morrer dentro de take_lock
ou release_lock
.
Aviso: eu escrevi o código acima diretamente em um navegador, eu não testei (e muito menos provou que está correto).