Как убедиться, что запускается только один экземпляр bash-скрипта?


26

Решение, которое не требует дополнительных инструментов, будет предпочтительным.


Как насчет файла блокировки?
— Марко

@Marco Я нашел этот SO-ответ, используя его, но, как указано в комментарии , это может создать условия для гонки
— Тобиас Кинцлер,

3
Это BashFAQ 45 .
— jw013

@ jw013 спасибо! Так что, может быть, что-то вроде ln -s my.pid .lockэтого потребует блокировки (после echo $$ > my.pid) и при ошибке может проверить, действительно ли сохраненный в PID .lockактивный экземпляр скрипта
— Тобиас Кинцлер,

Ответы:


18

Почти как ответ nsg: использовать каталог блокировки . Создание каталогов атомарно под Linux и Unix и * BSD и многими другими ОС.

if mkdir $LOCKDIR
then
    # Do important, exclusive stuff
    if rmdir $LOCKDIR
    then
        echo "Victory is mine"
    else
        echo "Could not remove lock dir" >&2
    fi
else
    # Handle error condition
    ...
fi

Вы можете поместить PID блокирующего sh в файл в каталоге lock для целей отладки, но не попадайтесь в ловушку, думая, что вы можете проверить этот PID, чтобы убедиться, что процесс блокировки все еще выполняется. Многие условия гонки лежат на этом пути.


1
Я хотел бы рассмотреть возможность использования сохраненного PID, чтобы проверить, жив ли экземпляр блокировки. Тем не менее, вот заявление, mkdirкоторое не является атомарным в NFS (что не так для меня, но я думаю, что следует упомянуть об этом, если это правда)
— Тобиас Кинцлер,

Да, во что бы то ни стало, используйте сохраненный PID, чтобы увидеть, выполняется ли еще процесс блокировки, но не пытайтесь делать что-либо кроме записи сообщения. Проверка сохраненного pid, создание нового файла PID и т. Д. Оставляет большое окно для гонок.
— Брюс Эдигер

Хорошо, как сказал Ihunath , lockdir, скорее всего, будет в том, /tmpгде обычно нет общего доступа к NFS, так что все должно быть в порядке.
— Тобиас Кинцлер

Я бы использовал, rm -rfчтобы удалить каталог блокировки. rmdirпотерпит неудачу, если кому-то (не обязательно вам) удалось добавить файл в каталог.
— Чепнер

18

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

#Remove the lock directory
function cleanup {
    if rmdir $LOCKDIR; then
        echo "Finished"
    else
        echo "Failed to remove lock directory '$LOCKDIR'"
        exit 1
    fi
}

if mkdir $LOCKDIR; then
    #Ensure that if we "grabbed a lock", we release it
    #Works for SIGTERM and SIGINT(Ctrl-C)
    trap "cleanup" EXIT

    echo "Acquired lock, running"

    # Processing starts here
else
    echo "Could not create lock directory '$LOCKDIR'"
    exit 1
fi

В качестве альтернативы if ! mkdir "$LOCKDIR"; then handle failure to lock and exit; fi trap and do processing after if-statement.
— Кусалананда

6

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

#!/bin/bash 

me="$(basename "$0")";
running=$(ps h -C "$me" | grep -wv $$ | wc -l);
[[ $running > 1 ]] && exit;

# do stuff below this comment

1
Хороший и / или блестящий. :)
— Spooky

1
Я использовал это условие в течение недели, и в двух случаях это не помешало запуску нового процесса. Я понял, в чем проблема - новый pid является подстрокой старого и скрыт grep -v $$. реальные примеры: старый - 14532, новый - 1453, старый - 28858, новый - 858.
— Naktibalda

Я исправил это, изменив grep -v $$наgrep -v "^${$} "
— Naktibalda

@Naktibalda хороший улов, спасибо! Вы также можете исправить это с помощью grep -wv "^$$"(см. Редактирование).
— Terdon

Спасибо за это обновление. Мой образец иногда терпел неудачу, потому что более короткие pids были оставлены дополненными пробелами.
— Naktibalda

4

Я бы использовал файл блокировки, как упоминал Марко

#!/bin/bash

# Exit if /tmp/lock.file exists
[ -f /tmp/lock.file ] && exit

# Create lock file, sleep 1 sec and verify lock
echo $$ > /tmp/lock.file
sleep 1
[ "x$(cat /tmp/lock.file)" == "x"$$ ] || exit

# Do stuff
sleep 60

# Remove lock file
rm /tmp/lock.file

1
(Я думаю, что вы забыли создать файл блокировки) Как насчет условий гонки ?
— Тобиас Кинцлер,

ops :) Да, условия гонки в моем примере - проблема, я обычно пишу почасовые или ежедневные задания cron, а условия гонки редки.
— НСГ

Они не должны быть актуальны и в моем случае, но об этом нужно помнить. Может быть, использование lsof $0не плохо, либо?
— Тобиас Кинцлер,

Вы можете уменьшить состояние гонки, написав свой $$файл блокировки. Затем sleepна короткий промежуток времени и прочитайте его обратно. Если PID остается вашим, вы успешно получили блокировку. Не требует абсолютно никаких дополнительных инструментов.
— manatwork

1
Я никогда не использовал lsof для этой цели, мне это должно работать. Обратите внимание, что в моей системе lsof работает очень медленно (1-2 секунды), и, скорее всего, для гонок достаточно времени.
— НСГ

3

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

Блокировка вашего скрипта (против параллельного запуска)

В противном случае вы можете проверить psили вызвать lsof <full-path-of-your-script>, так как я бы не назвал их дополнительными инструментами.


Дополнение :

на самом деле я думал сделать это так:

for LINE in `lsof -c <your_script> -F p`; do 
    if [ $$ -gt ${LINE#?} ] ; then
        echo "'$0' is already running" 1>&2
        exit 1;
    fi
done

это гарантирует, что только процесс с самым низким pidпродолжит работу, даже если вы выполняете <your_script>одновременно несколько форков и исполняете их .


1
Спасибо за ссылку, но не могли бы вы включить основные части в свой ответ? В SE распространена политика предотвращения гниения ссылок ... Но чего-то подобного на [[(lsof $0 | wc -l) > 2]] && exitсамом деле может быть достаточно, или это также подвержено условиям гонки?
— Тобиас Кинцлер,

Вы правы, основная часть моего ответа пропущена, и только публикация ссылок довольно хромая. Я добавил свое собственное предложение в ответ.
— user1146332

3

Еще один способ убедиться, что запускается один экземпляр скрипта bash:

#!/bin/bash

# Check if another instance of script is running
pidof -o %PPID -x $0 >/dev/null && echo "ERROR: Script $0 already running" && exit 1

...

pidof -o %PPID -x $0 получает PID существующего сценария, если он уже запущен, или завершается с кодом ошибки 1, если не запущен другой сценарий


3

Хотя вы просили решение без дополнительных инструментов, это мой любимый способ использования flock:

#!/bin/sh

[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :

echo "servus!"
sleep 10

Это происходит из раздела примеров man flock, который дополнительно объясняет:

Это полезный шаблонный код для сценариев оболочки. Поместите его в верхнюю часть сценария оболочки, который вы хотите заблокировать, и он автоматически заблокируется при первом запуске. Если env var $ FLOCKER не установлен для запускаемого сценария оболочки, тогда выполните flock и получите эксклюзивную неблокирующую блокировку (используя сам сценарий в качестве файла блокировки), прежде чем переопределить себя с правильными аргументами. Он также устанавливает FLOCKER env var в правильное значение, чтобы он больше не запускался.

Вопросы для рассмотрения:

  • Требуется flock, пример сценария завершается с ошибкой, если он не может быть найден
  • Не требует дополнительного файла блокировки
  • Может не работать, если скрипт находится в NFS (см. Https://serverfault.com/questions/66919/file-locks-on-an-nfs )

См. Также /programming/185451/quick-and-dirty-way-to-ensure-only-one-instance-of-a-shell-script-is-running-at .


1

Это модифицированная версия Ответа Ансельмо . Идея состоит в том, чтобы создать дескриптор файла только для чтения, используя сам скрипт bash, и использовать его flockдля обработки блокировки.

SCRIPT=`realpath $0`     # get absolute path to the script itself
exec 6< "$SCRIPT"        # open bash script using file descriptor 6
flock -n 6 || { echo "ERROR: script is already running" && exit 1; }   # lock file descriptor 6 OR show error message if script is already running

echo "Run your single instance code here"

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


Вы должны всегда заключать в кавычки все ссылки на переменные оболочки, если у вас нет веских причин не делать этого, и вы уверены, что знаете, что делаете. Так что ты должен делать exec 6< "$SCRIPT".
— Скотт

@ Скотт Я изменил код в соответствии с вашими предложениями. Большое спасибо.
— Джон Доу

1

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

Я не использую trap & lock file, потому что, если мой сервер внезапно отключается, мне нужно вручную удалить файл блокировки после того, как сервер заработает.

Примечание: #! / Bin / bash в первой строке требуется для grep ps

#!/bin/bash

checkinstance(){
   nprog=0
   mysum=$(cksum $0|awk '{print $1}')
   for i in `ps -ef |grep /bin/bash|awk '{print $2}'`;do 
        proc=$(ls -lha /proc/$i/exe 2> /dev/null|grep bash) 
        if [[ $? -eq 0 ]];then 
           cmd=$(strings /proc/$i/cmdline|grep -v bash)
                if [[ $? -eq 0 ]];then 
                   fsum=$(cksum /proc/$i/cwd/$cmd|awk '{print $1}')
                   if [[ $mysum -eq $fsum ]];then
                        nprog=$(($nprog+1))
                   fi
                fi
        fi
   done

   if [[ $nprog -gt 1 ]];then
        echo $0 is already running.
        exit
   fi
}

checkinstance 

#--- run your script bellow 

echo pass
while true;do sleep 1000;done

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

#!/bin/bash

mysum=1174212411

checkinstance(){
   nprog=0
   for i in `ps -ef |grep /bin/bash|awk '{print $2}'`;do 
        proc=$(ls -lha /proc/$i/exe 2> /dev/null|grep bash) 
        if [[ $? -eq 0 ]];then 
           cmd=$(strings /proc/$i/cmdline|grep -v bash)
                if [[ $? -eq 0 ]];then 
                   fsum=$(grep mysum /proc/$i/cwd/$cmd|head -1|awk -F= '{print $2}')
                   if [[ $mysum -eq $fsum ]];then
                        nprog=$(($nprog+1))
                   fi
                fi
        fi
   done

   if [[ $nprog -gt 1 ]];then
        echo $0 is already running.
        exit
   fi
}

checkinstance

#--- run your script bellow

echo pass
while true;do sleep 1000;done

1
Пожалуйста, объясните точно, как жесткая кодировка контрольной суммы является хорошей идеей.
— Скотт

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

ХОРОШО; пожалуйста, отредактируйте свой ответ, чтобы объяснить это. И, в будущем, не публикуйте несколько блоков кода длиной 30 строк, которые выглядят так, как будто они (почти) идентичны, не говоря и не объясняя, чем они отличаются. И не говорите что-то вроде «вы можете жестко закодировать [sic] cksum внутри вашего скрипта», и не продолжайте использовать имена переменных mysumи fsum, когда вы больше не говорите о контрольной сумме.
— Скотт

Выглядит интересно, спасибо! И добро пожаловать в unix.stackexchange :)
— Тобиас Кинцлер


0

Мой код тебе

#!/bin/bash

script_file="$(/bin/readlink -f $0)"
lock_file=${script_file////_}

function executing {
  echo "'${script_file}' already executing"
  exit 1
}

(
  flock -n 9 || executing

  sleep 10

) 9> /var/lock/${lock_file}

На основании man flockтолько улучшения:

  • имя файла блокировки, основанное на полном имени скрипта
  • сообщение executing

Где я положил здесь sleep 10, вы можете поместить все основные сценарий.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.