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

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

Ответы [ 39 ]

5 голосов
/ 10 апреля 2013

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

5 голосов
/ 11 августа 2014

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

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

4 голосов
/ 03 мая 2012

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

#!/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 
4 голосов
/ 23 мая 2009

Ориентируясь на компьютер 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
}
3 голосов
/ 09 октября 2008

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

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

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

3 голосов
/ 20 октября 2016

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

if [[ $(ps axf | awk -v pid=$$ '$1!=pid && $6~/'$(basename $0)'/{print $1}') ]]; then echo "Already running"; exit; fi
2 голосов
/ 15 октября 2012

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

P.S. Я не видел, чтобы Микель уже правильно ответил на вопрос, хотя он не включил команду trap, чтобы уменьшить вероятность того, что файл блокировки останется после остановки сценария с помощью Ctrl-C, например. Так что это полное решение

2 голосов
/ 31 декабря 2013

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

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

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

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

...