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


Відповіді:


109

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

LOCKFILE=/tmp/lock.txt
if [ -e ${LOCKFILE} ] && kill -0 `cat ${LOCKFILE}`; then
    echo "already running"
    exit
fi

# make sure the lockfile is removed when we exit and then claim it
trap "rm -f ${LOCKFILE}; exit" INT TERM EXIT
echo $$ > ${LOCKFILE}

# do stuff
sleep 1000

rm -f ${LOCKFILE}

У цьому полягає хитрість, kill -0яка не подає жодного сигналу, а лише перевіряє, чи існує процес із заданим PID. Також заклик до trapзабезпечення того, що файл блокування буде видалено навіть тоді, коли ваш процес буде вбито (за винятком kill -9).


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

1
Трюк символьного посилання акуратний, але якщо власник файлу блокування вбиває -9'd або система виходить з ладу, все ще існує умова гонки, щоб прочитати символьне посилання, зауважте, що власника немає, а потім видаліть його. Я дотримуюся свого рішення.
bmdhacks

9
Атомна перевірка та створення доступна в оболонці за допомогою flock (1) або lockfile (1). Дивіться інші відповіді.
dmckee --- кошеня колишнього модератора

3
Дивіться мою відповідь щодо портативного способу атомної перевірки та створення, не покладаючись на такі утиліти, як flock або lockfile.
lhunath

2
Це не атомно і, таким чином, марно. Вам потрібен атомний механізм для тестування та встановлення.
K Річард Пікслі

214

Використовуйте flock(1) для створення ексклюзивного діапазону блокування дескриптора файлу. Таким чином ви навіть можете синхронізувати різні частини сценарію.

#!/bin/bash

(
  # Wait for lock on /var/lock/.myscript.exclusivelock (fd 200) for 10 seconds
  flock -x -w 10 200 || exit 1

  # Do stuff

) 200>/var/lock/.myscript.exclusivelock

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

Caveat: ця команда є частиною util-linux. Якщо ви працюєте з операційною системою, відмінною від Linux, вона може бути, а може бути і недоступною.


11
Що таке 200? У манулі написано "fd", але я не знаю, що це означає.
chovy

4
@chovy "дескриптор файлу", ціла ручка, що позначає відкритий файл.
Алекс Б

6
Якщо хтось ще цікавиться: Синтаксис ( command A ) command Bвикликає нижню частину для command A. Документовано на tldp.org/LDP/abs/html/subshells.html . Я до сих пір не впевнений у термінах виклику
допоміжного корпусу

1
Я думаю, що код всередині підрозділу повинен бути більше схожий: if flock -x -w 10 200; then ...Do stuff...; else echo "Failed to lock file" 1>&2; fiтак що, якщо відбудеться час очікування (якийсь інший процес має файл заблокований), цей скрипт не випереджає і не змінює файл. Можливо ... зустрічний аргумент є "але якщо минуло 10 секунд, а блокування все ще недоступне, воно ніколи не буде доступним", мабуть тому, що процес, що тримає замок, не закінчується (можливо, його запускають під налагоджувачем?).
Джонатан Леффлер

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

158

Усі підходи, які перевіряють існування "файлів блокування", є хибними.

Чому? Тому що немає способу перевірити, чи існує файл, і створити його в одній атомній дії. Тому що; є умова перегонів, ЩО БУДЕ робити ваші спроби взаємного перерви виключення.

Замість цього потрібно використовувати mkdir. mkdirстворює каталог, якщо він ще не існує, і якщо він є, він встановлює вихідний код. Що ще важливіше, він робить це все в одній атомній дії, що робить його ідеальним для цього сценарію.

if ! mkdir /tmp/myscript.lock 2>/dev/null; then
    echo "Myscript is already running." >&2
    exit 1
fi

Детальніше дивіться у відмінній BashFAQ: http://mywiki.wooledge.org/BashFAQ/045

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

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

#       mutex file
#
# Open a mutual exclusion lock on the file, unless another process already owns one.
#
# If the file is already locked by another process, the operation fails.
# This function defines a lock on a file as having a file descriptor open to the file.
# This function uses FD 9 to open a lock on the file.  To release the lock, close FD 9:
# exec 9>&-
#
mutex() {
    local file=$1 pid pids 

    exec 9>>"$file"
    { pids=$(fuser -f "$file"); } 2>&- 9>&- 
    for pid in $pids; do
        [[ $pid = $$ ]] && continue

        exec 9>&- 
        return 1 # Locked by a pid.
    done 
}

Ви можете використовувати його в такому сценарії:

mutex /var/run/myscript.lock || { echo "Already running." >&2; exit 1; }

Якщо ви не переймаєтесь портативністю (ці рішення повинні працювати майже на будь-якому вікні UNIX), термофіксатор Linux (1) пропонує деякі додаткові параметри, а також є flock (1) .


1
Ви можете комбінувати if ! mkdirчастину, перевіряючи, чи процес із збереженим PID (при успішному запуску) всередині lockdir насправді працює та ідентичний сценарію для захисту stalenes. Це також захистить від повторного використання PID після перезавантаження і навіть не вимагатиме fuser.
Тобіас Кіенцлер

4
Безумовно, це правда, що mkdirце не визначено як атомна операція, і як таке, що "побічний ефект" є деталізацією реалізації файлової системи. Я йому повністю вірю, якщо він каже, що NFS не реалізує це атомно. Хоча я не підозрюю, що ваша участь /tmpбуде NFS, і, швидше за все, вона буде надана fs, який реалізує mkdirатомно.
lhunath

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

4
Там є «спосіб перевірити , чи існує файл і створити його в одному атомарному дії» - це open(... O_CREAT|O_EXCL). Для цього вам просто потрібна відповідна програма користувача, наприклад lockfile-create(in lockfile-progs) або dotlockfile(in liblockfile-bin). І переконайтеся, що ви правильно чистили (наприклад trap EXIT), або перевіряли наявність замку (наприклад, з --use-pid).
Toby Speight

5
"Усі підходи, які перевіряють існування" файлів блокування ", є помилковими. Чому? Тому що немає способу перевірити, чи існує файл і створити його в одній атомній дії." - Щоб зробити його атомарним, це потрібно зробити на рівень ядра - і це робиться на рівні ядра за допомогою flock (1) linux.die.net/man/1/flock, який з'являється з дати авторських прав man, яка існує приблизно щонайменше з 2006 року. Тому я зробив знищення (- 1), нічого особистого, просто переконані, що використання інструментів реалізованих у ядрі, наданих розробниками ядра, є правильним.
Крейг Хікс

42

Навколо системного виклику зграї (2) є обгортка, яку немислено називають зграєю (1). Це дозволяє порівняно легко надійно отримати ексклюзивні блокування, не турбуючись про очищення тощо. На сторінці man є приклади, як їх використовувати в сценарії оболонки.


3
flock()Системний виклик POSIX і не працює для файлів на монтує NFS.
maxschlepzig

17
Запуск із завдання Cron, який я використовую, flock -x -n %lock file% -c "%command%"щоб переконатися, що коли-небудь виконується лише один екземпляр.
Ryall

Aww, замість необразливої ​​зграї (1) вони повинні були піти з чимось на зразок зграї (U). .. .є деяке знайомство з цим. . Здається, я чув це раніше чи двох.
Кент Крюкберг

Примітно, що документація flock (2) вказує на використання лише з файлами, але документація flock (1) визначає використання з файлом чи каталогом. Документація flock (1) не є явною щодо того, як вказати різницю під час створення, але я припускаю, що це робиться шляхом додавання остаточного "/". У будь-якому випадку, якщо flock (1) може обробляти каталоги, але flock (2) не може, тоді flock (1) не реалізується лише у flock (2).
Крейг Хікс

27

Вам потрібна атомна операція, як зграя, інакше це врешті вийде з ладу.

Але що робити, якщо зграї немає. Ну є mkdir. Це теж атомна операція. Тільки один процес призведе до успішного mkdir, всі інші вийдуть з ладу.

Отже код:

if mkdir /var/lock/.myscript.exclusivelock
then
  # do stuff
  :
  rmdir /var/lock/.myscript.exclusivelock
fi

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


1
Виконайте це кілька разів одночасно (наприклад, "./a.sh & ./a.sh & ./a.sh & ./a.sh & ./a.sh & ./a.sh & ./a.sh & "), і сценарій просочиться кілька разів.
Ніппісавр

7
@Nippysaurus: Цей спосіб блокування не протікає. Ви побачили, що початковий сценарій закінчувався до запуску всіх копій, тож ще один міг (правильно) отримати блокування. Щоб уникнути цього помилкового позитиву, додайте sleep 10попередньо rmdirі спробуйте каскадувати знову - нічого не "просочиться".
Сер Афон

Інші джерела стверджують, що mkdir не є атомним у деяких файлових системах, таких як NFS. І btw я бачив випадки, коли в NFS паралельно рекурсивний mkdir призводить до помилок іноді з матричними завданнями jenkins. Тож я майже впевнений, що це так. Але mkdir дуже приємний для менш вимогливих випадків використання ІМО.
акостадінов

Ви можете використовувати опцію Bash'es noclobber для звичайних файлів.
Палець

26

Щоб зробити блокування надійним, вам потрібна атомна операція. Багато з перерахованих вище пропозицій не є атомними. Запропонована утиліта lockfile (1) виглядає багатообіцяючою, як згадується сторінка, що її "стійкий до NFS". Якщо ваша ОС не підтримує lockfile (1) і ваше рішення повинно працювати на NFS, у вас не так багато варіантів ....

NFSv2 має дві атомні операції:

  • симпосилання
  • перейменувати

З NFSv3 виклик створення також є атомним.

Операції з каталогом НЕ є атомними під NFSv2 та NFSv3 (будь ласка, зверніться до книги "NFS Illustrated" Brent Callaghan, ISBN 0-201-32570-5; Brent - ветеран NFS у Sun).

Знаючи це, ви можете реалізувати спін-блокування для файлів і каталогів (в оболонці, а не PHP):

заблокувати поточний реж:

while ! ln -s . lock; do :; done

заблокувати файл:

while ! ln -s ${f} ${f}.lock; do :; done

розблокувати поточний dir (припущення, що запущений процес дійсно придбав замок):

mv lock deleteme && rm deleteme

розблокувати файл (припущення, що запущений процес дійсно придбав замок):

mv ${f}.lock ${f}.deleteme && rm ${f}.deleteme

Видалити також не є атомним, тому спочатку перейменуйте (яке є атомним), а потім видаліть.

Для викликів символьного посилання та перейменування обидві назви файлів мають перебувати в одній файловій системі. Моя пропозиція: використовуйте лише прості назви файлів (без шляхів) і вкладайте файл і блокуйте в один і той же каталог.


Які сторінки NFS Illustrated підтримують твердження, що mkdir не є атомним для NFS?
maxschlepzig

Думає про цю техніку. Реалізація mutex оболонки доступна в моїй новій lib оболонці: github.com/Offirmo/offirmo-shell-lib , див. "Mutex". Він використовує, lockfileякщо він доступний, або резервний symlinkметод, якщо він відсутній.
Offirmo

Приємно. На жаль, цей метод не дає можливості автоматично видаляти застарілі замки.
Річард Хансен

Для розблокування двох етапів ( mv, rm) слід rm -fвикористовувати, а не rmу випадку, якщо два процеси P1, P2 гоняться? Наприклад, P1 починається з розблокування mv, потім блокується P2, потім P2 розблоковується (обидва)mv і rm), нарешті P1 намагається rmта не працює.
Метт Уолліс

1
@MattWallis Останню проблему можна легко усунути, включивши $$в ${f}.deletemeім’я файлу.
Стефан Маєвський

23

Інший варіант - використовувати noclobberпараметр оболонки, запустивши set -C. Тоді >помилка буде, якщо файл уже існує.

Коротко:

set -C
lockfile="/tmp/locktest.lock"
if echo "$$" > "$lockfile"; then
    echo "Successfully acquired lock"
    # do work
    rm "$lockfile"    # XXX or via trap - see below
else
    echo "Cannot acquire lock - already locked by $(cat "$lockfile")"
fi

Це викликає виклик оболонки:

open(pathname, O_CREAT|O_EXCL)

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


Відповідно до коментаря BashFAQ 045 , це може не вдатися ksh88, але воно працює у всіх моїх оболонках:

$ strace -e trace=creat,open -f /bin/bash /home/mikel/bin/testopen 2>&1 | grep -F testopen.lock
open("/tmp/testopen.lock", O_WRONLY|O_CREAT|O_EXCL|O_LARGEFILE, 0666) = 3

$ strace -e trace=creat,open -f /bin/zsh /home/mikel/bin/testopen 2>&1 | grep -F testopen.lock
open("/tmp/testopen.lock", O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY|O_LARGEFILE, 0666) = 3

$ strace -e trace=creat,open -f /bin/pdksh /home/mikel/bin/testopen 2>&1 | grep -F testopen.lock
open("/tmp/testopen.lock", O_WRONLY|O_CREAT|O_EXCL|O_TRUNC|O_LARGEFILE, 0666) = 3

$ strace -e trace=creat,open -f /bin/dash /home/mikel/bin/testopen 2>&1 | grep -F testopen.lock
open("/tmp/testopen.lock", O_WRONLY|O_CREAT|O_EXCL|O_LARGEFILE, 0666) = 3

Цікаво, що pdkshдодає O_TRUNCпрапор, але очевидно, що це зайве:
або ти створюєш порожній файл, або нічого не робиш.


Як ви це зробите, rmзалежить від того, як ви хочете обробляти нечисті виходи.

Видалити при чистому виході

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

# acquire lock
# do work (code here may call exit, etc.)
rm "$lockfile"

Видалити при будь-якому виході

Нові запуски вдаються за умови, що сценарій ще не працює.

trap 'rm "$lockfile"' EXIT

Дуже новий підхід ... Це, мабуть, є одним із способів досягнення атомності за допомогою файлу блокування, а не блокування каталогу.
Метт Колдвелл

Гарний підхід. :-) У пастці EXIT слід обмежити, який процес може очистити файл блокування. Наприклад: trap 'if [[$ (cat "$ lockfile") == "$$"]]; потім rm "$ lockfile"; fi 'EXIT
Кевін Зайферт

1
Файли блокування не є атомними для NFS. тому люди перейшли до використання каталогів блокування.
K Річард Пікслі

20

Ви можете використовувати GNU Parallelдля цього, оскільки він працює як mutex, коли його називають sem. Отже, конкретно, ви можете використовувати:

sem --id SCRIPTSINGLETON yourScript

Якщо ви хочете і час очікування, використовуйте:

sem --id SCRIPTSINGLETON --semaphoretimeout -10 yourScript

Час очікування <0 означає вихід без запуску скрипту, якщо семафор не буде випущений протягом тайм-ауту, час очікування> 0 означає середній запуск сценарію.

Зауважте, що ви повинні дати йому ім'я (з --id), інше воно за замовчуванням керує терміналом.

GNU Parallel це дуже проста установка на більшості платформ Linux / OSX / Unix - це просто сценарій Perl.


Занадто погані люди не хочуть оскаржувати марні відповіді: це призводить до того, що нові відповіді ховаються в купі сміття.
Дмитро Григор’єв

4
Нам просто потрібно багато грошей. Це така охайна і маловідома відповідь. (Хоча, щоб бути педантичним ОП хотілося швидко і брудно, тоді як це швидко і чисто!) Детальніше про semпов’язане питання unix.stackexchange.com/a/322200/199525 .
Мінлива хмарність

16

Для сценаріїв оболонок я схильний переходити із mkdirзакінченнямflock оскільки це робить блоки більш портативними.

У будь-якому випадку, використовуючи set -e недостатньо. Це закриває сценарій лише в тому випадку, якщо будь-яка команда не працює. Ваші замки все одно залишаться позаду.

Для правильного очищення блокування ви дійсно повинні встановити свої пастки на щось подібне до цього коду psuedo (знятий, спрощений та неперевірений, але з активно використовуваних сценаріїв):

#=======================================================================
# Predefined Global Variables
#=======================================================================

TMPDIR=/tmp/myapp
[[ ! -d $TMP_DIR ]] \
    && mkdir -p $TMP_DIR \
    && chmod 700 $TMPDIR

LOCK_DIR=$TMP_DIR/lock

#=======================================================================
# Functions
#=======================================================================

function mklock {
    __lockdir="$LOCK_DIR/$(date +%s.%N).$$" # Private Global. Use Epoch.Nano.PID

    # If it can create $LOCK_DIR then no other instance is running
    if $(mkdir $LOCK_DIR)
    then
        mkdir $__lockdir  # create this instance's specific lock in queue
        LOCK_EXISTS=true  # Global
    else
        echo "FATAL: Lock already exists. Another copy is running or manually lock clean up required."
        exit 1001  # Or work out some sleep_while_execution_lock elsewhere
    fi
}

function rmlock {
    [[ ! -d $__lockdir ]] \
        && echo "WARNING: Lock is missing. $__lockdir does not exist" \
        || rmdir $__lockdir
}

#-----------------------------------------------------------------------
# Private Signal Traps Functions {{{2
#
# DANGER: SIGKILL cannot be trapped. So, try not to `kill -9 PID` or 
#         there will be *NO CLEAN UP*. You'll have to manually remove 
#         any locks in place.
#-----------------------------------------------------------------------
function __sig_exit {

    # Place your clean up logic here 

    # Remove the LOCK
    [[ -n $LOCK_EXISTS ]] && rmlock
}

function __sig_int {
    echo "WARNING: SIGINT caught"    
    exit 1002
}

function __sig_quit {
    echo "SIGQUIT caught"
    exit 1003
}

function __sig_term {
    echo "WARNING: SIGTERM caught"    
    exit 1015
}

#=======================================================================
# Main
#=======================================================================

# Set TRAPs
trap __sig_exit EXIT    # SIGEXIT
trap __sig_int INT      # SIGINT
trap __sig_quit QUIT    # SIGQUIT
trap __sig_term TERM    # SIGTERM

mklock

# CODE

exit # No need for cleanup code here being in the __sig_exit trap function

Ось що буде. Усі пастки дають вихід, тому функція __sig_exitзавжди буде (забороняючи SIGKILL), яка очищає ваші замки.

Примітка: мої вихідні значення не є низькими. Чому? Різні системи пакетної обробки вимагають чи очікують числа від 0 до 31. Якщо встановити їх на щось інше, я можу зробити так, щоб мої сценарії та пакетні потоки відповідно реагували на попереднє пакетне завдання або сценарій.


2
Ваш сценарій занадто багатослівний, я можу бути набагато коротшим, я думаю, але в цілому, так, вам потрібно встановити пастки, щоб зробити це правильно. Також я додам SIGHUP.
mojuba

Це добре працює, за винятком випадків, коли він перевіряється на $ LOCK_DIR, тоді як він видаляє $ __ lockdir. Можливо, я б вам запропонував при видаленні блокування ви зробите rm -r $ LOCK_DIR?
бевада

Дякую за пропозицію. Наведене вище було знято з коду та розміщено в коді psuedo, тому його буде потрібно налаштовувати на основі використання людей. Однак я навмисно пішов з rmdir у своєму випадку, оскільки rmdir безпечно видаляє каталоги only_if вони порожні. Якщо люди розміщують в них ресурси, такі як PID-файли тощо, вони повинні змінити очищення блокування на більш агресивні rm -r $LOCK_DIRабо навіть змусити його за необхідності (як я це робив у особливих випадках, таких як зберігання відносних файлів скретчів). Ура.
Марк Стінсон

Ви протестували exit 1002?
Жиль Кінот

13

Дійсно швидкий і справді брудний? Цей одностроїк у верхній частині вашого сценарію буде працювати:

[[ $(pgrep -c "`basename \"$0\"`") -gt 1 ]] && exit

Звичайно, просто переконайтеся, що назва вашого сценарію унікальне. :)


Як я змоделюю це для тестування? Чи є спосіб запустити скрипт двічі в одному рядку і, можливо, отримати попередження, якщо він вже запущений?
rubo77

2
Це зовсім не працює! Навіщо перевіряти -gt 2? grep не завжди опиняється в результаті ps!
rubo77

pgrepнемає в POSIX. Якщо ви хочете, щоб ця робота працювала портативно, вам потрібен POSIX psта обробляти її вихід.
Палець

На OSX -cне існує, вам доведеться користуватися | wc -l. Про порівняння чисел: -gt 1перевіряється, оскільки перший екземпляр бачить себе.
Бенджамін Петро

6

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

#!/bin/dash

SCRIPTNAME=$(basename $0)
LOCKDIR="/var/lock/${SCRIPTNAME}"
PIDFILE="${LOCKDIR}/pid"

if ! mkdir $LOCKDIR 2>/dev/null
then
    # lock failed, but check for stale one by checking if the PID is really existing
    PID=$(cat $PIDFILE)
    if ! kill -0 $PID 2>/dev/null
    then
       echo "Removing stale lock of nonexistent PID ${PID}" >&2
       rm -rf $LOCKDIR
       echo "Restarting myself (${SCRIPTNAME})" >&2
       exec "$0" "$@"
    fi
    echo "$SCRIPTNAME is already running, bailing out" >&2
    exit 1
else
    # lock successfully acquired, save PID
    echo $$ > $PIDFILE
fi

trap "rm -rf ${LOCKDIR}" QUIT INT TERM EXIT


echo hello

sleep 30s

echo bye

5

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


5

Цей приклад пояснюється у зграї людини, але вона потребує певних зусиль, тому що ми повинні керувати помилками та вихідними кодами:

   #!/bin/bash
   #set -e this is useful only for very stupid scripts because script fails when anything command exits with status more than 0 !! without possibility for capture exit codes. not all commands exits >0 are failed.

( #start subprocess
  # Wait for lock on /var/lock/.myscript.exclusivelock (fd 200) for 10 seconds
  flock -x -w 10 200
  if [ "$?" != "0" ]; then echo Cannot lock!; exit 1; fi
  echo $$>>/var/lock/.myscript.exclusivelock #for backward lockdir compatibility, notice this command is executed AFTER command bottom  ) 200>/var/lock/.myscript.exclusivelock.
  # Do stuff
  # you can properly manage exit codes with multiple command and process algorithm.
  # I suggest throw this all to external procedure than can properly handle exit X commands

) 200>/var/lock/.myscript.exclusivelock   #exit subprocess

FLOCKEXIT=$?  #save exitcode status
    #do some finish commands

exit $FLOCKEXIT   #return properly exitcode, may be usefull inside external scripts

Можна скористатися іншим методом, перерахувати процеси, які я використовував у минулому. Але це складніше, ніж метод вище. Ви повинні перелічити процеси за ps, фільтрувати за його назвою, додаткові фільтри grep -v grep для видалення паразита і нарешті порахувати його grep -c. і порівняти з числом. Його складне і невизначене


1
Ви можете використовувати ln -s, оскільки це може створювати символьне посилання лише тоді, коли не існує жодного файлу чи символьної посилання, як mkdir. багато системних процесів, що використовувались посиланнями раніше, наприклад, init чи inetd. synlink зберігає ідентифікатор процесу, але насправді не вказує ні на що. за роки ця поведінка змінювалася. Процеси використовують зграї та семафори.
Znik

5

Опубліковані відповіді або покладаються на утиліту CLI flock або не захищають файл блокування належним чином. Утиліта flock доступна не у всіх системах, що не є Linux (тобто FreeBSD), і не працює належним чином у NFS.

У перші дні менеджменту системи та розвитку системи мені сказали, що безпечним і відносно портативним методом створення файлу блокування було створення файлу temp, використовуючи mkemp(3)абоmkemp(1) , записуючи ідентифікаційну інформацію в тимчасовий файл (тобто PID), а потім на жорстке посилання тимчасовий файл у файл блокування. Якщо посилання було успішним, то ви успішно отримали блокування.

Під час використання замків у скриптах оболонки я зазвичай розміщую obtain_lock()функцію у спільному профілі, а потім надсилаю її зі скриптів. Нижче наведено приклад моєї функції блокування:

obtain_lock()
{
  LOCK="${1}"
  LOCKDIR="$(dirname "${LOCK}")"
  LOCKFILE="$(basename "${LOCK}")"

  # create temp lock file
  TMPLOCK=$(mktemp -p "${LOCKDIR}" "${LOCKFILE}XXXXXX" 2> /dev/null)
  if test "x${TMPLOCK}" == "x";then
     echo "unable to create temporary file with mktemp" 1>&2
     return 1
  fi
  echo "$$" > "${TMPLOCK}"

  # attempt to obtain lock file
  ln "${TMPLOCK}" "${LOCK}" 2> /dev/null
  if test $? -ne 0;then
     rm -f "${TMPLOCK}"
     echo "unable to obtain lockfile" 1>&2
     if test -f "${LOCK}";then
        echo "current lock information held by: $(cat "${LOCK}")" 1>&2
     fi
     return 2
  fi
  rm -f "${TMPLOCK}"

  return 0;
};

Нижче наводиться приклад використання функції блокування:

#!/bin/sh

. /path/to/locking/profile.sh
PROG_LOCKFILE="/tmp/myprog.lock"

clean_up()
{
  rm -f "${PROG_LOCKFILE}"
}

obtain_lock "${PROG_LOCKFILE}"
if test $? -ne 0;then
   exit 1
fi
trap clean_up SIGHUP SIGINT SIGTERM

# bulk of script

clean_up
exit 0
# end of script

Не забудьте зателефонувати clean_up в будь-які точки виходу сценарію.

Я використовував вище, як в Linux, так і в FreeBSD.


4

Коли я орієнтуюся на машину Debian, я вважаю lockfile-progsпакет гарним рішенням. procmailтакож поставляється з lockfileінструментом. Однак іноді я застряг ні з одним із цього.

Ось моє рішення, яке використовує mkdirдля atomic-ness та PID-файл для виявлення застарілих замків. Цей код наразі виробляється у програмі Cygwin і працює добре.

Для його використання просто телефонуйте, exclusive_lock_requireколи вам потрібно отримати ексклюзивний доступ до чогось. Необов'язковий параметр імені блокування дозволяє ділитися блокуваннями між різними сценаріями. Також є дві функції нижчого рівня ( exclusive_lock_tryі exclusive_lock_retry), якщо вам потрібно щось складніше.

function exclusive_lock_try() # [lockname]
{

    local LOCK_NAME="${1:-`basename $0`}"

    LOCK_DIR="/tmp/.${LOCK_NAME}.lock"
    local LOCK_PID_FILE="${LOCK_DIR}/${LOCK_NAME}.pid"

    if [ -e "$LOCK_DIR" ]
    then
        local LOCK_PID="`cat "$LOCK_PID_FILE" 2> /dev/null`"
        if [ ! -z "$LOCK_PID" ] && kill -0 "$LOCK_PID" 2> /dev/null
        then
            # locked by non-dead process
            echo "\"$LOCK_NAME\" lock currently held by PID $LOCK_PID"
            return 1
        else
            # orphaned lock, take it over
            ( echo $$ > "$LOCK_PID_FILE" ) 2> /dev/null && local LOCK_PID="$$"
        fi
    fi
    if [ "`trap -p EXIT`" != "" ]
    then
        # already have an EXIT trap
        echo "Cannot get lock, already have an EXIT trap"
        return 1
    fi
    if [ "$LOCK_PID" != "$$" ] &&
        ! ( umask 077 && mkdir "$LOCK_DIR" && umask 177 && echo $$ > "$LOCK_PID_FILE" ) 2> /dev/null
    then
        local LOCK_PID="`cat "$LOCK_PID_FILE" 2> /dev/null`"
        # unable to acquire lock, new process got in first
        echo "\"$LOCK_NAME\" lock currently held by PID $LOCK_PID"
        return 1
    fi
    trap "/bin/rm -rf \"$LOCK_DIR\"; exit;" EXIT

    return 0 # got lock

}

function exclusive_lock_retry() # [lockname] [retries] [delay]
{

    local LOCK_NAME="$1"
    local MAX_TRIES="${2:-5}"
    local DELAY="${3:-2}"

    local TRIES=0
    local LOCK_RETVAL

    while [ "$TRIES" -lt "$MAX_TRIES" ]
    do

        if [ "$TRIES" -gt 0 ]
        then
            sleep "$DELAY"
        fi
        local TRIES=$(( $TRIES + 1 ))

        if [ "$TRIES" -lt "$MAX_TRIES" ]
        then
            exclusive_lock_try "$LOCK_NAME" > /dev/null
        else
            exclusive_lock_try "$LOCK_NAME"
        fi
        LOCK_RETVAL="${PIPESTATUS[0]}"

        if [ "$LOCK_RETVAL" -eq 0 ]
        then
            return 0
        fi

    done

    return "$LOCK_RETVAL"

}

function exclusive_lock_require() # [lockname] [retries] [delay]
{
    if ! exclusive_lock_retry "$@"
    then
        exit 1
    fi
}

Дякую, я спробував це на cygwin, і він пройшов прості тести.
ndemou

4

Якщо обмеження flock, які вже описані в іншому місці цього потоку, для вас не є проблемою, то це повинно працювати:

#!/bin/bash

{
    # exit if we are unable to obtain a lock; this would happen if 
    # the script is already running elsewhere
    # note: -x (exclusive) is the default
    flock -n 100 || exit

    # put commands to run here
    sleep 100
} 100>/tmp/myjob.lock 

3
Я просто задумав, що -x (блокування запису) вже встановлено за замовчуванням.
Keldon Alleyne

і -nбуде exit 1відразу ж , якщо він не може отримати блокування
Anentropic

Дякую @KeldonAlleyne, я оновив код, щоб видалити "-x", оскільки це за замовчуванням.
presto8

3

Деякі унікси мають lockfileдуже схожий з уже згаданим flock.

З сторінки сторінки:

lockfile може використовуватися для створення одного або декількох семафорних файлів. Якщо lock-файл не може створити всі вказані файли (у визначеному порядку), він чекає часу сну (за замовчуванням до 8) секунд і повторює останній файл, який не вдався. Ви можете вказати кількість повторень, які потрібно виконати, доки не буде повернуто помилку. Якщо кількість повторних спроб дорівнює -1 (за замовчуванням, тобто -r-1), файл блокування повторно буде повторено.


як ми отримуємо lockfileутиліту ??
Offirmo

lockfileрозповсюджується с procmail. Також є альтернатива, dotlockfileяка йде з liblockfileпакетом. Вони обидва заявляють, що надійно працюють над NFS.
Містер Безсмертний

3

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

lockfile=/var/lock/myscript.lock

if ( set -o noclobber; echo "$$" > "$lockfile") 2> /dev/null ; then
  trap 'rm -f "$lockfile"; exit $?' INT TERM EXIT
else
  # or you can decide to skip the "else" part if you want
  echo "Another instance is already running!"
fi

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

PS Я не бачив, що Мікель вже відповів правильно на питання, хоча він не включав команду trap, щоб зменшити шанс блокування файлу після залишення сценарію, наприклад, Ctrl-C. Тож це повне рішення


3

Мені хотілося покінчити з замками файлів, замками, спеціальними програмами блокування, і навіть pidofтому, що це не в усіх установах Linux. Також хотілося мати найпростіший можливий код (або принаймні якомога менше рядків). Найпростіша ifзаява в одному рядку:

if [[ $(ps axf | awk -v pid=$$ '$1!=pid && $6~/'$(basename $0)'/{print $1}') ]]; then echo "Already running"; exit; fi

1
Це чутливо до виводу 'ps' на моїй машині (Ubuntu 14.04, / bin / ps з procps-ng версії 3.3.9) команда 'ps axf' друкує символи дерева ascii, які порушують номери полів. Це працювало для мене: /bin/ps -a --format pid,cmd | awk -v pid=$$ '/'$(basename $0)'/ { if ($1!=pid) print $1; }'
qneill

2

Я використовую простий підхід, який обробляє застарілі файли блокування.

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

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

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

Має працювати з декількома оболонками на декількох платформах. Швидкий, портативний та простий.

#!/usr/bin/env sh
# Author: rouble

LOCKFILE=/var/tmp/lockfile #customize this line

trap release INT TERM EXIT

# Creates a lockfile. Sets global variable $ACQUIRED to true on success.
# 
# Returns 0 if it is successfully able to create lockfile.
acquire () {
    set -C #Shell noclobber option. If file exists, > will fail.
    UUID=`ps -eo pid,ppid,lstart $$ | tail -1`
    if (echo "$UUID" > "$LOCKFILE") 2>/dev/null; then
        ACQUIRED="TRUE"
        return 0
    else
        if [ -e $LOCKFILE ]; then 
            # We may be dealing with a stale lock file.
            # Bring out the magnifying glass. 
            CURRENT_UUID_FROM_LOCKFILE=`cat $LOCKFILE`
            CURRENT_PID_FROM_LOCKFILE=`cat $LOCKFILE | cut -f 1 -d " "`
            CURRENT_UUID_FROM_PS=`ps -eo pid,ppid,lstart $CURRENT_PID_FROM_LOCKFILE | tail -1`
            if [ "$CURRENT_UUID_FROM_LOCKFILE" == "$CURRENT_UUID_FROM_PS" ]; then 
                echo "Script already running with following identification: $CURRENT_UUID_FROM_LOCKFILE" >&2
                return 1
            else
                # The process that created this lock file died an ungraceful death. 
                # Take ownership of the lock file.
                echo "The process $CURRENT_UUID_FROM_LOCKFILE is no longer around. Taking ownership of $LOCKFILE"
                release "FORCE"
                if (echo "$UUID" > "$LOCKFILE") 2>/dev/null; then
                    ACQUIRED="TRUE"
                    return 0
                else
                    echo "Cannot write to $LOCKFILE. Error." >&2
                    return 1
                fi
            fi
        else
            echo "Do you have write permissons to $LOCKFILE ?" >&2
            return 1
        fi
    fi
}

# Removes the lock file only if this script created it ($ACQUIRED is set), 
# OR, if we are removing a stale lock file (first parameter is "FORCE") 
release () {
    #Destroy lock file. Take no prisoners.
    if [ "$ACQUIRED" ] || [ "$1" == "FORCE" ]; then
        rm -f $LOCKFILE
    fi
}

# Test code
# int main( int argc, const char* argv[] )
echo "Acquring lock."
acquire
if [ $? -eq 0 ]; then 
    echo "Acquired lock."
    read -p "Press [Enter] key to release lock..."
    release
    echo "Released lock."
else
    echo "Unable to acquire lock."
fi

Я дав вам +1 для іншого рішення. Але він не працює ні в AIX (> ps -eo pid, ppid, lstart $$ | хвост -1 ps: недійсний список з -o.) Не HP-UX (> ps -eo pid, ppid, lstart $$ | хвіст -1 пс: незаконний варіант - о). Дякую.
Тагар

2

Додайте цей рядок на початку сценарію

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

Це код котла від зграї людини.

Якщо ви хочете більше журналу, використовуйте цей

[ "${FLOCKER}" != "$0" ] && { echo "Trying to start build from queue... "; exec bash -c "FLOCKER='$0' flock -E $E_LOCKED -en '$0' '$0' '$@' || if [ \"\$?\" -eq $E_LOCKED ]; then echo 'Locked.'; fi"; } || echo "Lock is free. Completing."

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

Схоже, він не працює на Debian 7, але, здається, знову працює з експериментальним пакетом util-linux 2.25. На ньому написано "flock: ... Текстовий файл зайнятий". Це може бути відмінено, відключивши дозвіл на написання сценарію.


1

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


1

Я вважаю, що рішення bmdhack є найбільш практичним, принаймні для мого використання. Використовуючи flock та lockfile, розраховуйте на видалення файлу lock за допомогою rm, коли сценарій закінчується, що не завжди можна гарантувати (наприклад, kill -9).

Я б змінив одне незначне питання щодо рішення bmdhack: Це робить сенс видалити файл блокування, не заявляючи, що це не потрібно для безпечної роботи цього семафору. Його використання kill -0 гарантує, що старий файл блокування для мертвого процесу буде просто ігнорований / перезаписаний.

Моє спрощене рішення полягає в тому, щоб просто додати наступне у верхній частині вашого одиночного:

## Test the lock
LOCKFILE=/tmp/singleton.lock 
if [ -e ${LOCKFILE} ] && kill -0 `cat ${LOCKFILE}`; then
    echo "Script already running. bye!"
    exit 
fi

## Set the lock 
echo $$ > ${LOCKFILE}

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


1

У semaphoric утиліта використовує flock(як описано вище, наприклад , з допомогою presto8) для здійснення підрахунку семафора . Це дозволяє будь-яку конкретну кількість одночасних процесів, які ви хочете. Ми використовуємо це для обмеження рівня сумісності різних процесів робочих черг.

Це як пів, але набагато легшої ваги. (Повне розкриття: я написав це після того, як виявив, що сема була занадто важкою для наших потреб, і не було простої утиліти для підрахунку семафору.)


1

Приклад з flock (1), але без допоміжних оболонок. flock () ed file / tmp / foo ніколи не видаляється, але це не має значення, оскільки він отримує flock () та un-flock () ed.

#!/bin/bash

exec 9<> /tmp/foo
flock -n 9
RET=$?
if [[ $RET -ne 0 ]] ; then
    echo "lock failed, exiting"
    exit
fi

#Now we are inside the "critical section"
echo "inside lock"
sleep 5
exec 9>&- #close fd 9, and release lock

#The part below is outside the critical section (the lock)
echo "lock released"
sleep 5

1

Відповіли вже мільйон разів, але іншим способом, без необхідності зовнішніх залежностей:

LOCK_FILE="/var/lock/$(basename "$0").pid"
trap "rm -f ${LOCK_FILE}; exit" INT TERM EXIT
if [[ -f $LOCK_FILE && -d /proc/`cat $LOCK_FILE` ]]; then
   // Process already exists
   exit 1
fi
echo $$ > $LOCK_FILE

Кожен раз, коли він записує поточний PID ($$) у файл запису та при запуску скрипту перевіряє, чи працює процес із останнім PID.


1
Без виклику пастки (або принаймні очищення ближче до кінця для звичайного випадку) у вас є помилкова помилка помилки, коли файл блокування залишається навколо після останнього запуску, а PID повторно використовується іншим процесом пізніше. (І в гіршому випадку, це обдаровано тривалим процесом, як
апаш

1
Я погоджуюся, у мене підхід хибний, йому потрібна пастка. Я оновив своє рішення. Я все ще вважаю за краще не мати зовнішніх залежностей.
Філідор Візе

1

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

lock_file=/tmp/`basename $0`.lock

if fuser $lock_file > /dev/null 2>&1; then
    echo "WARNING: Other instance of $(basename $0) running."
    exit 1
fi
exec 3> $lock_file 

1

Я використовую oneliner @ на самому початку сценарію:

#!/bin/bash

if [[ $(pgrep -afc "$(basename "$0")") -gt "1" ]]; then echo "Another instance of "$0" has already been started!" && exit; fi
.
the_beginning_of_actual_script

Добре бачити наявність процесу в пам’яті (незалежно від стану процесу); але це робить роботу для мене.


0

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


0

Швидкий і брудний?

#!/bin/sh

if [ -f sometempfile ]
  echo "Already running... will now terminate."
  exit
else
  touch sometempfile
fi

..do what you want here..

rm sometempfile

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

3
C Новини, яка мене багато навчила про сценарій портативної оболонки, використовувалась для створення блокування. Файл $$, а потім спроба пов’язати його з "замком" - якщо посилання вдалося, у вас був замок, інакше ви зняли замок. $$ і вийшли.
Пол Томблін

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

2
Швидкий і брудний, про це він і попросив :)
Aupajo
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.