Как убедиться, что запускается только один экземпляр 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
NSG
источник
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>одновременно несколько форков и исполняете их .

user1146332
источник
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, если не запущен другой сценарий

Sethu
источник
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 .

schieferstapel
источник
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
arputra
источник
1
Пожалуйста, объясните точно, как жесткая кодировка контрольной суммы является хорошей идеей.
Скотт
не жесткая контрольная сумма, он только создает ключ идентификации вашего скрипта, когда другой экземпляр будет запущен, он будет проверять другой процесс сценария оболочки и сначала отследит файл, если ваш ключ идентификации находится в этом файле, поэтому он означает, что ваш экземпляр уже запущен.
арпутра
ХОРОШО; пожалуйста, отредактируйте свой ответ, чтобы объяснить это. И, в будущем, не публикуйте несколько блоков кода длиной 30 строк, которые выглядят так, как будто они (почти) идентичны, не говоря и не объясняя, чем они отличаются. И не говорите что-то вроде «вы можете жестко закодировать [sic] cksum внутри вашего скрипта», и не продолжайте использовать имена переменных mysumи fsum, когда вы больше не говорите о контрольной сумме.
Скотт
Выглядит интересно, спасибо! И добро пожаловать в unix.stackexchange :)
Тобиас Кинцлер
0

Вы можете использовать это: https://github.com/sayanarijit/pidlock

sudo pip install -U pidlock

pidlock -n sleepy_script -c 'sleep 10'
Ариджит Басу
источник
> Было бы предпочтительным решение, не требующее дополнительных инструментов.
Даг
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, вы можете поместить все основные сценарий.

Ансельмо Бланко Домингес
источник