Правильне блокування у скриптах оболонки?


66

Іноді доводиться переконатися, що одночасно працює лише один екземпляр скрипта оболонки.

Наприклад, завдання cron, яке виконується через crond, що не забезпечує блокування самостійно (наприклад, за замовчуванням Solaris crond).

Поширений зразок для здійснення блокування - такий код:

#!/bin/sh
LOCK=/var/tmp/mylock
if [ -f $LOCK ]; then            # 'test' -> race begin
  echo Job is already running\!
  exit 6
fi
touch $LOCK                      # 'set'  -> race end
# do some work
rm $LOCK

Звичайно, такий код має перегони. Існує часове вікно, коли виконання двох примірників може просуватися за рядком 3, перш ніж один зможе торкнутися $LOCKфайлу.

Для роботи з клоном це зазвичай не є проблемою, оскільки у вас є інтервал хвилин між двома викликами.

Але все може піти не так - наприклад, коли файл замка знаходиться на сервері NFS - це зависає. У такому випадку декілька завдань Cron можуть блокуватись на лінії 3 та чергувати. Якщо сервер NFS знову активний, у вас громить стадо паралельно запущених завдань.

Шукаючи в Інтернеті, я знайшов інструмент блокування інструменту, який здається хорошим рішенням цієї проблеми. З ним ви запускаєте сценарій, який потребує блокування таким чином:

$ lockrun --lockfile=/var/tmp/mylock myscript.sh

Ви можете покласти це в обгортку або використовувати його з вашого кронтабу.

Він використовує lockf()(POSIX), якщо він доступний, і повертається до flock()(BSD). І lockf()підтримка через NFS повинна бути відносно поширеною.

Чи є альтернативи lockrun?

Що з іншими демонами кронів? Чи існують поширені манжети, які підтримують блокування надійним способом? Швидкий погляд на підручну сторінку Vixie Crond (за замовчуванням у системах Debian / Ubuntu) нічого не свідчить про блокування.

Було б гарною ідеєю включити такий інструмент, як lockrunу coreutils ?

На мій погляд , він реалізує тему , дуже схожий на timeout, niceі друзів.


4
Дотично та на користь інших, хто може вважати ваш початковий шаблон Good Enough (tm), що код оболонки, можливо, повинен захопити термін, щоб видалити його файл запису при killредагуванні; і, здається, є хорошою практикою зберігати власний pid у файлі блокування, а не просто торкатися його.
Ульріх Шварц


@Shawn, насправді, не згадує crond та NFS.
maxschlepzig

пов'язані питання про SO: stackoverflow.com/questions/185451 / ...
maxschlepzig

1
@Ulrich дуже запізніло, зберігання PID у файлі блокування NFS додає дуже мало значення. Навіть додавання імені хоста все ще не допомагає перевірити процес живого процесу
roaima

Відповіді:


45

Ось ще один спосіб зробити блокування в скрипті оболонки, який може запобігти умові гонки, описаному вище, де два завдання можуть обидва проходити рядок 3. Цей noclobberпараметр буде працювати в ksh та bash. Не використовуйте, set noclobberтому що ви не повинні писати сценарії в csh / tcsh. ;)

lockfile=/var/tmp/mylock

if ( set -o noclobber; echo "$$" > "$lockfile") 2> /dev/null; then

        trap 'rm -f "$lockfile"; exit $?' INT TERM EXIT

        # do stuff here

        # clean up after yourself, and release your trap
        rm -f "$lockfile"
        trap - INT TERM EXIT
else
        echo "Lock Exists: $lockfile owned by $(cat $lockfile)"
fi

YMMV з блокуванням на NFS (ви знаєте, коли сервери NFS недоступні), але в цілому він набагато надійніший, ніж раніше. (10 років тому)

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

Я не маю досвіду роботи з lockrun, але створення попередньо встановленої обстановки блокування перед запуском сценарію може допомогти. А може і не. Ви просто встановлюєте тест для lockfile поза вашим сценарієм в обгортці, і теоретично, чи не могли б ви просто потрапити в один і той же стан гонки, якби два завдання були викликані lockrun точно в один і той же час, так само, як і з "внутрішнім" рішення сценарію?

Блокування файлів у значній мірі шанує поведінку системи, і будь-які сценарії, які не перевіряють існування блокування файлів до запуску, будуть робити все, що вони будуть робити. Просто встановивши тест lockfile та правильну поведінку, ви вирішите 99% потенційних проблем, якщо не 100%.

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


НЕДОСТАВИТИ ЗНІМ - 2016-05-06 (якщо ви використовуєте KSH88)


Базуйтесь на коментарі @Clint Pachl нижче, якщо ви використовуєте ksh88, використовуйте mkdirзамість noclobber. Це здебільшого пом'якшує потенційний стан гонки, але не повністю обмежує його (хоча ризик є незначним). Для отримання додаткової інформації читайте посилання, яке Клінт розмістив нижче .

lockdir=/var/tmp/mylock
pidfile=/var/tmp/mylock/pid

if ( mkdir ${lockdir} ) 2> /dev/null; then
        echo $$ > $pidfile
        trap 'rm -rf "$lockdir"; exit $?' INT TERM EXIT
        # do stuff here

        # clean up after yourself, and release your trap
        rm -rf "$lockdir"
        trap - INT TERM EXIT
else
        echo "Lock Exists: $lockdir owned by $(cat $pidfile)"
fi

І, як додаткову перевагу, якщо вам потрібно створити tmpfiles у своєму скрипті, ви можете використовувати lockdirкаталог для них, знаючи, що вони будуть очищені, коли сценарій закінчиться.

Для більш сучасного башу повинен бути придатний метод ноклобер вгорі.


1
Ні, із lockrun у вас не виникає проблем - коли сервер NFS зависне, усі виклики блокування будуть зависати (принаймні) у lockf()системному виклику - коли це резервне копіювання, усі процеси відновляються, але лише один процес виграє блокування. Немає стану гонки. Я часто не стикаюся з такими проблемами із кроновими роботами - навпаки, - але це проблема, коли вона потрапляє у вас, вона має потенціал створити багато болю.
maxschlepzig

1
Я прийняв цю відповідь, оскільки метод безпечний і поки що найелегантніший. Я пропоную невеликий варіант: set -o noclobber && echo "$$" > "$lockfile"отримати безпечний відкат, коли оболонка не підтримує опцію noclobber.
maxschlepzig

3
Хороша відповідь, але ви також повинні "вбити -0" значення в lockfile, щоб переконатися, що процес, який створив блокування, все ще існує.
Найджел Хорн

1
Цей noclobberваріант може бути схильний до перегонів. Дивіться на mywiki.wooledge.org/BashFAQ/045 дещо для роздумів.
Клінт Пахл

2
Примітка: використання noclobber(або -C) в ksh88 не працює, оскільки ksh88 не використовується O_EXCLдля noclobber. Якщо ви працюєте з новою оболонкою, можливо, вам все в порядку ...
jrw32982

14

Я вважаю за краще використовувати жорсткі посилання.

lockfile=/var/lock/mylock
tmpfile=${lockfile}.$$
echo $$ > $tmpfile
if ln $tmpfile $lockfile 2>&-; then
    echo locked
else
    echo locked by $(<$lockfile)
    rm $tmpfile
    exit
fi
trap "rm ${tmpfile} ${lockfile}" 0 1 2 3 15
# do what you need to

Жорсткі посилання є атомними над NFS і здебільшого mkdir також . Використовують mkdir(2)або link(2)приблизно однакові на практичному рівні; Я просто вважаю за краще використовувати жорсткі посилання, тому що більше реалізацій NFS дозволяло атомні жорсткі посилання, ніж атомні mkdir. З сучасними випусками NFS вам не доведеться турбуватися, використовуючи будь-який.


12

Я розумію, що mkdirце атомно, тому, можливо:

lockdir=/var/tmp/myapp
if mkdir $lockdir; then
  # this is a new instance, store the pid
  echo $$ > $lockdir/PID
else
  echo Job is already running, pid $(<$lockdir/PID) >&2
  exit 6
fi

# then set traps to cleanup upon script termination 
# ref http://www.shelldorado.com/goodcoding/tempfiles.html
trap 'rm -r "$lockdir" >/dev/null 2>&1' 0
trap "exit 2" 1 2 3 13 15

Гаразд, але я не міг знайти інформацію, чи mkdir()над NFS (> = 3) стандартизовано атомне.
maxschlepzig

2
@maxschlepzig RFC 1813 прямо не закликає mkdirбути атомним (це робиться для rename). На практиці відомо, що деякі реалізації не є. Пов’язано: цікава нитка, включаючи внесок автора арх ГНУ .
Жиль

8

Найпростіший спосіб - це використання, що lockfileйде разом із procmailпакетом.

LOCKFILE="/tmp/mylockfile.lock"
# try once to get the lock else exit
lockfile -r 0 "$LOCKFILE" || exit 0

# here the actual job

rm -f "$LOCKFILE"

5

semщо є частиною parallelінструментів GNU, можливо, саме те, що ви шукаєте:

sem [--fg] [--id <id>] [--semaphoretimeout <secs>] [-j <num>] [--wait] command

А саме:

sem --id my_semaphore --fg "echo 1 ; date ; sleep 3" &
sem --id my_semaphore --fg "echo 2 ; date ; sleep 3" &
sem --id my_semaphore --fg "echo 3 ; date ; sleep 3" &

виведення:

1
Thu 10 Nov 00:26:21 UTC 2016
2
Thu 10 Nov 00:26:24 UTC 2016
3
Thu 10 Nov 00:26:28 UTC 2016

Зверніть увагу, що замовлення не гарантується. Також вихід не відображається, поки він не закінчиться (дратує!). Але навіть так, я знаю найбільш стислий спосіб захистити від одночасного виконання, не турбуючись про замкові файли та повторні спроби та очищення.


Чи semзбивається ручка, що пропонується ручкою в середині виконання?
roaima

2

Я використовую dtach.

$ dtach -n /tmp/socket long_running_task ; echo $?
0
$ dtach -n /tmp/socket long_running_task ; echo $?
dtach: /tmp/socket: Address already in use
1

1

Я використовую інструмент командного рядка "flock" для управління замками в моїх скриптах bash, як описано тут і тут . Я використовував цей простий метод з manckpage flock, щоб запустити кілька команд в нижній частині ...

   (
     flock -n 9
     # ... commands executed under lock ...
   ) 9>/var/lock/mylockfile

У цьому прикладі він не працює з кодом виходу 1, якщо він не може придбати файл блокування. Але flock також може бути використаний способами, які не вимагають виконання команд у підколонці :-)


3
flock()Системний виклик не працює через NFS .
maxschlepzig

1
BSD має подібний інструмент "lockf".
сумнівним

2
@dubiousjim, BSD lockf також дзвонить flock()і тому є проблематичним для NFS. Btw, тим часом flock () в Linux тепер відновлюється, fcntl()коли файл розташований на кріпленні NFS, таким чином, у середовищі NFS, що працює лише в Linux, flock()зараз працює над NFS.
maxschlepzig

1

Не використовуйте файл.

Якщо ваш сценарій виконаний так, наприклад:

bash my_script

Ви можете визначити, чи працює він за допомогою:

running_proc=$(ps -C bash -o pid=,cmd= | grep my_script);
if [[ "$running_proc" != "$$ bash my_script" ]]; do 
  echo Already locked
  exit 6
fi

Гм, код перевірки ps працює зсередини my_script? У випадку, якщо працює інший примірник - чи не running_procмістять двох відповідних рядків? Мені подобається ідея, але звичайно - ви отримаєте помилкові результати, коли інший користувач запустить сценарій з такою ж назвою ...
maxschlepzig

3
Він також включає умову перегонів: якщо два екземпляри паралельно виконують перший рядок, жоден не отримує «замок», а обидва виходять зі статусом 6. Це буде певним взаємним голодуванням . Btw, я не впевнений, чому ти використовуєш $!замість $$свого прикладу.
maxschlepzig

@maxschlepzig дійсно вибачте за неправильний $! vs. $$
frogstarr78

@maxschlepzig для обробки декількох користувачів, що виконують сценарій, додайте euser = до аргументу -o.
frogstarr78

@maxschlepzig для запобігання декількох рядків ви також можете змінити аргументи на grep або додаткові "фільтри" (наприклад grep -v $$). В основному я намагався запропонувати інший підхід до проблеми.
frogstarr78

1

Для фактичного використання слід скористатись голосовою відповіддю .

Однак я хочу обговорити деякі різні зламані та напівзапрацьовані підходи із використанням psбагатьох застережень, оскільки я постійно бачу, як люди ними користуються.

Ця відповідь справді є відповіддю на питання "Чому б не використовувати psта grepне обробляти замовлення в оболонці?"

Порушений підхід №1

По-перше, підхід, поданий в іншій відповіді, який має декілька оновлень, незважаючи на те, що він не (і ніколи не міг) працювати і явно ніколи не перевірявся:

running_proc=$(ps -C bash -o pid=,cmd= | grep my_script);
if [[ "$running_proc" != "$$ bash my_script" ]]; do 
  echo Already locked
  exit 6
fi

Давайте виправимо синтаксичні помилки та зламані psаргументи та отримаємо:

running_proc=$(ps -C bash -o pid,cmd | grep "$0");
echo "$running_proc"
if [[ "$running_proc" != "$$ bash $0" ]]; then
  echo Already locked
  exit 6
fi

Цей сценарій завжди буде виходити з 6, кожен раз, незалежно від того, як ви його запустите.

Якщо запустити його ./myscript, то psвихід буде просто таким 12345 -bash, який не відповідає необхідному рядку 12345 bash ./myscript, так що не вдасться.

Якщо ви запускаєте це bash myscript, речі стають цікавішими. Процес bash примушує запускати конвеєр, а дочірня оболонка запускає psі grep. І вихідна оболонка, і дочірня оболонка з'являться у psвисновку, приблизно так:

25793 bash myscript
25795 bash myscript

Це не очікуваний результат $$ bash $0, тому ваш сценарій вийде.

Порушений підхід №2

Тепер, справедливо до користувача, який написав порушений підхід №1, я щось подібне зробив сам, коли вперше спробував це:

if otherpids="$(pgrep -f "$0" | grep -vFx "$$")" ; then
  echo >&2 "There are other copies of the script running; exiting."
  ps >&2 -fq "${otherpids//$'\n'/ }" # -q takes about a tenth the time as -p
  exit 1
fi

Це майже працює. Але факт прискорення запустити трубу це відкидає. Тож цей завжди буде також вихід.

Ненадійний підхід №3

pids_this_script="$(pgrep -f "$0")"
if not_this_process="$(echo "$pids_this_script" | grep -vFx "$$")"; then
  echo >&2 "There are other copies of this script running; exiting."
  ps -fq "${not_this_process//$'\n'/ }"
  exit 1
fi

Ця версія дозволяє уникнути проблеми з розкриттям конвеєра в підході №2, спочатку отримуючи всі PID, які мають поточний скрипт в аргументах командного рядка, а потім фільтруючи цей підписник окремо, щоб опустити PID поточного сценарію.

Це може спрацювати ... якщо жоден інший процес не відповідає командному рядку $0, а сценарій завжди називається однаковим чином (наприклад, якщо він викликається відносним шляхом, а потім абсолютним шляхом, останній екземпляр не помітить колишнього ).

Ненадійний підхід №4

Що робити, якщо ми пропустимо перевірку повного командного рядка, оскільки це може не вказувати на справді запущений скрипт, і перевіримо lsofнатомість, щоб знайти всі процеси, у яких цей скрипт відкритий?

Ну так, цей підхід насправді не надто поганий:

if otherpids="$(lsof -t "$0" | grep -vFx "$$")"; then
  echo >&2 "Error: There are other processes that have this script open - most likely other copies of the script running.  Exiting to avoid conflicts."
  ps >&2 -fq "${otherpids//$'\n'/ }"
  exit 1
fi

Звичайно, якщо копія сценарію запущена, то новий екземпляр запуститься просто чудово, і у вас будуть запущені дві копії .

Або якщо запущений скрипт модифікований (наприклад, з Vim або з a git checkout), тоді "нова" версія сценарію запуститься без проблем, оскільки як Vim, так і git checkoutновий результат (новий inode) замість Старий.

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

Все ще можуть бути помилкові позитиви, якщо інший процес має відкритий файл сценарію, але зауважте, що навіть якщо він відкритий для редагування у Vim, vim насправді не тримає файл сценарію відкритим, тому не призведе до помилкових позитивних результатів.

Але пам’ятайте, не використовуйте цей підхід, якщо сценарій може бути відредагований або скопійований, оскільки ви отримаєте помилкові негативи, тобто кілька запущених екземплярів одночасно - тому факт редагування за допомогою Vim не дає помилкових позитивних даних, не має значення тобі. Я згадую це, хоча, тому що підхід # 3 робить помилкові спрацьовування (тобто не запускається) , якщо у вас є сценарій відкритого з Vim.

То що ж робити тоді?

Топ проголосували відповідь на це питання дає хороший твердий підхід.

Можливо, ви можете написати кращий ... але якщо ви не розумієте всіх проблем і застережень з усіма вищезазначеними підходами, ви, швидше за все, не будете писати метод блокування, який уникає їх усіх.


0

Використовуючи інструмент FLOM (Free LOck Manager) , серіалізація команд стає настільки ж простою, як і запуск

flom -- command_to_serialize

FLOM дозволяє реалізувати більш складні випадки використання (розподілене блокування, читачі / записи, числові ресурси тощо), як пояснено тут: http://sourceforge.net/p/flom/wiki/FLOM%20by%20examples/


0

Ось що я іноді додаю на сервері, щоб легко обробляти гоночні умови для будь-якої роботи на машині. Це схоже на посаду Тіма Кеннеді, але таким чином ви отримуєте змагання з гонкою, додаючи лише один рядок до кожного сценарію bash, який потребує цього.

Вмістіть вміст нижче в напр. / Opt / racechecker / racechecker:

ZPROGRAMNAME=$(readlink -f $0)
EZPROGRAMNAME=`echo $ZPROGRAMNAME | sed 's/\//_/g'`
EZMAIL="/usr/bin/mail"
EZCAT="/bin/cat"

if  [ -n "$EZPROGRAMNAME" ] ;then
        EZPIDFILE=/tmp/$EZPROGRAMNAME.pid
        if [ -e "$EZPIDFILE" ] ;then
                EZPID=$($EZCAT $EZPIDFILE)
                echo "" | $EZMAIL -s "$ZPROGRAMNAME already running with pid $EZPID"  alarms@someemail.com >>/dev/null
                exit -1
        fi
        echo $$ >> $EZPIDFILE
        function finish {
          rm  $EZPIDFILE
        }
        trap finish EXIT
fi

Ось як його використовувати. Зверніть увагу на рядок після шебангу:

     #/bin/bash
     . /opt/racechecker/racechecker
     echo "script are running"
     sleep 120

Як він працює, це те, що він з'ясовує основне ім'я файлу bashscript і створює pidfile під "/ tmp". Це також додає слухача до сигналу фінішу. Слухач видалить pidfile, коли основний сценарій належним чином закінчиться.

Замість цього, якщо під час запуску екземпляра існує pidfile, тоді буде виконано оператор if, що містить код всередині другого if-оператора. У цьому випадку я вирішив запустити тривогу, коли це станеться.

Що робити, якщо сценарій виходить з ладу

Наступною вправою було б подолання аварій. В ідеалі pidfile слід видалити, навіть якщо основний сценарій виходить з ладу з будь-якої причини, це не робиться в моїй версії вище. Це означає, що якщо сценарій збиває pidfile, його потрібно буде видалити вручну, щоб відновити функціональність.

У разі збою системи

Добре ідею зберігати pidfile / lockfile під, наприклад, / tmp. Таким чином ваші сценарії, безумовно, продовжуватимуть виконуватись після краху системи, оскільки pidfiles завжди буде видалено під час завантаження.


На відміну від ансажа Тіма Кеннеді, ваш сценарій НЕ містить умови перегонів. Це тому, що ваша перевірка наявності ПІДФІЛУ та його умовного створення не проводиться в атомній операції.
maxschlepzig

+1 на це! Я візьму це на розгляд і модифікую свій сценарій.
ziggestardust

-2

Перевірте мій сценарій ...

Ви можете ЛЮБИТИ це ....

[rambabu@Server01 ~]$ sh Prevent_cron-OR-Script_against_parallel_run.sh
Parallel RUN Enabled
Now running
Task completed in Parallel RUN...
[rambabu@Server01 ~]$ cat Prevent_cron-OR-Script_against_parallel_run.sh
#!/bin/bash
#Created by RambabuKella
#Date : 12-12-2013

#LOCK file name
Parallel_RUN="yes"
#Parallel_RUN="no"
PS_GREP=0
LOCK=/var/tmp/mylock_`whoami`_"$0"
#Checking for the process
PS_GREP=`ps -ef |grep "sh $0" |grep -v grep|wc -l`
if [ "$Parallel_RUN" == "no" ] ;then
echo "Parallel RUN Disabled"

 if [ -f $LOCK ] || [ $PS_GREP -gt 2   ] ;then
        echo -e "\nJob is already running OR LOCK file exists. "
        echo -e "\nDetail are : "
        ps -ef |grep  "$0" |grep -v grep
        cat "$LOCK"
  exit 6
 fi
echo -e "LOCK file \" $LOCK \" created on : `date +%F-%H-%M` ." &> $LOCK
# do some work
echo "Now running"
echo "Task completed on with single RUN ..."
#done

rm -v $LOCK 2>/dev/null
exit 0
else

echo "Parallel RUN Enabled"

# do some work
echo "Now running"
echo "Task completed in Parallel RUN..."
#done

exit 0
fi
echo "some thing wrong"
exit 2
[rambabu@Server01 ~]$

-3

Я пропоную наступне рішення в сценарії з назвою "flocktest"

#!/bin/bash
export LOGFILE=`basename $0`.logfile
logit () {
echo "$1" >>$LOGFILE
}
PROGPATH=$0
(
flock -x -n 257
(($?)) && logit "'$PROGPATH' is already running!" && exit 0
logit "'$PROGPATH', proc($$): sleeping 30 seconds"
sleep 30
)257<$PROGPATH
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.