Время от времени возникает необходимость сделать так, чтобы программа гарантированно работала в одном экземпляре. Например, это может быть скрипт, который генерирует некий файл — если запустить одновременно два экземпляра скрипта, то файл будет испорчен.
Нужно проверить — является ли запускаемый процесс единственным, запущенным в данный момент, экземпляром программы, или уже есть другой, запущенный экземпляр?
Есть несколько методов такой проверки, отличающихся надежностью.
Основные методы
1) Проверка существования пид-файла
Скрипт запускается и проверяет наличие пид-файла. Если пид-файл уже существует — значит, другой экземпляр скрипта уже запущен и второй раз запускаться не следует. Если же пид-файла не существует, то скрипт создает пид-файл и начинает работать.
Проблема в том, что первый экземпляр может упасть, не удалив пид-файл. И теперь запустить скрипт будет вообще невозможно, так как запускаемый скрипт всегда будет обнаруживать пид-файл, считать себя вторым экземпляром и отказываться запускаться, пока пид-файл не будет удален вручную. Кроме того, существует проблема с гонкой условий, так как проверка существования файла и последующее создание этого файла являются двумя отдельными операциями, а не одной атомарной.
2) Проверка наличия пида в списке процессов
Скрипт запускается, читает пид-файл и затем проверяет — есть ли процесс с прочитанным пидом в таблице процессов. Если такой процесс существует — значит, другой экземпляр скрипта уже запущен и второй раз запускаться не следует. Если же такого процесса не существует, то скрипт записывает свой пид в пид-файл и начинает работать.
Проблема в том, что первый экземпляр может упасть, а пид, с которым он работал, может быть выдан другому процессу. После этого, как и в первом методе, возникнет проблема с запуском скрипта. Конечно, вероятность возникновения такой ситуации несколько ниже, чем в первом случае, ведь повторно пид будет выдан не сразу. Да и вероятность того, что посторонний процесс получит точно такой же пид, как и наш процесс, не очень большая, однако она есть, так как пидов не бесконечное количество и они выдаются по кругу. Ну и плюс гонка условий, так как операций тут еще больше, чем в первом методе.
3) Блокировка пид-файла
Скрипт запускается и пытается заблокировать пид-файл. Если заблокировать не удалось — значит, другой экземпляр скрипта уже запущен и второй раз запускаться не следует. Если же заблокировать пид-файл удалось, то скрипт продолжает работать.
Этот метод не имеет проблем, возникающих в предыдущих двух методах:
- Падение первого экземпляра скрипта автоматически снимает блокировку с пид-файла, поэтому следующий экземпляр может быть запущен без проблем
- Не возникает гонка условий, так как блокировка является атомарным действием
Таким образом, этот метод гарантированно обеспечивает блокировку запуска второго экземпляра программы.
Метод блокировки пид-файла
Рассмотрим подробно реализацию этого метода.
#!/usr/bin/perl
use Carp;
use Fcntl qw(:DEFAULT :flock);
check_proc('/tmp/testscript.pid') and die "Скрипт уже запущен, запуск дубля отклонен!n";
# Тут находится код,
# который должен исполняться
# в единственном экземпляре
sleep 15;
# Проверка существования запущенного экземпляра
sub check_proc {
my ($file) = @_;
my $result;
sysopen LOCK, $file, O_RDWR|O_CREAT or croak "Невозможно открыть файл $file: $!";
if ( flock LOCK, LOCK_EX|LOCK_NB ) {
truncate LOCK, 0 or croak "Невозможно усечь файл $file: $!";
my $old_fh = select LOCK;
$| = 1;
select $old_fh;
print LOCK $$;
}
else {
$result = <LOCK>;
if (defined $result) {
chomp $result;
}
else {
carp "Отсутствует PID в пид-файле $file";
$result = '0 but true';
}
}
return $result;
}
В первую очередь скрипт вызывает функцию check_proc, проверяющую наличие другого запущенного экземпляра, и, если проверка завершается успешно, скрипт останавливается с соответствующим сообщением.
Обратите внимание, что в этой строке функции check_proc и die объединены через условный оператор and. Обычно подобные связки делаются через оператор or, но в нашем случае логика связки другая — мы как бы говорим скрипту: «Осознай бессмысленность своего существования и умри!».
Функция check_proc возвращает пид уже запущенного экземпляра, если он действительно запущен, либо undef. Соответственно, истинный результат выполнения этой функции означает, что один экземпляр программы уже запущен и второй раз запускаться не нужно.
Функция check_proc
Теперь разберем построчно саму функцию check_proc.
1) Функция sysopen открывает файл на чтение и запись
Тут важно то, что файл нужно открывать в неразрушающем режиме, иначе содержимое файла будет уничтожено. Из-за этого нельзя воспользоваться простой функцией open, так как она не умеет открывать файлы в неразрушающем режиме.
Функция sysopen c флагами O_RDWR|O_CREAT открывают файл именно в неразрушающем режиме. Флаг O_RDWR означает открытие одновременно на чтение и запись, флаг O_CREAT создает файл, если его не существует на момент открытия. Флаги импортируются из модуля Fcntl (можно обойтись без Fcntl, если использовать численные значения флагов).
2) Функция flock блокирует файл
Поскольку нам нужно сделать так, чтобы блокировка была только у одного процесса, то нужно запрашивать эксклюзивную блокировку. Эксклюзивная блокировка задается флагом LOCK_EX. Как только процесс получает эксклюзивную блокировку — всё, никто другой такую блокировку параллельно получить не сможет. На этом, собственно, и основан механизм блокировки запуска второго экземпляра программы, это ключевая функция.
Если функция flock обнаруживает, что кто-то другой уже заблокировал файл, то она будет ждать, пока блокировка не будет снята. Такое поведение не подходит для нашей проверки. Нам не нужно ждать освобождения файла, нам нужно, чтобы при обнаружении блокировки функция check_proc сразу вернула положительный результат. Для этого нужно использовать флаг LOCK_NB.
Дальнейшее поведение зависит от того, удалось ли получить блокировку (3) или не удалось (4).
3а) Функция truncate очищает файл
Поскольку мы открыли файл в неразрушающем режиме, то старое содержимое файла осталось нетронутым. Это содержимое нам не нужно, и даже может мешать, поэтому файл нужно очистить.
3б) Комбинация функций select и переменной $| отключает буферизацию
Нам нужно записать пид ткущего процесса в пид-файл. Но вывод в файл буферизуется поблочно, поэтому запись пида будет (казалось бы) выполнена, но на самом деле в файле будет (пока еще) пусто. Из-за этого какой-нибудь другой процесс, пытающийся прочитать из пид-файла пид нашего запущенного процесса, обнаружит там пустоту. Наша проверка основана на блокировке пид-файла, а не на проверке пида, поэтому для наших процессов отсутствие пида не станет катастрофой. Но для процессов, которым важен сам пид, это создаст проблему.
Чтобы отключить буферизацию вывода, нужно связанную с дескриптором этого вывода переменную $| установить в истинное значение. Первый select устанавливает текущим дескриптором дескриптор нашего пид-файла, затем переменная устанавливается в истинное значение, потом второй select возвращает STDOUT обратно на место текущего дескриптора. После этого запись в файл будет происходить немедленно, без буферизации.
4а) Читаем пид из пид-файла
Само чтение из файла тривиально, но нужно иметь в виду, что возможна ситуация, когда пид в файле не будет обнаружен. Это будет означать, что экземпляр программы уже запущен (ведь блокировку получить не удалось), но пид этого запущенного экземпляра почему-то не записан. Это не должно стать проблемой для нашей проверки, ведь она основана не на проверке пида. Но функция check_proc должна возвращать истинное значение в случае обнаружения запущенного экземпляра, поэтому вместо отсутствующего пида нужно вернуть что-то другое, являющееся, тем не менее, истиной.
Подходящим значением в этом случае будет «истинный ноль». Это магическое значение (которых в перле много), которое в числовом контексте равно нулю, а в булевом равно истине. Вариантов записи истинного ноля несколько, я использую вариант «0 but true».
Заключение
Метод блокировки пид-файла является самым надежным способом обеспечения работы программы в единственном экземпляре.
Функцию check_proc и подключение модуля Fcntl можно вынести в отдельный модуль (например, c названием MacLeod.pm), в этом случае обеспечение работы программы в одном экземпляре будет делаться всего в две строчки:
use MacLeod;
check_proc('/tmp/testscript.pid') and die "Скрипт уже запущен, запуск дубля отклонен!n";
Либо, проверку можно сделать немного более развернутой:
use MacLeod;
my $pid = check_proc('/tmp/testscript.pid');
if ($pid) {
die "Скрипт с пидом $pid уже запущен, запуск дубля отклонен!n";
}
else {
print "Поехали!n";
}
В этом случае возвращаемый функцией check_proc пид запущенного процесса записывается в переменную $pid и его можно вывести в сообщении.
Автор: ivanych