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

Какой самый простой / лучший способ убедиться, что запущен только один экземпляр данного скрипта - при условии, что это Bash в Linux?

На данный момент я делаю:

ps -C script.name.sh > /dev/null 2>&1 || ./script.name.sh

но у него есть несколько проблем:

  1. это ставит проверку вне сценария
  2. он не позволяет мне запускать один и тот же скрипт из разных учетных записей - что мне иногда хотелось бы.
  3. -C проверяет только первые 14 символов имени процесса

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

14 ответов

Решение

Если сценарий одинаков для всех пользователей, вы можете использовать lockfile подход. Если вы получили блокировку, продолжайте, покажите сообщение и выйдите.

В качестве примера:

[Terminal #1] $ lockfile -r 0 /tmp/the.lock
[Terminal #1] $ 

[Terminal #2] $ lockfile -r 0 /tmp/the.lock
[Terminal #2] lockfile: Sorry, giving up on "/tmp/the.lock"

[Terminal #1] $ rm -f /tmp/the.lock
[Terminal #1] $ 

[Terminal #2] $ lockfile -r 0 /tmp/the.lock
[Terminal #2] $ 

После /tmp/the.lock Ваш скрипт будет единственным, имеющим доступ к выполнению. Когда вы закончите, просто удалите замок. В форме сценария это может выглядеть так:

#!/bin/bash

lockfile -r 0 /tmp/the.lock || exit 1

# Do stuff here

rm -f /tmp/the.lock

Консультативная блокировка использовалась целую вечность и может использоваться в скриптах bash. Я предпочитаю простой flock (от util-linux[-ng]) над lockfile (от procmail). И всегда помните о ловушке на выходе (sigspec == EXIT или же 0перехват определенных сигналов является излишним) в этих скриптах.

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

Вот упомянутый шаблон для вашего удобства.

#!/bin/bash
# SPDX-License-Identifier: MIT

## Copyright (C) 2009 Przemyslaw Pawelczyk <przemoc@gmail.com>
##
## This script is licensed under the terms of the MIT license.
## https://opensource.org/licenses/MIT
#
# Lockable script boilerplate

### HEADER ###

LOCKFILE="/var/lock/`basename $0`"
LOCKFD=99

# PRIVATE
_lock()             { flock -$1 $LOCKFD; }
_no_more_locking()  { _lock u; _lock xn && rm -f $LOCKFILE; }
_prepare_locking()  { eval "exec $LOCKFD>\"$LOCKFILE\""; trap _no_more_locking EXIT; }

# ON START
_prepare_locking

# PUBLIC
exlock_now()        { _lock xn; }  # obtain an exclusive lock immediately or fail
exlock()            { _lock x; }   # obtain an exclusive lock
shlock()            { _lock s; }   # obtain a shared lock
unlock()            { _lock u; }   # drop a lock

### BEGIN OF SCRIPT ###

# Simplest example is avoiding running multiple instances of script.
exlock_now || exit 1

# Remember! Lock file is removed when one of the scripts exits and it is
#           the only script holding the lock or lock is not acquired at all.

Я думаю flock это, наверное, самый простой (и самый запоминающийся) вариант. Я использую его в работе cron для автоматического кодирования DVD и CD

# try to run a command, but fail immediately if it's already running
flock -n /var/lock/myjob.lock   my_bash_command

использование -w для тайм-аутов или опускайте опции, чтобы дождаться снятия блокировки. Наконец, на странице руководства показан хороший пример для нескольких команд:

   (
     flock -n 9 || exit 1
     # ... commands executed under lock ...
   ) 9>/var/lock/mylockfile

Использовать set -o noclobber вариант и попытка перезаписать общий файл.

Краткий пример

if ! (set -o noclobber ; echo > /tmp/global.lock) ; then
    exit 1  # the global.lock already exists
fi
# ...remainder of script...

Более длинный пример. Этот пример будет ожидать global.lock, но время ожидания истечет слишком долго.

 function lockfile_waithold()
 {
    declare -ir time_beg=$(date '+%s')
    declare -ir maxtime=7140  # 7140 s = 1 hour 59 min.

    # waiting up to ${maxtime}s for /tmp/global.lock ...
    while ! \
       (set -o noclobber ; \
        echo -e "DATE:$(date)\nUSER:$(whoami)\nPID:$$" > /tmp/global.lock \ 
       ) 2>/dev/null
    do
        if [ $(( $(date '+%s') - ${time_beg})) -gt ${maxtime} ] ; then
            echo "waited too long for /tmp/global.lock" 1>&2
            return 1
        fi
        sleep 1
    done

    return 0
 }

 function lockfile_release()
 {
    rm -f /tmp/global.lock
 }

 if ! lockfile_waithold ; then
      exit 1
 fi

 # ...remainder of script

 lockfile_release

Репост отсюда @ Барри Келли.

Я не уверен, что есть какое-то надежное решение в одну строку, так что вы можете закончить своим собственным.

Lockfiles несовершенны, но менее, чем при использовании 'ps | grep | grep -v'трубопроводы.

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

. my_script_control.ksh

# Function exits if cannot start due to lockfile or prior running instance.
my_start_me_up lockfile_name;
trap "rm -f $lockfile_name; exit" 0 2 3 15

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

Использование отдельного управляющего сценария означает, что вы можете проверить исправность для крайних случаев: удалить устаревшие файлы журнала, убедиться, что файл блокировки правильно связан с запущенным в данный момент экземпляром сценария, дать возможность завершить запущенный процесс и т. Д. Это также означает, что у вас больше шансов использовать grep на ps вывод успешно. Ps-grep может использоваться для проверки того, что с файлом блокировки связан связанный с ним запущенный процесс. Возможно, вы могли бы назвать свои файлы блокировок каким-либо образом, чтобы включить информацию о процессе: user, pid и т. Д., Который может использоваться при последующем вызове сценария, чтобы определить, все ли еще существует процесс, создавший файл блокировки.

Я нашел это в зависимостях пакета procmail:

apt install liblockfile-bin

Бежать:dotlockfile -l file.lock

file.lock будет создан.

Отпереть:dotlockfile -u file.lock

Используйте это, чтобы перечислить этот файл пакета / команду: dpkg-query -L liblockfile-bin

первый тестовый пример

[[ $(lsof -t $0| wc -l) > 1 ]] && echo "At least one of $0 is running"

второй тестовый пример

currsh=$0
currpid=$$
runpid=$(lsof -t $currsh| paste -s -d " ")
if [[ $runpid == $currpid ]]
then
  sleep 11111111111111111
else
  echo -e "\nPID($runpid)($currpid) ::: At least one of \"$currsh\" is running !!!\n"
  false
  exit 1
fi

объяснение

"lsof -t" для вывода списка всех текущих запущенных скриптов с именем "$0".

Команда "lsof" сделает два преимущества.

  1. Игнорируйте pids, который редактируется редактором, таким как vim, потому что vim редактирует свой файл отображения, такой как ".file.swp".
  2. Игнорировать pids, разветвленные текущими запущенными сценариями оболочки, которые большинство "grep" производных команд не может достичь. Используйте команду "pstree -pH pidnum", чтобы увидеть подробную информацию о текущем статусе разветвления процесса.

Я также рекомендовал бы посмотреть на chpst (часть runit):

chpst -L /tmp/your-lockfile.loc ./script.name.sh

Одноканальное конечное решение:

[ "$(pgrep -fn $0)" -ne "$(pgrep -fo $0)" ] && echo "At least 2 copies of $0 are running"

В дистрибутивах Ubuntu/Debian есть start-stop-daemon инструмент, который предназначен для той же цели, которую вы описываете. Смотрите также /etc/init.d/skeleton, чтобы увидеть, как он используется при написании сценариев запуска / остановки.

- Ной

У меня была та же проблема, и я придумал шаблон, который использует lockfile, pid-файл, содержащий номер идентификатора процесса, и kill -0 $(cat $pid_file) установите флажок, чтобы прерванные сценарии не останавливались при следующем запуске. Это создает папку foobar-$USERID в /tmp, где находятся файлы блокировки и pid.

Вы по-прежнему можете вызывать сценарий и делать другие вещи, пока вы сохраняете эти действия в alertRunningPS,

#!/bin/bash

user_id_num=$(id -u)
pid_file="/tmp/foobar-$user_id_num/foobar-$user_id_num.pid"
lock_file="/tmp/foobar-$user_id_num/running.lock"
ps_id=$$

function alertRunningPS () {
    local PID=$(cat "$pid_file" 2> /dev/null)
    echo "Lockfile present. ps id file: $PID"
    echo "Checking if process is actually running or something left over from crash..."
    if kill -0 $PID 2> /dev/null; then
        echo "Already running, exiting"
        exit 1
    else
        echo "Not running, removing lock and continuing"
        rm -f "$lock_file"
        lockfile -r 0 "$lock_file"
    fi
}

echo "Hello, checking some stuff before locking stuff"

# Lock further operations to one process
mkdir -p /tmp/foobar-$user_id_num
lockfile -r 0 "$lock_file" || alertRunningPS

# Do stuff here
echo -n $ps_id > "$pid_file"
echo "Running stuff in ONE ps"

sleep 30s

rm -f "$lock_file"
rm -f "$pid_file"
exit 0

С вашего сценария:

ps -ef | grep $0 | grep $(whoami)

Я нашел довольно простой способ обработки "одной копии скрипта на систему". Это не позволяет мне запускать несколько копий скрипта из разных учетных записей (в стандартном Linux).

Решение:

В начале сценария я дал:

pidof -s -o '%PPID' -x $( basename $0 ) > /dev/null 2>&1 && exit

Очевидно, pidof прекрасно работает таким образом, что:

  • это не имеет ограничения на имя программы, как ps -C ...
  • это не требует от меня grep -v grep (или что-нибудь подобное)

И это не зависит от файлов блокировок, что для меня является большой победой, потому что ретрансляция на них означает, что вы должны добавить обработку устаревших файлов блокировок - что не очень сложно, но если этого можно избежать - почему бы и нет?

Что касается проверки "одной копии скрипта на работающего пользователя", я написал это, но я не слишком доволен этим:

(
    pidof -s -o '%PPID' -x $( basename $0 ) | tr ' ' '\n'
    ps xo pid= | tr -cd '[0-9\n]'
) | sort | uniq -d

и затем я проверяю его вывод - если он пуст - нет копий скрипта от того же пользователя.

Вот наш стандартный бит. Он может восстановиться после того, как скрипт как-то умирает, не очистив свой файл блокировки.

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

# lock to ensure we don't get two copies of the same job
script_name="myscript.sh"
lock="/var/run/${script_name}.pid"
if [[ -e "${lock}" ]]; then
    pid=$(cat ${lock})
    if [[ -e /proc/${pid} ]]; then
        echo "${script_name}: Process ${pid} is still running, exiting."
        exit 1
    else
        # Clean up previous lock file
        rm -f ${lock}
   fi
fi
trap "rm -f ${lock}; exit $?" INT TERM EXIT
# write $$ (PID) to the lock file
echo "$$" > ${lock}
Другие вопросы по тегам