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


Ответы:


109

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


73
Как уже упоминалось в комментарии к другому ответу, у этого есть фатальный недостаток - если другой скрипт запускается между проверкой и эхом, вы тост.
Пол Томблин

1
Трюк с символьными ссылками хорош, но если владельцем файла блокировки является kill -9'd или система падает, все еще существует условие гонки, чтобы прочитать символическую ссылку, заметить, что владелец пропал, а затем удалить его. Я придерживаюсь своего решения.
bmdhacks

9
Атомная проверка и создание доступны в оболочке с помощью flock (1) или lockfile (1). Смотрите другие ответы.
dmckee --- котенок экс-модератора

3
Смотрите мой ответ для портативного способа выполнения атомарной проверки и создания без необходимости полагаться на такие утилиты, как flock или lockfile.
лунат

2
Это не атомарно и поэтому бесполезно. Вам нужен атомарный механизм для тестирования и установки.
К Ричард Пиксли

214

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


11
Что такое 200? В мануле написано «fd», но я не знаю, что это значит.
Чови

4
@chovy "дескриптор файла", целочисленный дескриптор, обозначающий открытый файл.
Alex B

6
Если кому-то еще интересно: синтаксис ( command A ) command Bвызывает подоболочку для command A. Документально подтвержден по адресу tldp.org/LDP/abs/html/subshells.html . Я все еще не уверен в сроках вызова подоболочки и команды B.
Доктор Ян-Филипп Герке

1
Я думаю, что код внутри суб-оболочки должен быть больше похожим: if flock -x -w 10 200; then ...Do stuff...; else echo "Failed to lock file" 1>&2; fiтак что, если происходит тайм-аут (какой-то другой процесс заблокировал файл), этот сценарий не идет вперед и не изменяет файл. Вероятно ... контраргумент: «но если это заняло 10 секунд, а блокировка все еще недоступна, она никогда не будет доступна», предположительно потому, что процесс, удерживающий блокировку, не завершается (возможно, он выполняется под отладчиком?).
Джонатан Леффлер

1
Файл, к которому перенаправлен, является только папкой для действия блокировки, в него не поступает значимых данных. Это exitот части внутри ( ). Когда подпроцесс завершается, блокировка автоматически снимается, потому что процесс не удерживает ее.
клак

158

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

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

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


1
Вы можете комбинировать if ! mkdirдеталь с проверкой, действительно ли процесс с PID, сохраненным (при успешном запуске) внутри lockdir, действительно работает и идентичен сценарию для защиты стандартизации. Это также защитит от повторного использования PID после перезагрузки и даже не потребует fuser.
Тобиас Кинцлер,

4
Это, безусловно, верно, что mkdirне определено, чтобы быть атомарной операцией и как таковой, что «побочный эффект» является деталью реализации файловой системы. Я полностью верю ему, если он говорит, что NFS не реализует это атомарно. Хотя я не подозреваю, что у вас /tmpбудет общий ресурс NFS, и он, вероятно, будет предоставлен fs, который реализует mkdirатомарно.
июня

5
Но есть способ проверить наличие обычного файла и создать его атомарно, если это не так: использовать lnдля создания жесткой ссылки из другого файла. Если у вас есть странные файловые системы, которые не гарантируют этого, вы можете проверить inode нового файла, чтобы узнать, совпадает ли он с исходным файлом.
Хуан Сеспедес

4
Там является «способ проверить , существует ли файл и создать его в одном атомарном действии» - это open(... O_CREAT|O_EXCL). Вам просто нужна подходящая пользовательская программа, например, lockfile-create(in lockfile-progs) или dotlockfile(in liblockfile-bin). И убедитесь, что вы чистите правильно (например trap EXIT), или проверьте на устаревшие замки (например, с --use-pid).
Тоби Спейт

5
«Все подходы, которые проверяют существование« файлов блокировки », несовершенны. Почему? Потому что нет способа проверить, существует ли файл и создать его в одном атомарном действии». - Чтобы сделать его атомарным, это должно быть сделано в на уровне ядра - и это делается на уровне ядра с помощью flock (1) linux.die.net/man/1/flock, который, как видно из даты авторского права человека, существует примерно с 2006 года. Поэтому я сделал понижающее голосование (- 1) ничего личного, просто твердо убежден, что использование инструментов, реализованных в ядре разработчиками ядра, является правильным.
Крейг Хикс

42

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


3
flock()Системный вызов POSIX и не работает для файлов на монтирует NFS.
maxschlepzig

17
Запуск из задания Cron, которое я использую, flock -x -n %lock file% -c "%command%"чтобы убедиться, что выполняется только один экземпляр.
Ryall

О, вместо невообразимой паствы (1) они должны были пойти с чем-то вроде стаи (U). ... у него есть некоторое знакомство с этим. , Кажется, я слышал это раньше или через два.
Кент Крукеберг

Примечательно, что документация flock (2) определяет использование только с файлами, а документация flock (1) определяет использование с файлом или каталогом. В документации flock (1) не указано, как указать разницу при создании, но я предполагаю, что это делается путем добавления окончательного символа "/". В любом случае, если flock (1) может обрабатывать каталоги, а flock (2) - нет, то flock (1) не реализуется только в flock (2).
Крейг Хикс,

27

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

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

Итак, код:

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

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


1
Выполните это несколько раз одновременно (например, "./a.sh & ./a.sh & ./a.sh & ./a.sh & ./a.sh & ./a.sh & ./a.sh & ") и скрипт пропустит несколько раз.
Nippysaurus

7
@Nippysaurus: этот метод блокировки не протекает. То, что вы видели, было завершением начального сценария до запуска всех копий, поэтому другой смог (правильно) получить блокировку. Чтобы избежать этого ложного срабатывания, добавьте sleep 10перед rmdirи попробуйте снова каскадировать - ничто не будет «просачиваться».
Сэр Афон

Другие источники утверждают, что mkdir не является атомарным в некоторых файловых системах, таких как NFS. И, между прочим, я видел случаи, когда в NFS одновременная рекурсивная MKDIR иногда приводила к ошибкам с матричными заданиями Дженкинса. Так что я уверен, что это так. Но mkdir довольно хорош для менее требовательных случаев использования IMO.
Акостадинов

Вы можете использовать опцию Bash'es noclobber с обычными файлами.
Palec

26

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


Какие страницы NFS Illustrated поддерживают утверждение, что mkdir не является атомарным по сравнению с NFS?
maxschlepzig

Спасибо за эту технику. Реализация мьютекса оболочки доступна в моей новой оболочке lib: github.com/Offirmo/offirmo-shell-lib , см. «Мьютекс». Используется, lockfileесли доступно, или откат к этому symlinkметоду, если нет.
Offirmo

Ницца. К сожалению, этот метод не позволяет автоматически удалять устаревшие блокировки.
Ричард Хансен

Для двухэтапной разблокировки ( mv, rm) следует rm -fиспользовать, а не rmв случае гонок двух процессов P1, P2? Например, P1 начинает разблокировку с mv, затем P2 блокирует, затем P2 разблокирует (оба mvи rm), в конце концов P1 пытается rmи терпит неудачу.
Мэтт Уоллис

1
@MattWallis Эта последняя проблема может быть легко устранена путем включения $$в ${f}.deletemeимя файла.
Стефан Маевский

23

Другой вариант - использовать 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

Очень новый подход ... кажется, это один из способов достижения атомарности с использованием файла блокировки, а не каталога блокировки.
Мэтт Колдуэлл

Хороший подход. :-) В ловушке EXIT должно быть указано, какой процесс может очистить файл блокировки. Например: trap 'if [[$ (cat "$ lockfile") == "$$"]]; затем rm "$ lockfile"; fi 'EXIT
Кевин Сейферт

1
Файлы блокировки не являются атомарными по сравнению с NFS. Вот почему люди перешли на использование каталогов блокировки.
К Ричард Пиксли,

20

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

sem --id SCRIPTSINGLETON yourScript

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

sem --id SCRIPTSINGLETON --semaphoretimeout -10 yourScript

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

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

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


Жаль, что люди не хотят принизить бесполезные ответы: это приводит к тому, что новые важные ответы будут похоронены в куче мусора.
Дмитрий Григорьев

4
Нам просто нужно много голосов. Это такой аккуратный и малоизвестный ответ. (Хотя для того, чтобы быть педантичным, ОП хотел быстро и грязно, тогда как это быстро и чисто!) Подробнее на semсмежный вопрос unix.stackexchange.com/a/322200/199525 .
Небольшая

16

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


2
Ваш сценарий слишком многословен, я думаю, он мог бы быть намного короче, но в целом, да, вы должны настроить ловушки, чтобы сделать это правильно. Также я бы добавил SIGHUP.
Моджуба

Это работает хорошо, за исключением того, что он проверяет $ LOCK_DIR, тогда как он удаляет $ __ lockdir. Может быть, я должен предложить при снятии блокировки сделать rm -r $ LOCK_DIR?
Бевада

Спасибо за предложение. Выше был снят код и размещен в виде кода псевдо, так что он будет нуждаться в настройке, основанной на использовании людей. Тем не менее, я намеренно пошел с rmdir в моем случае, так как rmdir безопасно удаляет директории only_, если они пусты. Если люди размещают в них ресурсы, такие как PID-файлы и т. Д., Они должны изменить свою очистку блокировки на более агрессивную rm -r $LOCK_DIRили даже принудительно применить ее по мере необходимости (как я это сделал в особых случаях, например, при хранении относительных файлов с нулями). Приветствия.
Марк Стинсон

Вы проверяли exit 1002?
Жиль Квено

13

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

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

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


Как мне смоделировать это, чтобы проверить это? Есть ли способ запустить скрипт дважды в одной строке и, возможно, получить предупреждение, если он уже запущен?
rubo77 21.09.16

2
Это не работает вообще! Зачем проверять -gt 2? grep не всегда оказывается в результате ps!
rubo77

pgrepне в POSIX. Если вы хотите, чтобы это работало переносимо, вам нужен POSIX psи обработать его вывод.
Палек

На OSX -cне существует, вам придется использовать | wc -l. О сравнении чисел: -gt 1проверяется, так как первый экземпляр видит себя.
Бенджамин Питер

6

Вот подход, который объединяет атомарную блокировку каталогов с проверкой устаревшей блокировки через 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

5

Создать файл блокировки в известном месте и проверить его существование при запуске скрипта? Поместить PID в файл может быть полезно, если кто-то пытается отследить ошибочный экземпляр, препятствующий выполнению сценария.


5

Этот пример объясняется в 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. и сравните с номером. Это сложно и неопределенно


1
Вы можете использовать ln -s, потому что это может создать символическую ссылку только тогда, когда нет файла или символической ссылки, так же, как mkdir. в прошлом многие системные процессы использовали символические ссылки, например init или inetd. synlink сохраняет идентификатор процесса, но на самом деле ничего не указывает. за эти годы это поведение изменилось. процессы используют стада и семафоры.
Znik

5

Опубликованные существующие ответы либо используют утилиту 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.


4

Ориентируясь на компьютер 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
}

Спасибо, попробовал это на cygwin сам, и это прошло простые тесты.
ndemou

4

Если ограничения стада, которые уже были описаны в этом разделе, не являются для вас проблемой, тогда это должно сработать:

#!/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
Просто подумал, что укажу, что -x (блокировка записи) уже установлена ​​по умолчанию.
Келдон Аллейн

и -nбудет exit 1сразу же , если он не может получить блокировку
Anentropic

Спасибо @KeldonAlleyne, я обновил код, чтобы удалить "-x", так как это по умолчанию.
presto8

3

У некоторых юниксов есть lockfileчто очень похоже на уже упомянутое flock.

Из справочной страницы:

lockfile может быть использован для создания одного или нескольких файлов семафоров. Если lock-файл не может создать все указанные файлы (в указанном порядке), он ожидает время сна (по умолчанию 8) секунд и повторяет последний файл, который не удалось. Вы можете указать количество повторных попыток до тех пор, пока ошибка не будет возвращена. Если число повторных попыток равно -1 (по умолчанию, то есть -r-1), lockfile будет повторяться бесконечно.


как мы можем получить lockfileутилиту ??
Offirmo

lockfileраспространяется с procmail. Также есть альтернатива, dotlockfileкоторая идет с liblockfileпакетом. Они оба утверждают, что надежно работают на NFS.
Мистер Бессмертный

3

На самом деле, хотя ответ 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. Так что это полное решение


3

Я хотел покончить с lockfiles, lockdirs, специальными программами блокировки и даже pidofпотому, что он не найден во всех установках Linux. Также хотелось иметь максимально простой код (или, по крайней мере, как можно меньше строк). Простейшее ifутверждение в одну строку:

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

1
Это чувствительно к выводу 'ps', на моей машине (Ubuntu 14.04, / bin / ps из procps-ng версии 3.3.9) команда 'ps axf' печатает символы дерева ascii, которые разрушают номера полей. Это сработало для меня: /bin/ps -a --format pid,cmd | awk -v pid=$$ '/'$(basename $0)'/ { if ($1!=pid) print $1; }'
Qneill

2

Я использую простой подход, который обрабатывает устаревшие файлы блокировки.

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

Я дал тебе +1 за другое решение. Хотя это и не работает ни в AIX (> ps -eo pid, ppid, lstart $$ | tail -1 ps: неверный список с -o.), Но не HP-UX (> ps -eo pid, ppid, lstart $$ | tail -1 ps: недопустимый вариант - o). Спасибо.
Тагар

2

Добавьте эту строку в начале вашего скрипта

[ "${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". Это может быть отменено путем отключения разрешения на запись в вашем скрипте.


1

PID и lockfiles, безусловно, самые надежные. Когда вы пытаетесь запустить программу, она может проверить файл блокировки, который и, если он существует, он может использовать, psчтобы увидеть, работает ли еще процесс. Если это не так, скрипт может запуститься, обновив PID в файле блокировки до своего собственного.


1

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


1

В semaphoric утилита использует flock(как описано выше, например , с помощью presto8) для осуществления подсчета семафора . Это позволяет любое конкретное количество параллельных процессов, которые вы хотите. Мы используем его для ограничения уровня параллелизма различных рабочих процессов очереди.

Это похоже на сем, но гораздо легче. (Полное раскрытие: я написал это после того, как обнаружил, что sem слишком тяжел для наших нужд, и не было простой утилиты для подсчета семафоров.)


1

Пример с 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

1

Ответили уже миллион раз, но по-другому, без необходимости внешних зависимостей:

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.


1
Без вызова прерывания (или, по крайней мере, очистки в конце для обычного случая), вы получаете ложно-положительную ошибку, в которой файл блокировки остается после последнего запуска, а PID повторно используется другим процессом позже. (И в худшем случае это был одаренный длительный процесс, такой как apache ....)
Филипп Chaintreuil

1
Я согласен, мой подход некорректен, ему нужна ловушка. Я обновил свое решение. Я все еще предпочитаю не иметь внешних зависимостей.
Филидор Визе

1

Использование блокировки процесса намного сильнее и заботится о неблаговидных выходах. 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 

1

Я использую 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

Хорошо видеть присутствие процесса в памяти (независимо от состояния процесса); но это делает работу для меня.


0

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


0

Быстро и грязно?

#!/bin/sh

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

..do what you want here..

rm sometempfile

7
Это может или не может быть проблемой, в зависимости от того, как она используется, но существует условие состязания между тестированием блокировки и ее созданием, так что два сценария могут быть запущены одновременно. Если один завершится первым, другой продолжит работу без файла блокировки.
TimB

3
C News, которая многому научила меня в сценариях переносимых оболочек, использовала для блокировки файл. $$, а затем пыталась связать его с помощью «lock» - если ссылка удалась, у вас была блокировка, в противном случае вы сняли блокировку. $$ и вышел.
Пол Томблин

Это действительно хороший способ сделать это, за исключением того, что вы по-прежнему испытываете необходимость удалить файл блокировки вручную, если что-то пойдет не так и файл блокировки не будет удален.
Мэтью Шарли

2
Быстро и грязно, вот что он просил :)
Aupajo
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.