Каков наилучший способ обеспечить запуск только одного экземпляра сценария 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]) over lockfile (от procmail). И всегда помните о ловушке на выходе (sigspec == EXIT или 0, захват конкретных сигналов является излишним) в этих сценариях.

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

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

#!/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-дисков и cds

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

более длинный пример. Этот пример будет ждать глобальные.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

Repost отсюда @Barry Kelly .


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

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

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

. 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

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

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


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

[[ $(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 редактирует свой файл отображения, такой как ".файл.swp".
  2. игнорировать pids, разветвленные текущими запущенными сценариями оболочки, которые большинство производных команд "grep не могу этого достичь. Используйте команду" pstree-pH pidnum", чтобы просмотреть сведения о текущем состоянии разветвления процесса.

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

-- Noah


Я бы также рекомендовал посмотреть на 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}