Як переконатися, що працює лише один екземпляр bash-скрипту?


26

Краще рішення, яке не потребує додаткових інструментів.


А що з файлом блокування?
Марко

@Marco Я знайшов цю відповідь, використовуючи це, але, як зазначено в коментарі , це може створити умову гонки
Tobias Kienzler

3
Це BashFAQ 45 .
jw013

@ jw013 дякую! Тож, можливо, щось подібне ln -s my.pid .lockбуде вимагати блокування (супроводжується цим echo $$ > my.pid), і при відмові можна перевірити, чи PID, що зберігається, .lockє дійсно активним екземпляром сценарію
Tobias Kienzler

Відповіді:


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 у файл у каталозі блокування для налагодження, але не потрапляйте у пастку мислення, ви можете перевірити PID, щоб перевірити, чи все-таки виконується процес блокування. На цьому шляху лежать багато умов перегонів.


1
Я б міг використати збережений PID, щоб перевірити, чи зберігається екземпляр блокування. Однак, ось твердження, яке mkdirне є атомним щодо NFS (що для мене не так, але я думаю, слід згадати про це, якщо це правда)
Tobias Kienzler

Так, будь-якими способами використовуйте збережений PID, щоб побачити, чи все-таки виконується процес блокування, але не намагайтеся робити щось, окрім журналу повідомлення. Робота з перевірки збереженого pid, створення нового PID-файлу тощо, залишає велике вікно для перегонів.
Брюс Едігер

Добре, як заявив Іхунат , 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.
Kusalananda

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
Я використовував цю умову протягом тижня, і 2 рази це не завадило запустити новий процес. Я зрозумів, в чому проблема - новий pid є підрядком старого і приховується grep -v $$. реальні приклади: старий - 14532, новий - 1453, старий - 28858, новий - 858.
Naktibalda

Я виправив це, змінивши grep -v $$наgrep -v "^${$} "
Naktibalda

@Naktibalda гарний улов, дякую! Ви також можете це виправити grep -wv "^$$"(див. Редагування).
тердон

Дякуємо за це оновлення Моя картина періодично виходила з ладу, тому що коротші припуски залишалися пробілами.
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 :) Так, умови мотоциклів є проблемою на моєму прикладі, я зазвичай пишу погодинну або щоденну роботу з кронами, а умови гонки рідкісні.
nsg

Вони також не повинні бути актуальними в моєму випадку, але це потрібно пам’ятати. Можливо, і використання lsof $0не погано?
Тобіас Кіенцлер

Ви можете зменшити умову гонки, записавши свій $$файл у замок. Потім sleepза короткий проміжок часу і прочитайте його назад. Якщо PID все ще залишається вашим, ви успішно придбали замок. Не потребує абсолютно ніяких додаткових інструментів.
манатура

1
Я ніколи не використовував lsof для цієї мети, я це повинен працювати. Зауважте, що lsof дійсно повільний у моїй системі (1-2 сек), і, швидше за все, є багато часу для перегонів.
nsg

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насправді може бути достатньо, чи це також схильне до перегонів?
Тобіас Кіенцлер

Ви праві, суттєва частина моєї відповіді пропала, і лише розміщення посилань є досить кульгавим. Я додав власну пропозицію до відповіді.
користувач1146332

3

Ще один спосіб переконатися, що працює один екземпляр bash script:

#!/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".
Скотт

@Scott Я змінив код відповідно до ваших пропозицій. Велике дякую.
Джон Доу

1

Я використовую cksum, щоб перевірити, чи мій сценарій справді працює єдиним екземпляром, навіть я змінюю ім'я файлу та шлях до файлу .

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

Примітка: #! / 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.