Быстрый и грязный способ убедиться, что одновременно работает только один экземпляр сценария оболочки - PullRequest
166 голосов
/ 09 октября 2008

Какой быстрый и грязный способ убедиться, что в данный момент выполняется только один экземпляр сценария оболочки?

Ответы [ 39 ]

193 голосов
/ 04 октября 2008

Используйте 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, она может быть или не быть доступной.

150 голосов
/ 09 апреля 2009

Все подходы, которые проверяют существование "файлов блокировки", имеют недостатки.

Почему? Потому что нет способа проверить, существует ли файл и создать его в одном атомарном действии. Из-за этого; есть условие гонки: БУДЕТ делать ваши попытки взаимного исключения.

Вместо этого вам нужно использовать 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

Если вы хотите позаботиться об устаревших замках, вам пригодится fuser (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 ) .

100 голосов
/ 09 октября 2008

Вот реализация, которая использует lockfile и отображает в нем 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).

41 голосов
/ 09 октября 2008

Вокруг системного вызова flock (2) есть обертка, которая невообразимо называется flock (1). Это позволяет относительно легко получать эксклюзивные блокировки, не беспокоясь об очистке и т. Д. На странице руководства приведены примеры использования этой функции в сценарии оболочки.

27 голосов
/ 13 октября 2009

Вам нужна атомарная операция, например, flock, иначе это в конечном итоге завершится неудачей.

Но что делать, если стадо недоступно. Ну, есть Mkdir. Это тоже атомарная операция. Только один процесс приведет к успешному выполнению mkdir, все остальные потерпят неудачу.

Итак, код:

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

Вам нужно позаботиться об устаревших блокировках, иначе после сбоя ваш сценарий больше никогда не запустится.

24 голосов
/ 29 ноября 2008

Чтобы обеспечить надежную блокировку, требуется атомарная операция. Многие из вышеперечисленных предложений не атомарны. Предложенная утилита lockfile (1) выглядит многообещающе как man-страница упомянул, что его "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

разблокировать текущий каталог (при условии, что запущенный процесс действительно получил блокировку):

mv lock deleteme && rm deleteme

разблокировать файл (предположительно, запущенный процесс действительно получил блокировку):

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

Удалить также не атомарное, поэтому сначала переименуйте (это атомарно), а затем удалите.

Для вызовов символической ссылки и переименования оба имени файла должны находиться в одной и той же файловой системе. Мое предложение: используйте только простые имена файлов (без путей) и поместите файл и заблокируйте его в том же каталоге.

22 голосов
/ 25 февраля 2011

Другой вариант - использовать параметр оболочки 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
18 голосов
/ 18 мая 2016

Вы можете использовать GNU Parallel для этого, так как он работает как мьютекс, когда вызывается как sem. Итак, в конкретных терминах, вы можете использовать:

sem --id SCRIPTSINGLETON yourScript

Если вы тоже хотите установить тайм-аут, используйте:

sem --id SCRIPTSINGLETON --semaphoretimeout -10 yourScript

Тайм-аут <0 означает выход без выполнения скрипта, если семафор не был выпущен в течение тайм-аута, тайм-аут> 0 означает, что скрипт все равно будет запущен.

Обратите внимание, что вы должны дать ему имя (с --id), иначе оно по умолчанию будет управляющим терминалом.

GNU Parallel - это очень простая установка на большинство платформ Linux / OSX / Unix - это всего лишь скрипт на Perl.

16 голосов
/ 29 октября 2011

Для сценариев оболочки я склонен использовать 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. Устанавливая их в другое значение, я могу заставить свои сценарии и потоки пакетной обработки реагировать соответственно на предыдущее пакетное задание или сценарий.

13 голосов
/ 07 сентября 2013

Действительно быстро и действительно грязно? Эта однострочная строка в верхней части вашего скрипта будет работать:

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

Конечно, просто убедитесь, что имя вашего скрипта уникально. :)

...