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

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

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

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

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

  • он помещает проверку вне script
  • он не позволяет мне запускать те же script из отдельных учетных записей, которые мне бы хотелось иногда.
  • -C проверяет только первые 14 символов имени процесса

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

Ответ 1

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

#!/bin/bash

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

# Do stuff here

rm -f /tmp/the.lock

Ответ 2

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

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

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

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

## Copyright (C) 2009 Przemyslaw Pawelczyk <[email protected]>
##
## 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.

Ответ 3

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

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

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

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

Ответ 4

Используйте параметр set -o noclobber и 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 но время ожидания global.lock слишком долго.

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

    # poll for lock file up to ${time_max}s
    # put debugging info in lock file in case of issues ...
    while ! \
       (set -o noclobber ; \
        echo -e "DATE:$(date)\nUSER:$(whoami)\nPID:$$" > /tmp/global.lock \ 
       ) 2>/dev/null
    do
        if [ $(($(date '+%s') - ${time_beg})) -gt ${time_max} ] ; then
            echo "Error: waited too long for lock file /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
 trap lockfile_release EXIT

 # ... remainder of script ...


(Это похоже на этот пост @Барри Келли, который был замечен позже.)

Ответ 5

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

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

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

. 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

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

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

Ответ 6

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

[[ $(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 сделает два преимущества.

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

Ответ 7

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

- Ной

Ответ 8

Я также рекомендую посмотреть chpst (часть runit):

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

Ответ 9

Решение для одной строки:

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

Ответ 10

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

apt install liblockfile-bin

Для запуска: dotlockfile -l file.lock

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

Чтобы разблокировать: dotlockfile -u file.lock

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

Ответ 11

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

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

Ответ 12

с помощью script:

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

Ответ 13

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

Решение:

В начале script я дал:

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

По-видимому pidof отлично работает так:

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

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

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

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

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

Ответ 14

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

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

# 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}