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


этот вопрос уже есть ответ здесь:

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

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

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

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

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

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

14 82

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]) over 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 `"
LOCKFD=99

# PRIVATE
_lock()             { flock - $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-дисков и диски

# try to run a command, but fail immediately if it's 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

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

короткий пример

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

более длинный пример. Этот пример будет ждать глобальные.замок но тайм-аут после слишком долго.

 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

репост отсюда от @Barry Kelly .

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

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

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

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

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

currsh=
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" для перечисления всех PID текущих запущенных скриптов с именем "$0".

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

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

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

Ноя

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

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

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

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

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

apt install liblockfile-bin

запустить: dotlockfile -l file.lock

.блокировка будет создана.

для разблокировки: dotlockfile -u file.lock

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

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

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

устранение:

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

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

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

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

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

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

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

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

вот наш стандартный бит. Он может восстановиться из сценария, каким-то образом умирая, не очищая его lockfile.

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

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