Поиск по сайту:

Избегайте этих проблем, ограничивая запуск сценариев Bash один раз за раз


Ключевые выводы

  • Убедитесь, что работает только один экземпляр вашего скрипта, используя pgrep, lsof или flock, чтобы предотвратить проблемы параллелизма.
  • Легко реализуйте проверки для самозавершения сценария, если обнаружены другие запущенные экземпляры.
  • Используя команды exec и env, команда flock может добиться всего этого с помощью одной строки кода.

Некоторые сценарии Linux имеют такие накладные расходы на выполнение, что необходимо предотвратить одновременный запуск нескольких экземпляров. К счастью, есть несколько способов добиться этого в ваших собственных сценариях Bash.

Иногда одного раза достаточно

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

Но не только пожиратели ресурсов должны работать в изоляции. Если ваш сценарий изменяет файлы, может возникнуть конфликт между двумя (или более) экземплярами сценария, поскольку они борются за доступ к файлам. Обновления могут быть потеряны или файл может быть поврежден.

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

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

Мы рассмотрим два примера первого метода, а затем рассмотрим один из способов реализации второго.

Использование pgrep для предотвращения параллелизма

Команда pgrep выполняет поиск процессов, запущенных на компьютере Linux, и возвращает идентификаторы процессов, соответствующих шаблону поиска.

У меня есть скрипт под названиемloop.sh. Он содержит цикл for, который печатает итерацию цикла, а затем приостанавливает работу на секунду. Это происходит десять раз.

#!/bin/bash
for (( i=1; i<=10; i+=1 ))
do
 echo "Loop:" $i
 sleep 1
done
exit 0

Я запустил два его экземпляра, а затем использовал pgrep для поиска по имени.

pgrep loop.sh

Он находит два экземпляра и сообщает их идентификаторы процессов. Мы можем добавить опцию -c (count), чтобы pgrep возвращал количество экземпляров.

pregp -c loop.sh

Мы можем использовать это количество экземпляров в нашем скрипте. Если значение, возвращаемое pgrep, больше единицы, должно быть запущено более одного экземпляра, и наш скрипт закроется.

Мы создадим сценарий, использующий эту технику. Мы назовем его pgrep-solo.sh.

Сравнение if проверяет, превышает ли число, возвращаемое pgrep, единицу. Если это так, сценарий завершается.

# count the instances of this script 
if [ $(pgrep -c pgrep-solo.sh) -gt 1 ]; then
 echo "Another instance of $0 is running. Stopping."
 exit 1
fi

Если число, возвращаемое pgrep, равно единице, сценарий можно продолжить. Вот полный сценарий.

#!/bin/bash
echo "Starting."
# count the instances of this script 
if [ $(pgrep -c pgrep-solo.sh) -gt 1 ]; then
 echo "Another instance of $0 is running. Stopping."
 exit 1
fi
# we're cleared for take off
for (( i=1; i<=10; i+=1 ))
do
 echo "Loop:" $i
 sleep 1
done
exit 0

Скопируйте это в свой любимый редактор и сохраните как pgrep-solo.sh. Затем сделайте его исполняемым с помощью chmod.

chmod +x pgrep-loop.sh

Когда он работает, это выглядит так.

./pgrep-solo.sh

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

./pgrep-solo.sh

Использование lsof для предотвращения параллелизма

Мы можем сделать очень похожую вещь с помощью команды lsof.

Если мы добавим опцию -t (краткую), lsof выведет список идентификаторов процессов.

lsof -t loop.sh

Мы можем передать вывод lsof в wc. Опция -l (строки) подсчитывает количество строк, которое в этом сценарии совпадает с количеством идентификаторов процессов.

lsof -t loop.sh | wc -l

Мы можем использовать это как основу для проверки при сравнении if в нашем скрипте.

Сохраните эту версию как lsof-solo.sh.

#!/bin/bash
echo "Starting."
# count the instances of this script 
if [ $(lsof -t "$0" | wc -l) -gt 1 ]; then
 echo "Another instance of $0 is running. Stopping."
 exit 1
fi
# we're cleared for take off
for (( i=1; i<=10; i+=1 ))
do
 echo "Loop:" $i
 sleep 1
done
exit 0

Используйте chmod, чтобы сделать его исполняемым.

chmod +x lsof-solo.sh

Теперь, когда скрипт lsof-solo.sh работает в другом окне терминала, мы не можем запустить вторую копию.

./lsof-solo.sh

Для метода pgrep требуется только один вызов внешней программы (pgrep), для метода lsof — два (lsof и wc). Но преимущество метода lsof перед методом pgrep заключается в том, что вы можете использовать переменную $0 в сравнении if. Здесь хранится имя сценария.

Это означает, что вы можете переименовать скрипт, и он все равно будет работать. Вам не нужно не забывать редактировать строку сравнения if и вставлять новое имя скрипта. Переменная $0 включает в себя «./» в начале имени сценария (например, ./lsof-solo.sh), и pgrep это не нравится.

Использование flock для предотвращения параллелизма

Наш третий метод использует команду flock, которая предназначена для установки блокировок файлов и каталогов внутри скриптов. Пока он заблокирован, ни один другой процесс не может получить доступ к заблокированному ресурсу.

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

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

Вскоре мы расшифруем эти иероглифы. А пока давайте просто проверим, что это работает. Сохраните это как flock-solo.sh.

#!/bin/bash
[ "${GEEKLOCK}" != "$0" ] && exec env GEEKLOCK="$0" flock -en "$0" "$0" "$@" || :
echo "Starting."
# we're cleared for take off
for (( i=1; i<=10; i+=1 ))
do
 echo "Loop:" $i
 sleep 1
done
exit 0

Конечно, нам нужно сделать его исполняемым.

chmod +x flock-solo.sh

Я запустил скрипт в одном окне терминала, затем попытался запустить его снова в другом окне терминала.

./flock-solo
./flock-solo
./flock-solo

Я не могу запустить сценарий, пока не завершится выполнение экземпляра в другом окне терминала.

Давайте выделим линию, которая творит волшебство. В основе всего этого лежит команда стаи.

flock -en "$0" "$0" "$@"

Команда flock используется для блокировки файла или каталога, а затем для запуска команды. Мы используем параметры -e (эксклюзивный) и -n (неблокирующий).

Эксклюзивный вариант означает, что если нам удастся заблокировать файл, никто другой не сможет получить к нему доступ. Неблокирующий вариант означает, что если нам не удастся получить блокировку, мы немедленно прекратим попытки. Мы не повторяем попытку в течение определенного периода времени, мы сразу же изящно раскланяемся.

Первый $0 указывает файл, который мы хотим заблокировать. Эта переменная содержит имя текущего скрипта.

Второй $0 — это команда, которую мы хотим выполнить, если нам удалось получить блокировку. Опять же, мы передаем имя этого скрипта. Поскольку блокировка блокирует всех, кроме нас, мы можем запустить файл сценария.

Мы можем передавать параметры запускаемой команде. Мы используем $@ для передачи любых параметров командной строки, которые были переданы этому сценарию, при новом вызове сценария, который будет запущен с помощью flock.

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

Мы используем переменную среды, называемую GEEKLOCK, чтобы указать, должен ли работающий скрипт применять блокировку или нет. Если сценарий запущен, а блокировки нет, ее необходимо применить. Если скрипт запустился и стоит блокировка, то ничего делать не нужно, можно просто запустить. При работающем сценарии и установленной блокировке никакие другие экземпляры сценария не могут быть запущены.

[ "${GEEKLOCK}" != "$0" ] 

Этот тест переводится как «возвращает true, если переменная среды GEEKLOCK не установлена на имя сценария». Тест связан с остальной частью команды с помощью && (and) и || (или). Часть && выполняется, если тест возвращает true, а часть || раздел выполняется, если тест возвращает false.

env GEEKLOCK="$0"

Команда env используется для запуска других команд в измененных средах. Мы модифицируем нашу среду, создавая переменную среды GEEKLOCK и присваивая ей имя скрипта. Команда, которую собирается запустить env, — это команда flock, а команда flock запускает второй экземпляр сценария.

Второй экземпляр сценария выполняет проверку на предмет того, не переменная среды GEEKLOCK, но обнаруживает, что она существует. || выполняется раздел команды, который не содержит ничего, кроме двоеточия «:», что на самом деле является командой, которая ничего не делает. Затем путь выполнения проходит через остальную часть сценария.

Но у нас все еще остается проблема, связанная с тем, что первый сценарий продолжает свою собственную обработку после завершения работы второго сценария. Решением этой проблемы является команда exec. При этом выполняются другие команды, заменяя вызывающий процесс вновь запущенным процессом.

exec env GEEKLOCK="$0" flock -en "$0" "$0" "$@" 

Итак, полная последовательность:

  • Скрипт запускается и не может найти переменную среды. Предложение && выполняется.
  • exec запускает env и заменяет исходный процесс сценария новым процессом env.
  • Процесс env создает переменную среды и запускает flock.
  • flock блокирует файл сценария и запускает новый экземпляр сценария, который обнаруживает переменную среды, запускает || и сценарий может завершиться до конца.
  • Поскольку исходный сценарий был заменен процессом env, он больше не существует и не может продолжить выполнение после завершения второго сценария.
  • Поскольку файл сценария блокируется во время его работы, другие экземпляры не могут быть запущены до тех пор, пока сценарий, запущенный flock, не перестанет работать и не снимет блокировку.

Это может звучать как сюжет «Начала», но работает прекрасно. Эта одна строчка, безусловно, производит впечатление.

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

Блокировка и загрузка

Обеспечить одновременное выполнение только одного экземпляра скрипта проще, чем вы могли ожидать. Все три метода работают. Хотя однострочник Flock наиболее запутан в работе, его легче всего вставить в любой сценарий.

Статьи по данной тематике: