Відповіді:
Ось реалізація, яка використовує файл блокування та повторює 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
Це гарантує, що код між (
і )
запускається лише одним процесом за один раз, і процес не чекає занадто довго для блокування.
Caveat: ця команда є частиною util-linux
. Якщо ви працюєте з операційною системою, відмінною від Linux, вона може бути, а може бути і недоступною.
( command A ) command B
викликає нижню частину для command A
. Документовано на tldp.org/LDP/abs/html/subshells.html . Я до сих пір не впевнений у термінах виклику
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) . Єдиним недоліком тут є те, що операція займає близько секунди, тому вона не є миттєвою.
Ось функція, яку я написав одного разу, яка вирішує проблему за допомогою термоблока:
# 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 (1) пропонує деякі додаткові параметри, а також є flock (1) .
if ! mkdir
частину, перевіряючи, чи процес із збереженим PID (при успішному запуску) всередині lockdir насправді працює та ідентичний сценарію для захисту stalenes. Це також захистить від повторного використання PID після перезавантаження і навіть не вимагатиме fuser
.
mkdir
це не визначено як атомна операція, і як таке, що "побічний ефект" є деталізацією реалізації файлової системи. Я йому повністю вірю, якщо він каже, що NFS не реалізує це атомно. Хоча я не підозрюю, що ваша участь /tmp
буде NFS, і, швидше за все, вона буде надана fs, який реалізує mkdir
атомно.
ln
для створення жорсткого посилання з іншого файлу. Якщо у вас є дивні файлові системи, які не гарантують цього, ви можете перевірити вкладення нового файлу згодом, щоб побачити, чи він збігається з оригінальним файлом.
open(... O_CREAT|O_EXCL)
. Для цього вам просто потрібна відповідна програма користувача, наприклад lockfile-create
(in lockfile-progs
) або dotlockfile
(in liblockfile-bin
). І переконайтеся, що ви правильно чистили (наприклад trap EXIT
), або перевіряли наявність замку (наприклад, з --use-pid
).
Навколо системного виклику зграї (2) є обгортка, яку немислено називають зграєю (1). Це дозволяє порівняно легко надійно отримати ексклюзивні блокування, не турбуючись про очищення тощо. На сторінці man є приклади, як їх використовувати в сценарії оболонки.
flock()
Системний виклик POSIX і не працює для файлів на монтує NFS.
flock -x -n %lock file% -c "%command%"
щоб переконатися, що коли-небудь виконується лише один екземпляр.
Вам потрібна атомна операція, як зграя, інакше це врешті вийде з ладу.
Але що робити, якщо зграї немає. Ну є 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 виклик створення також є атомним.
Операції з каталогом НЕ є атомними під NFSv2 та NFSv3 (будь ласка, зверніться до книги "NFS Illustrated" Brent Callaghan, ISBN 0-201-32570-5; Brent - ветеран NFS у Sun).
Знаючи це, ви можете реалізувати спін-блокування для файлів і каталогів (в оболонці, а не PHP):
заблокувати поточний реж:
while ! ln -s . lock; do :; done
заблокувати файл:
while ! ln -s ${f} ${f}.lock; do :; done
розблокувати поточний dir (припущення, що запущений процес дійсно придбав замок):
mv lock deleteme && rm deleteme
розблокувати файл (припущення, що запущений процес дійсно придбав замок):
mv ${f}.lock ${f}.deleteme && rm ${f}.deleteme
Видалити також не є атомним, тому спочатку перейменуйте (яке є атомним), а потім видаліть.
Для викликів символьного посилання та перейменування обидві назви файлів мають перебувати в одній файловій системі. Моя пропозиція: використовуйте лише прості назви файлів (без шляхів) і вкладайте файл і блокуйте в один і той же каталог.
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
для цього, оскільки він працює як mutex, коли його називають 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
Цей приклад пояснюється у зграї людини, але вона потребує певних зусиль, тому що ми повинні керувати помилками та вихідними кодами:
#!/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.
У перші дні менеджменту системи та розвитку системи мені сказали, що безпечним і відносно портативним методом створення файлу блокування було створення файлу temp, використовуючи 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
для atomic-ness та 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
}
Якщо обмеження flock, які вже описані в іншому місці цього потоку, для вас не є проблемою, то це повинно працювати:
#!/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
розповсюджується с 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
The noclobber
параметр переконається, що команда переадресації не вдасться, якщо файл вже існує. Отже команда переадресації насправді атомна - ви пишете та перевіряєте файл однією командою. Вам не потрібно видаляти файл блокування в кінці файлу - він буде видалений пасткою. Я сподіваюся, що це допомагає людям, які прочитають його згодом.
PS Я не бачив, що Мікель вже відповів правильно на питання, хоча він не включав команду trap, щоб зменшити шанс блокування файлу після залишення сценарію, наприклад, Ctrl-C. Тож це повне рішення
Мені хотілося покінчити з замками файлів, замками, спеціальними програмами блокування, і навіть 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 встановлено правильно, то злітається на попередній ітерації вдалося, і це нормально. Якщо блокування зайняте, воно виходить з ладу з настроюваним кодом виходу.
Схоже, він не працює на Debian 7, але, здається, знову працює з експериментальним пакетом util-linux 2.25. На ньому написано "flock: ... Текстовий файл зайнятий". Це може бути відмінено, відключивши дозвіл на написання сценарію.
PID та lockfiles, безумовно, є найбільш надійними. При спробі запустити програму вона може перевірити наявність файлу блокування, і якщо він існує, він може використовувати, ps
щоб перевірити, чи процес все ще працює. Якщо це не так, скрипт може запуститися, оновивши PID у файлі блокування до власного.
Я вважаю, що рішення bmdhack є найбільш практичним, принаймні для мого використання. Використовуючи flock та lockfile, розраховуйте на видалення файлу lock за допомогою 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) для здійснення підрахунку семафора . Це дозволяє будь-яку конкретну кількість одночасних процесів, які ви хочете. Ми використовуємо це для обмеження рівня сумісності різних процесів робочих черг.
Це як пів, але набагато легшої ваги. (Повне розкриття: я написав це після того, як виявив, що сема була занадто важкою для наших потреб, і не було простої утиліти для підрахунку семафору.)
Приклад з flock (1), але без допоміжних оболонок. flock () ed file / 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