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

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

44 ответа

Решение

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

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

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

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

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

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

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

mv lock deleteme && rm deleteme

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

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

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

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

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

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

Итак, код:

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

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

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

sem --id SCRIPTSINGLETON yourScript

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

sem --id SCRIPTSINGLETON --semaphoretimeout -10 yourScript

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

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

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

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

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

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

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

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

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

Если ограничения 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 

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

Существующие ответы, размещенные либо полагаются на утилиту 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.

Просто добавь [ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || : в начале вашего сценария. Это стандартный код от паствы людей. Чтобы понять, как это работает, я написал скрипт и запустил его одновременно с двух консолей:

#!/bin/bash

if [ "${FLOCKER}" != "$0" ]; then
        echo "FLOCKER=$FLOCKER \$0=$0 ($$)"
        exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :
else
        echo "FLOCKER equals \$0 = $FLOCKER ($$)"
fi

sleep 10
echo "Process $$ finished"

Я не до конца осознал, как это работает, но, похоже, он снова запускается, используя себя как файл блокировки. FLOCKER установлен в "$0" просто чтобы установить какое-то разумное разумное значение. || : ничего не делать, если что-то пошло не так.

Кажется, он не работает на Debian 7, но, похоже, снова работает с экспериментальным пакетом util-linux 2.25. Он пишет "flock: ... Text file busy". Это может быть отменено путем отключения разрешения на запись в вашем скрипте.

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

При нацеливании на машину 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
}

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

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

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

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

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

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

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

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

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

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

Этот однострочный ответ от кого-то, связанного с Ask Ubuntu Q & A:

[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :
#     This is useful boilerplate code for shell scripts.  Put it at the top  of
#     the  shell script you want to lock and it'll automatically lock itself on
#     the first run.  If the env var $FLOCKER is not set to  the  shell  script
#     that  is being run, then execute flock and grab an exclusive non-blocking
#     lock (using the script itself as the lock file) before re-execing  itself
#     with  the right arguments.  It also sets the FLOCKER env var to the right
#     value so it doesn't run again.

У меня есть следующие проблемы с существующими ответами:

  • Некоторые ответы пытаются очистить файлы блокировки, а затем иметь дело с устаревшими файлами блокировки, вызванными, например, внезапным сбоем / перезагрузкой. ИМО, это излишне сложно. Пусть файлы блокировки останутся.
  • В некоторых ответах используется сам файл сценария или же для блокировки часто ссылаются на примеры из . Это не удается, когда сценарий заменяется из-за обновления или редактирования, что приводит к открытию следующего запуска и получению блокировки нового файла сценария, даже если другой экземпляр, удерживающий блокировку удаленного файла, все еще работает.
  • В нескольких ответах используется фиксированный файловый дескриптор. Это не идеально. Я не хочу полагаться на то, как это будет себя вести, например, при открытии файла блокировки происходит сбой, но он неправильно обрабатывается и пытается заблокировать несвязанный файловый дескриптор, унаследованный от родительского процесса. Другим случаем сбоя является внедрение оболочки блокировки для стороннего двоичного файла, который не обрабатывает блокировку сам по себе, но фиксированные файловые дескрипторы могут мешать передаче файловых дескрипторов дочерним процессам.
  • Я отклоняю ответы, используя поиск процесса для уже запущенного имени сценария. Для этого есть несколько причин, таких как, помимо прочего, надежность/атомарность, синтаксический анализ вывода и наличие скрипта, который выполняет несколько связанных функций, некоторые из которых не требуют блокировки.

Этот ответ делает:

  • полагаться, потому что оно заставляет ядро ​​​​обеспечивать блокировку ... при условии, что файл блокировки создается атомарно и не заменяется.
  • предполагать и полагаться на то, что файл блокировки хранится в локальной файловой системе, а не в NFS.
  • измените наличие файла блокировки на НЕ означающее ничего о работающем экземпляре. Его роль заключается исключительно в предотвращении создания двумя одновременными экземплярами файла с одинаковым именем и замены другой копии. Файл блокировки не удаляется, он остается и может сохраняться после перезагрузки. Блокировка отображается через не через наличие файла блокировки.
  • предположим оболочку bash, как указано в вопросе.

Это не однострочник, но без комментариев и сообщений об ошибках он достаточно мал:

      #!/bin/bash

LOCKFILE=/var/lock/TODO

set -o noclobber
exec {lockfd}<> "${LOCKFILE}" || exit 1
set +o noclobber # depends on what you need
flock --exclusive --nonblock ${lockfd} || exit 1

Но я предпочитаю комментарии и сообщения об ошибках:

      #!/bin/bash

# TODO Set a lock file name
LOCKFILE=/var/lock/myprogram.lock

# Set noclobber option to ensure lock file is not REPLACED.
set -o noclobber

# Open lock file for R+W on a new file descriptor
# and assign the new file descriptor to "lockfd" variable.
# This does NOT obtain a lock but ensures the file exists and opens it.
exec {lockfd}<> "${LOCKFILE}" || {
  echo "pid=$$ failed to open LOCKFILE='${LOCKFILE}'" 1>&2
  exit 1
}

# TODO!!!! undo/set the desired noclobber value for the remainder of the script
set +o noclobber

# Lock on the allocated file descriptor or fail
# Adjust flock options e.g. --noblock as needed
flock --exclusive --nonblock ${lockfd} || {
  echo "pid=$$ failed to obtain lock fd='${lockfd}' LOCKFILE='${LOCKFILE}'" 1>&2
  exit 1
}

# DO work here
echo "pid=$$ obtained exclusive lock fd='${lockfd}' LOCKFILE='${LOCKFILE}'"

# Can unlock after critical section and do more work after unlocking
#flock -u ${lockfd};
# if unlocking then might as well close lockfd too
#exec {lockfd}<&-

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

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

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

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.

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

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

Приятно видеть наличие процесса в памяти (независимо от статуса процесса); но он выполняет свою работу за меня.

Другие вопросы по тегам