Ответы:
Вот реализация, которая использует файл блокировки и отображает в нем PID. Это служит защитой, если процесс завершается перед удалением 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}
Хитрость здесь в том, kill -0
что она не доставляет никакого сигнала, а просто проверяет, существует ли процесс с данным PID. Кроме того, вызов trap
гарантирует, что файл блокировки будет удален, даже если ваш процесс убит (кроме kill -9
).
Используйте, flock(1)
чтобы сделать эксклюзивную блокировку области действия для файлового дескриптора. Таким образом, вы можете даже синхронизировать различные части скрипта.
#!/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
Это гарантирует, что код между (
и )
выполняется только одним процессом за раз, и что процесс не будет слишком долго ждать блокировки.
Предостережение: эта конкретная команда является частью util-linux
. Если вы используете операционную систему, отличную от Linux, она может быть или не быть доступной.
( command A ) command B
вызывает подоболочку для command A
. Документально подтвержден по адресу tldp.org/LDP/abs/html/subshells.html . Я все еще не уверен в сроках вызова подоболочки и команды B.
if flock -x -w 10 200; then ...Do stuff...; else echo "Failed to lock file" 1>&2; fi
так что, если происходит тайм-аут (какой-то другой процесс заблокировал файл), этот сценарий не идет вперед и не изменяет файл. Вероятно ... контраргумент: «но если это заняло 10 секунд, а блокировка все еще недоступна, она никогда не будет доступна», предположительно потому, что процесс, удерживающий блокировку, не завершается (возможно, он выполняется под отладчиком?).
exit
от части внутри (
)
. Когда подпроцесс завершается, блокировка автоматически снимается, потому что процесс не удерживает ее.
Все подходы, которые проверяют существование «файлов блокировки», имеют недостатки.
Зачем? Потому что нет способа проверить, существует ли файл и создать его в одном атомарном действии. Из-за этого; есть условие гонки, которое БУДЕТ сделать ваши попытки взаимного исключения разорвать.
Вместо этого вам нужно использовать mkdir
. mkdir
создает каталог, если он еще не существует, и если он существует, он устанавливает код выхода. Что еще более важно, он делает все это в одном атомном действии, что делает его идеальным для этого сценария.
if ! mkdir /tmp/myscript.lock 2>/dev/null; then
echo "Myscript is already running." >&2
exit 1
fi
Для всех деталей, смотрите отличный BashFAQ: http://mywiki.wooledge.org/BashFAQ/045
Если вы хотите позаботиться об устаревших замках, фьюзер (1) пригодится. Единственным недостатком здесь является то, что операция занимает около секунды, поэтому она не мгновенная.
Вот функция, которую я написал однажды, которая решает проблему с помощью fuser:
# 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
}
Вы можете использовать его в скрипте так:
mutex /var/run/myscript.lock || { echo "Already running." >&2; exit 1; }
Если вас не волнует переносимость (эти решения должны работать практически на любой UNIX-системе), Linux fuser (1) предлагает некоторые дополнительные опции, а также flock (1) .
if ! mkdir
деталь с проверкой, действительно ли процесс с PID, сохраненным (при успешном запуске) внутри lockdir, действительно работает и идентичен сценарию для защиты стандартизации. Это также защитит от повторного использования PID после перезагрузки и даже не потребует fuser
.
mkdir
не определено, чтобы быть атомарной операцией и как таковой, что «побочный эффект» является деталью реализации файловой системы. Я полностью верю ему, если он говорит, что NFS не реализует это атомарно. Хотя я не подозреваю, что у вас /tmp
будет общий ресурс NFS, и он, вероятно, будет предоставлен fs, который реализует mkdir
атомарно.
ln
для создания жесткой ссылки из другого файла. Если у вас есть странные файловые системы, которые не гарантируют этого, вы можете проверить inode нового файла, чтобы узнать, совпадает ли он с исходным файлом.
open(... O_CREAT|O_EXCL)
. Вам просто нужна подходящая пользовательская программа, например, lockfile-create
(in lockfile-progs
) или dotlockfile
(in liblockfile-bin
). И убедитесь, что вы чистите правильно (например trap EXIT
), или проверьте на устаревшие замки (например, с --use-pid
).
Вокруг системного вызова flock (2) есть обертка, которая невообразимо называется flock (1). Это позволяет относительно легко получить эксклюзивные блокировки, не беспокоясь об очистке и т. Д. На странице руководства приведены примеры использования этой функции в сценарии оболочки.
flock()
Системный вызов POSIX и не работает для файлов на монтирует NFS.
flock -x -n %lock file% -c "%command%"
чтобы убедиться, что выполняется только один экземпляр.
Вам нужна атомарная операция, например, flock, иначе это в конечном итоге не удастся.
Но что делать, если стадо недоступно. Ну, есть Mkdir. Это тоже атомарная операция. Только один процесс приведет к успешному выполнению mkdir, все остальные потерпят неудачу.
Итак, код:
if mkdir /var/lock/.myscript.exclusivelock
then
# do stuff
:
rmdir /var/lock/.myscript.exclusivelock
fi
Вам нужно позаботиться об устаревших блокировках, иначе после сбоя ваш скрипт никогда не запустится снова.
sleep 10
перед rmdir
и попробуйте снова каскадировать - ничто не будет «просачиваться».
Чтобы сделать блокировку надежной, вам нужна атомарная операция. Многие из вышеперечисленных предложений не являются атомарными. Предложенная утилита lockfile (1) выглядит многообещающе, поскольку на странице руководства упоминается, что она «устойчива к NFS». Если ваша ОС не поддерживает lockfile (1) и ваше решение должно работать на NFS, у вас не так много вариантов ....
NFSv2 имеет две атомарные операции:
В NFSv3 вызов create также является атомарным.
Операции с каталогами НЕ являются атомарными в NFSv2 и NFSv3 (см. Книгу «Иллюстрированный NFS» Брента Каллагана, ISBN 0-201-32570-5; Брент - ветеран NFS в Sun).
Зная это, вы можете реализовать спин-блокировки для файлов и каталогов (в оболочке, а не в PHP):
заблокировать текущий каталог:
while ! ln -s . lock; do :; done
заблокировать файл:
while ! ln -s ${f} ${f}.lock; do :; done
unlock current dir (предположим, что запущенный процесс действительно получил блокировку):
mv lock deleteme && rm deleteme
разблокировать файл (предположим, что запущенный процесс действительно получил блокировку):
mv ${f}.lock ${f}.deleteme && rm ${f}.deleteme
Удалить также не атомарный, поэтому сначала переименуйте (который является атомным), а затем удалить.
Для вызовов symlink и rename оба имени файла должны находиться в одной файловой системе. Мое предложение: использовать только простые имена файлов (без путей) и поместить файл и заблокировать в том же каталоге.
lockfile
если доступно, или откат к этому symlink
методу, если нет.
mv
, rm
) следует rm -f
использовать, а не rm
в случае гонок двух процессов P1, P2? Например, P1 начинает разблокировку с mv
, затем P2 блокирует, затем P2 разблокирует (оба mv
и rm
), в конце концов P1 пытается rm
и терпит неудачу.
$$
в ${f}.deleteme
имя файла.
Другой вариант - использовать noclobber
опцию оболочки при запуске set -C
. Тогда не >
получится, если файл уже существует.
Вкратце:
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
Это заставляет оболочку вызывать:
open(pathname, O_CREAT|O_EXCL)
который атомарно создает файл или терпит неудачу, если файл уже существует.
Согласно комментарию к BashFAQ 045 , это может не сработать ksh88
, но оно работает во всех моих оболочках:
$ 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
Интересно, что pdksh
добавляет O_TRUNC
флаг, но, очевидно, это избыточно:
либо вы создаете пустой файл, либо ничего не делаете.
То, как вы это сделаете, rm
зависит от того, как вы хотите обрабатывать нечистые выходы.
Удалить на чистый выход
Новые запуски терпят неудачу, пока проблема, которая вызвала нечистый выход, не будет решена, и файл блокировки удален вручную.
# acquire lock
# do work (code here may call exit, etc.)
rm "$lockfile"
Удалить на любом выходе
Новые запуски успешны, если сценарий еще не запущен.
trap 'rm "$lockfile"' EXIT
Вы можете использовать GNU Parallel
это как мьютекс при вызове как sem
. Итак, в конкретных терминах, вы можете использовать:
sem --id SCRIPTSINGLETON yourScript
Если вы тоже хотите установить тайм-аут, используйте:
sem --id SCRIPTSINGLETON --semaphoretimeout -10 yourScript
Тайм-аут <0 означает выход без выполнения скрипта, если семафор не был выпущен в течение тайм-аута, тайм-аут> 0 означает, что скрипт все равно будет запущен.
Обратите внимание, что вы должны дать ему имя (с --id
), иначе он по умолчанию будет управляющим терминалом.
GNU Parallel
это очень простая установка на большинстве платформ Linux / OSX / Unix - это всего лишь скрипт на Perl.
sem
смежный вопрос unix.stackexchange.com/a/322200/199525 .
Для сценариев оболочки, я , как правило, идут с mkdir
более , flock
как это делает замки более переносимым.
В любом случае, использования set -e
недостаточно. Это выходит из сценария только в случае сбоя какой-либо команды. Ваши замки все равно останутся позади.
Для правильной очистки блокировки вы действительно должны установить свои ловушки на что-то вроде этого кода psuedo (поднятого, упрощенного и непроверенного, но из активно используемых скриптов):
#=======================================================================
# 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
Вот что будет. Все ловушки будут создавать выход, поэтому функция __sig_exit
всегда будет выполняться (за исключением SIGKILL), которая очищает ваши блокировки.
Примечание: мои значения выхода не являются низкими значениями. Зачем? Различные системы пакетной обработки делают или имеют ожидания от чисел от 0 до 31. Установив их на что-то другое, я могу заставить свои сценарии и потоки пакетов реагировать соответственно на предыдущее пакетное задание или сценарий.
rm -r $LOCK_DIR
или даже принудительно применить ее по мере необходимости (как я это сделал в особых случаях, например, при хранении относительных файлов с нулями). Приветствия.
exit 1002
?
Действительно быстро и действительно грязно? Эта однострочная строка в верхней части вашего скрипта будет работать:
[[ $(pgrep -c "`basename \"$0\"`") -gt 1 ]] && exit
Конечно, просто убедитесь, что имя вашего скрипта уникально. :)
-gt 2
? grep не всегда оказывается в результате ps!
pgrep
не в POSIX. Если вы хотите, чтобы это работало переносимо, вам нужен POSIX ps
и обработать его вывод.
-c
не существует, вам придется использовать | wc -l
. О сравнении чисел: -gt 1
проверяется, так как первый экземпляр видит себя.
Вот подход, который объединяет атомарную блокировку каталогов с проверкой устаревшей блокировки через PID и перезапускает, если устарел. Кроме того, это не зависит от каких-либо нарушений.
#!/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
Создать файл блокировки в известном месте и проверить его существование при запуске скрипта? Поместить PID в файл может быть полезно, если кто-то пытается отследить ошибочный экземпляр, препятствующий выполнению сценария.
Этот пример объясняется в man flock, но он требует некоторых улучшений, потому что мы должны управлять ошибками и кодами выхода:
#!/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
Вы можете использовать другой метод, перечислить процессы, которые я использовал в прошлом. Но это сложнее, чем метод выше. Вы должны перечислить процессы по ps, отфильтровать по его имени, дополнительный фильтр grep -v grep для удаления паразита и, наконец, считать его по grep -c. и сравните с номером. Это сложно и неопределенно
Опубликованные существующие ответы либо используют утилиту CLI, flock
либо неправильно защищают файл блокировки. Утилита flock доступна не во всех системах, отличных от Linux (например, FreeBSD), и не работает должным образом в NFS.
В первые годы системного администрирования и разработки системы мне говорили, что безопасным и относительно переносимым способом создания файла блокировки было создание временного файла с использованием mkemp(3)
или mkemp(1)
, запись идентифицирующей информации во временный файл (т. Е. PID), затем жесткая ссылка. временный файл к файлу блокировки. Если ссылка была успешной, значит, вы успешно получили блокировку.
При использовании блокировок в сценариях оболочки я обычно помещаю obtain_lock()
функцию в общий профиль и затем извлекаю ее из сценариев. Ниже приведен пример моей функции блокировки:
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;
};
Ниже приведен пример использования функции блокировки:
#!/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
Не забудьте звонить clean_up
в любой точке выхода в вашем сценарии.
Я использовал вышеупомянутое в Linux и FreeBSD.
Ориентируясь на компьютер Debian, я считаю lockfile-progs
пакет хорошим решением. procmail
также поставляется с lockfile
инструментом. Однако иногда я застреваю ни с одним из них.
Вот мое решение, которое использует mkdir
атомарность и PID-файл для обнаружения устаревших блокировок. Этот код в настоящее время находится в производстве на установке Cygwin и работает хорошо.
Чтобы использовать его просто позвоните, exclusive_lock_require
когда вам нужно получить эксклюзивный доступ к чему-либо. Необязательный параметр имени блокировки позволяет разделять блокировки между различными сценариями. Также есть две функции более низкого уровня ( exclusive_lock_try
и exclusive_lock_retry
), если вам нужно что-то более сложное.
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
}
Если ограничения стада, которые уже были описаны в этом разделе, не являются для вас проблемой, тогда это должно сработать:
#!/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
-n
будет exit 1
сразу же , если он не может получить блокировку
У некоторых юниксов есть lockfile
что очень похоже на уже упомянутое flock
.
Из справочной страницы:
lockfile может быть использован для создания одного или нескольких файлов семафоров. Если lock-файл не может создать все указанные файлы (в указанном порядке), он ожидает время сна (по умолчанию 8) секунд и повторяет последний файл, который не удалось. Вы можете указать количество повторных попыток до тех пор, пока ошибка не будет возвращена. Если число повторных попыток равно -1 (по умолчанию, то есть -r-1), lockfile будет повторяться бесконечно.
lockfile
утилиту ??
lockfile
распространяется с procmail
. Также есть альтернатива, dotlockfile
которая идет с liblockfile
пакетом. Они оба утверждают, что надежно работают на NFS.
На самом деле, хотя ответ bmdhacks почти хороший, есть небольшой шанс, что второй скрипт запустится после первой проверки файла блокировки и до того, как он его написал. Таким образом, они оба напишут файл блокировки, и они оба будут работать. Вот как заставить это работать наверняка:
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
noclobber
Вариант будет убедиться , что редирект команда потерпит неудачу , если файл уже существует. Таким образом, команда перенаправления на самом деле атомарна - вы пишете и проверяете файл одной командой. Вам не нужно удалять файл блокировки в конце файла - он будет удален ловушкой. Я надеюсь, что это поможет людям, которые будут читать это позже.
PS Я не видел, что Микель уже правильно ответил на вопрос, хотя он не включил команду trap, чтобы уменьшить вероятность того, что файл блокировки останется после остановки сценария, например, с помощью Ctrl-C. Так что это полное решение
Я хотел покончить с lockfiles, lockdirs, специальными программами блокировки и даже pidof
потому, что он не найден во всех установках Linux. Также хотелось иметь максимально простой код (или, по крайней мере, как можно меньше строк). Простейшее if
утверждение в одну строку:
if [[ $(ps axf | awk -v pid=$$ '$1!=pid && $6~/'$(basename $0)'/{print $1}') ]]; then echo "Already running"; exit; fi
/bin/ps -a --format pid,cmd | awk -v pid=$$ '/'$(basename $0)'/ { if ($1!=pid) print $1; }'
Я использую простой подход, который обрабатывает устаревшие файлы блокировки.
Обратите внимание, что некоторые из приведенных выше решений, в которых хранится пид, игнорируют тот факт, что пид может обернуться вокруг. Итак, просто проверить, существует ли допустимый процесс с сохраненным pid, недостаточно, особенно для долго выполняющихся сценариев.
Я использую noclobber, чтобы убедиться, что только один скрипт может открывать и записывать в файл блокировки одновременно. Кроме того, я храню достаточно информации, чтобы однозначно идентифицировать процесс в файле блокировки. Я определяю набор данных, чтобы однозначно идентифицировать процесс, который будет pid, ppid, lstart.
Когда запускается новый скрипт, если он не может создать файл блокировки, он затем проверяет, что процесс, который создал файл блокировки, все еще существует. Если нет, мы предполагаем, что первоначальный процесс умер из-за неуместной смерти и оставил файл устаревшей блокировки. Затем новый сценарий становится владельцем файла блокировки, и снова все в порядке.
Должен работать с несколькими оболочками на разных платформах. Быстро, портативно и просто.
#!/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
Добавьте эту строку в начале вашего скрипта
[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :
Это стандартный код от паствы людей.
Если вы хотите больше регистрации, используйте этот
[ "${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."
Это устанавливает и проверяет блокировки с помощью flock
утилиты. Этот код обнаруживает, запускался ли он впервые, проверяя переменную FLOCKER, если для него не задано имя сценария, затем он пытается рекурсивно запустить сценарий снова с использованием flock и с инициализированной переменной FLOCKER, если FLOCKER установлен правильно, а затем выполнить flock на предыдущей итерации. удалось, и это нормально, чтобы продолжить. Если блокировка занята, она терпит неудачу с настраиваемым кодом выхода.
Кажется, он не работает на Debian 7, но, похоже, снова работает с экспериментальным пакетом util-linux 2.25. Он пишет "flock: ... Text file busy". Это может быть отменено путем отключения разрешения на запись в вашем скрипте.
PID и lockfiles, безусловно, самые надежные. Когда вы пытаетесь запустить программу, она может проверить файл блокировки, который и, если он существует, он может использовать, ps
чтобы увидеть, работает ли еще процесс. Если это не так, скрипт может запуститься, обновив PID в файле блокировки до своего собственного.
Я считаю, что решение bmdhack является наиболее практичным, по крайней мере, для моего случая использования. Использование flock и lockfile основывается на удалении lockfile с использованием rm, когда скрипт завершается, что не всегда может быть гарантировано (например, kill -9).
Я хотел бы изменить одну незначительную вещь в решении bmdhack: он имеет смысл удалить файл блокировки, не заявляя, что это не нужно для безопасной работы этого семафора. Его использование kill -0 гарантирует, что старый файл блокировки для мертвого процесса будет просто проигнорирован / перезаписан.
Поэтому мое упрощенное решение состоит в том, чтобы просто добавить следующее в начало вашего синглтона:
## 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}
Конечно, у этого скрипта все еще есть недостаток, заключающийся в том, что процессы, которые могут запускаться одновременно, имеют опасность гонки, поскольку тест блокировки и операции установки не являются единичным атомарным действием. Но предлагаемое решение для этого с помощью lhunath для использования mkdir имеет недостаток, заключающийся в том, что убитый скрипт может оставить каталог, таким образом предотвращая запуск других экземпляров.
В semaphoric утилита использует flock
(как описано выше, например , с помощью presto8) для осуществления подсчета семафора . Это позволяет любое конкретное количество параллельных процессов, которые вы хотите. Мы используем его для ограничения уровня параллелизма различных рабочих процессов очереди.
Это похоже на сем, но гораздо легче. (Полное раскрытие: я написал это после того, как обнаружил, что sem слишком тяжел для наших нужд, и не было простой утилиты для подсчета семафоров.)
Пример с flock (1), но без подоболочки. Файл flock () ed / tmp / foo никогда не удаляется, но это не имеет значения, так как он получает flock () и 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
Ответили уже миллион раз, но по-другому, без необходимости внешних зависимостей:
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
Каждый раз он записывает текущий PID ($$) в файл блокировки и при запуске скрипта проверяет, запущен ли процесс с последним PID.
Использование блокировки процесса намного сильнее и заботится о неблаговидных выходах. lock_file остается открытым, пока процесс запущен. Он будет закрыт (оболочкой), как только процесс будет существовать (даже если его убьют). Я нашел это очень эффективным:
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
Я использую oneliner @ в самом начале сценария:
#!/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
Хорошо видеть присутствие процесса в памяти (независимо от состояния процесса); но это делает работу для меня.
Путь стада - путь. Подумайте о том, что происходит, когда сценарий внезапно умирает. В случае с стадом вы просто теряете стадо, но это не проблема. Кроме того, обратите внимание, что злой трюк состоит в том, чтобы наброситься на сам сценарий ... но это, конечно, позволяет вам полностью справиться с проблемами разрешения.
Быстро и грязно?
#!/bin/sh
if [ -f sometempfile ]
echo "Already running... will now terminate."
exit
else
touch sometempfile
fi
..do what you want here..
rm sometempfile