Управление процессами и межпроцессные взаимодействия
Введение
Многие из нас относятся к Perl по-своему, но большинство считает его чем-то вроде "клея", объединяющего разнородные компоненты. Эта глава посвящена командам и отдельным процессам - их созданию, взаимодействию и завершению. Итак, речь пойдет о системном пр
В области системного программирования на Perl, как обычно, все простое упрощается, а все сложное становится доступным. Если вы хотите работать на высоком уровне, Perl с радостью вам поможет. Если вы собираетесь закатать рукава и заняться низкоуровневым пр
Perl позволяет очень близко подобраться к системе, но при этом могут возникнуть некоторые проблемы переносимости. Из всей книги эта глава в наибольшей степени ориентирована на UNIX. Изложенный материал чрезвычайно полезен для тех, кто работает в UNIX-
Создание процессов
В этой главе рассматриваются порожденные процессы. Иногда вы просто выполняете автономную команду (с помощью system) и оставляете созданный процесс на произвол судьбы. В других случаях приходится сохранять тесную связь с созданным процессом, скармлива
Сначала мы рассмотрим самые переносимые и распространенные операции управления процессами: '...', system, open и операции с хэшем %SIG. Здесь нет ничего сложного, но мы не остановимся па этом и покажем, что делать, когда простые решения не подходят.
Допустим, вы хотите прервать свою программу в тот момент, когда она запустила другую программу. Или вам захотелось отделить стандартный поток ошибок порожденного процесса от его стандартного вывода. Или вы собираетесь одновременно управлять и как вводом,
В подобных ситуациях приходится обращаться к системным функциям: pipe, fork и ехес. Функция pipe создает два взаимосвязанных манипулятора, записывающий и читающий; при этом все данные, записываемые в первый, могут быть прочитаны из первого. Функция fork я
При уничтожении порожденного процесса его память возвращается операционной системе, но соответствующий элемент таблицы процессов не освобождается. Благодаря этому родитель может проверить статус завершения всех порожденных процессов. Процессы, которые
Сигналы
Ваш процесс узнает о смерти созданного им порожденного процесса с помощью сигнала. Сигналы представляют собой нечто вроде оповещений, доставляемых операционной системой. Они сообщают о произошедших ошибках (когда ядро говорит: "Не трогай эту область памят
Каждый процесс имеет стандартные обработчики для всех возможных сигналов. Вы можете установить свой собственный обработчик или изменить отношение программы к большинству сигналов. Не изменяются только SIGKILL и SIGTOP - все остальные сигналы можно игн
Приведем краткую сводку важнейших сигналов.
SIGINT
Обычно возникает при нажатии Ctrl+C. Требует, чтобы процесс завершил свою работу. Простые программы (например, фильтры) обычно просто умирают, но более сложные программы - командные интерпретаторы, редакторы и программы FTP - обычно используют SIGINT
SIGQUIT
vОбычно генерируется терминалом, как правило, при нажатии Ctrl+\. По умолчанию выводит в файл содержимое памяти.
SIGTERM
Посылается командой kill при отсутствии явно заданного имени сигнала. Может рассматриваться как вежливая просьба умереть, адресованная процессу.
SIGUSR1 и SIGUSR2
Никогда не вызываются системными событиями, поэтому пользовательские приложения могут смело использовать их для собственных целей.
SIGPIPE
Посылается ядром, когда ваш процесс пытается записать в канал (pipe) или со-кет, а процесс на другом конце канала/сокета отсоединился (обычно потому, что он перестал существовать).
SIGALRM
Посылается при истечении промежутка времени, установленного функцией alarm (см. рецепт 16.21).
SIGHUP
Посылается процессу при разрыве связи (hang-up) на управляющем терминале (например, при потере несущей модемом), но также часто означает, что программа должна перезапуститься или заново прочитать свою конфигурацию.
SIGCHLD
Вероятно, самый важный сигнал во всем низкоуровневом системном програм мировании. Система посылает процессу сигнал SIGSHLD в том случае, если один из его порожденных процессов перестает выполняться - или, что более вероятно, при его завершении. Дополн
Имена сигналов существуют лишь для удобства программистов. С каждым сигналом связано определенное число, используемое операционной системой вместо имени. Хотя мы говорим о сигнале SIGCHLD, операционная система опознает его по номеру - например, 20 (в
Обработка сигналов рассматривается в рецептах 16.7, 16.15, 16.18, 16.20 и 16.21.
Требуется запустить программу и сохранить ее вывод в переменной.
Решение
Воспользуйтесь либо оператором '...':
$output = 'ПРОГРАММА АРГУМЕНТЫ';
# Сохранение данных в одной # многострочной переменной.
@output = 'ПРОГРАММА АРГУМЕНТЫ'; # Сохранение данных в массиве,
# по одной строке на элемент.
#либо решением из рецепта 16.4: open(README, "ПРОГРАММА АРГУМЕНТЫ |") or die "Can't run program: $!\n";
while() { $output .= $_;
} close(README);
Комментарий
Оператор ' . . . ' является удобным средством для запуска других программ и получения их выходных данных. Возврат из него происходит лишь после завершения вызванной программы. Для получения вывода Perl предпринимает некоторые дополнительные усилия, поэтом
ofsck -у /dev/rsd-la'; # ОТВРАТИТЕЛЬНО
И функция open, и оператор '. . . ' обращаются к командному интерпретатору для выполнения команд. Из-за этого они недостаточно безопасно работают в привилегированных программах.
Приведем низкоуровневое обходное решение с использованием pipe, fork и ехес:
use POSIX qw(:sys_wait_h);
pipe(README, WRITEME);
if ($pid = fork) {
# Родительский процесс
$SIG{CHLD} = sub { 1 while ( waitpid(-1, WNOHANG)) > 0 };
close(WRITEME);
} else {
die "cannot fork: $!" unless defined $pid;
# Порожденный процесс
open(STDOUT, ">&=WRITEME") or die "Couldn't redirect STDOUT: $!";
close(README);
exec($program, $arg1, $arg2) or die "Couldn't run $program : $!\n";
}
while () {
$string .= $_;
# or push(@strings, $_) } close(README);
> Смотри также ----
perlsec(1); рецепты 16.2; 16.4; 16.19; 19.6.
Вы хотите запустить другую программу из своей, дождаться ее завершения и затем продолжить работу. Другая программа должна использовать те же STDIN и STDOUT, что и основная.
Решение
Вызовите функцию system со строковым аргументом, который интерпретируется как командная строка:
$status = system("vi $myfile"); Если вы не хотите привлекать командный интерпретатор, передайте syster список:
$status = system("vi", $myfile);
Комментарий
Функция system обеспечивает самую простую и универсальную возможность запуска других программ в Perl. Она не возвращает выходные данные внешней программы, как '. . . ' или open. Вместо этого ее возвращаемое значение (фактически) совпадает с кодом завершен
При вызове с одним аргументом функция system (как и open, exec и '...') использует командный интерпретатор для запуска программы. Это может пригодиться для перенаправления или других фокусов:
system("cmd1 args | cmd2 | cmd3 outfile");
system("cmd args outfile2>errfile"); Чтобы избежать обращений к интерпретатору, вызывайте system со списком аргументов:
$status = system($program, $arg1, $arg);
die "$program exited funny: $?" unless $status == 0;
Возвращаемое значение не является обычным кодом возврата; оно включает номер сигнала, от которого умер процесс (если он был). Это же значение присваивается переменной $? функцией wait. В рецепте 16.19 рассказано о том, как декодировать tuj.
Функция system (но не '...'!) игнорирует SIGINT и SIGQUIT во время работы порожденных процессов. Сигналы убивают лишь порожденные процессы. Если вы хотите, чтобы основная программа умерла вместе с ними, проверьте возвращаемое значение system или перем
if (($signo = system((Sarglist)) &= 127)
{ die "program killed by signal $signo\n";
}
Чтобы игнорировать SIGINT, как это делает system, установите собственный обработчик сигнала, а затем вручную вызовите fork и ехес:
if ($pid = fork) {
# Родитель перехватывает INT и предупреждает пользователя
local $SIG{INT} = sub < print "tsk tsk, no process interruptus\n" };
waitpid($pid, 0);
} else {
die "cannot fork: $!" unless defined $pid;
# Потомок игнорирует INT и делает свое дело
$SIG{INT} = "IGNORE";
exec("summarize", "/etc/logfiles") or die "Can't exec: $!\n";
}
($pid = fork) ? waitpid($pid, 0) : exec(@ARGV)
or die "Can't exec: $!\n"; Некоторые программы просматривают свое имя. Командные интерпретаторы узнают, были ли они вызваны с префиксом -, обозначающим интерактивность. Программа ехрп в конце главы 18 при вызове под именем vrfy работает иначе; такая ситуация возникает при созда
Если вы хотите подсунуть запускаемой программе другое имя, укажите настоящий путь в виде "косвенного объекта" перед списком, передаваемым system (также работает для exec). После косвенного объекта не ставится запятая, по аналогии с вызовом printf для
$shell = '/bin/tcsh';
system $shell '-csh'; # Прикинуться другим интерпретатором Или непосредственно:
system {'/bin/tcsh'} '-csh'; # Прикинуться другим интерпретатором В следующем примере настоящее имя программы передается в виде косвенного объекта {'/home/tchrist/scripts/expn '}. Фиктивное имя 'vrfy' передается в виде первого настоящего аргумента функции, и программа увидит его в переменной $0.
# Вызвать ехрn как vrfy system {'/home/tchrist/scripts/expn'} 'vrfy', @ADDRESSES; Применение косвенных объектов с system более надежно. В этом случае аргументы заведомо интерпретируются как список, даже если он состоит лишь из од ного элемента. Это предотвращает расширение метасимволов командным интерпретатором или разделение слов,
@args = ( "echo surprise" );
system @args; # Если Oargs == 1, используются
# служебные преобразования интерпретатора
system { $args[0] } @args; # Безопасно даже для одноаргументного списка Первая версия (без косвенного объекта) запускает программу echo и передает ей аргумент "surprise". Вторая версия этого не делает - она честно пытается запустить программу "echo surprise", не находит ее и присваивает $9 ненулевое значение, свидетельств
> Смотри также --------------------------------
perlsec(1); описание функций waitpid, fork, exec, system и open в perlfunc(1);
рецепты 16.1; 16.4; 16.19; 19.6.
Требуется заменить работающую программу другой - например, после проверки параметров и настройки окружения, предшествующих выполнению основной программы.
Решение
Воспользуйтесь встроенной функцией exec. Если exec вызывается с одним аргументом, содержащим метасимволы, для запуска будет использован командный интерпретатор:
exec("archive *.data")
or die "Couldn't replace myself with archive: $!\n"; Если exec передаются несколько аргументов, командный интерпретатор не используется:
exec("archive", "accounting.data")
or die "Couldn't replace myself with archive: $!\n"; При вызове с одним аргументом, не содержащим метасимволов, аргумент разбивается по пропускам и затем интерпретируется так, словно функция ехес была вызвана для полученного списка:
ехес("archive accounting.data")
or die "Couldn't replace myself with archive: $!\n";
Комментарий
Функция Perl ехес обеспечивает прямой интерфейс к системной функции ехес1р(2), которая заменяет текущую программу другой без изменения идентификатор процесса. Программа, вызвавшая ехес, стирается, а ее место в таблице процессов операционной системы занима
При переходе к другой программе с помощью ехес не будут автоматически вызваны ни блоки END, ни деструкторы объектов, как бы это произошло при нормальном завершении процесса.
> Смотри также --------------------------------
Описание функции ехес в perlfunc(1) страница руководства ехес1р(2) вашей системы (если есть); рецепт 16.2.
Вы хотите запустить другую программу и либо прочитать ее вывод, либо предоставить входные данные.
Решение
Вызовите open с символом | в начале или конце строки. Чтобы прочитать вывод программы, поставьте | в конце:
$pid = open(README, "program arguments |") or die "Couldn't fork: $!\n";
while () {
# ...
} close(README) or die "Couldn't close: $!\n"; Чтобы передать данные, поставьте | в начале:
$pid = open(WRITEME, "| program arguments") or die "Couldn't fork: $!\n";
print WRITEME "data\n":
close(WRITEME) or die "Couldn't close: $!\n";
Комментарий
При чтении происходящее напоминает '...', разве что на этот раз у вас имеется идентификатор процесса и файловый манипулятор. Функция open также использует командный интерпретатор, если встречает в аргументе метасимволы, и не использует в противном случае.
Однако в некоторых ситуациях это нежелательно. Конвейерные вызовы open, в которых участвуют непроверенные пользовательские данные, ненадежны при работе в режиме меченых данных или в ситуациях, требующих абсолютной уверенности. Рецепт 19.6 показывает,
Обратите внимание на явный вызов close для файлового манипулятора. Когда функция open используется для подключения файлового манипулятора к порожденному процессу, Perl запоминает этот факт и при закрытии манипулятора автоматически переходит в ожидание
$pid = open(F, "sleep 100000]"); # Производный процесс приостановлен
close(F); # Родитель надолго задумался Чтобы избежать этого, уничтожьте производный процесс по значению PID, полу-ченному от open, или воспользуйтесь конструкцией pipe-fork-exec (см. рецепт 16.10).
При попытке записать данные в завершившийся процесс, ваш процесс получит сигнал SIGPIPE. По умолчанию этот сигнал убивает ваш процесс, поэтому про-граммист-параноик на всякий случай установит обработчик SIGPIPE.
Если вы хотите запустить другую программу и предоставить содержимое ее STDIN, используется аналогичная конструкция: |
$pid = open(WRITEME, "| program args");
print WRITEME "hello\n"; # Программа получит hello\n в STDIN
close(WRITEME); # Программа получит EOF в STDIN Символ | в начале аргумента функции open, определяющего имя файла, сооб-и щает Perl о необходимости запустить другой процесс. Файловый манипулятор, от-крытый функцией open, подключается к STDIN порожденного процесса. Все,что вы запишете в этот манипул
Описанная методика может применяться для изменения нормального вывода вашей программы. Например, для автоматической обработки всех данных утнли-_ той постраничного вывода используется фрагмент вида:
$pager = $ENV{PAGER} || '/usr/bin/less'; # XXX: может не существовать
open(STDOUT, "| $радег"); Теперь все данные, направленные в стандартный вывод, будут автоматически проходить через утилиту постраничного вывода. Вам не придется исправлять дру-гие части программы.
Как и при открытии процесса для чтения, в тексте, передаваемом командному интерпретатору, происходит расширение метасимволов. Чтобы избежать обращения к интерпретатору, следует воспользоваться решением, аналогичным приведенному выше. Как и прежде, род
При использовании сцепленных открытий всегда проверяйте значения, возвращаемые open и close, не ограничиваясь одним open. Дело в том, что возвращаемое значение open не говорит о том, была ли команда успешно запущена. При сцепленном открытии команда вы
К тому моменту, когда порожденный процесс пытается выполнить команду ехес, он уже является самостоятельно планируемым. Следовательно, если команда не будет найдена, практически не существует возможности сообщить об этом функции open, поскольку она при
Проверка значения, возвращаемого close, позволяет узнать, успешно ли выполнилась команда. Если порожденный процесс завершается с ненулевым кодом (что произойдет в случае, если команда не найдена), то close возвращает false, a переменной $? присваивает
> Смотри также --------------------------------
Описание функции open в perlfunc(1); рецепты 16.10; 16.15; 16.19; 19.6.
Требуется обработать выходные данные вашей программы без написания отдельного фильтра.
Решение
Присоедините фильтр с помощью разветвляющего (forking) вызова open. Например, в следующем фрагменте вывод программы ограничивается сотней строк:
head(100);
while (о) { print;
}
sub head {
my $lines = shift 11 20;
return if $pid = open(STDOUT, "|-");
die "cannot fork: $!" unless defined $pid;
while () {
print;
last unless --$lines ;
} exit;
}
Комментарий
Создать выходной фильтр несложно - достаточно открыть STDOUT разветвляющим вызовом open, а затем позволить порожденному процессу фильтровать STDIN в STDOUT и внести те изменения, которые он посчитает нужным. Обратите внимание: выходной фильтр устанавливае
Все подобные фильтры должны устанавливаться в порядке очередности стека -последний установленный фильтр работает первым.
Рассмотрим пример, в котором используются два выходных фильтра. Первый фильтр нумерует строки; второй - снабжает их символами цитирования (как в сообщениях электронной почты). Для файла /etc/motd результат выглядит примерно так:
1: > Welcome to Linux, version 2.0.33 on a i686
2: >
3: > "The software required 'Windows 95 or better',
4: > so I installed Linux."
Если изменить порядок установки фильтров, вы получите следующий результат:
> 1: Welcome to Linux, Kernel version 2.0.33 on a i686 >
2:
> 3: "The software required 'Windows 95 or better', >
4: so I installed Linux."
Исходный текст программы приведен в примере 16.1. Пример 16.1. qnumcat
#!/usr/bin/perl
# qnumcat - установка сцепленных выходных фильтров
number(); # Установить для STDOUT нумерующий фильтр
quote(); # Установить для STDOUT цитирующий фильтр
while (<>) { # Имитировать /bin/cat print;
}
close STDOUT; # Вежливо сообщить потомкам о завершении exit;
sub number {
my $pid;
return if $pid = open(STDOUT, "|-");
die "cannot fork: $!" unless defined $pid;
while () { printf "%d: %s", $., $_ } exit;
}
sub quote { my $pid;
return if $pid = open(STDOUT, "|-");
die "cannot fork: $!" unless defined $pid;
while () { print "> $_" } exit;
} Как и при любых разветвлениях, для миллиона процессов такое решение не подойдет, но для пары (или даже нескольких десятков) процессов расходы будут небольшими. Если ваша система изначально проектировалась как многозадачная (как UNIX), все обойдется де
> Смотри также --------------------------------
Описание функции open в perlfunc(1), рецепт 16.4.
Ваша программа умеет работать лишь с обычным текстом в локальных файлах. Однако возникла необходимость работать с экзотическими файловыми форматами - например, сжатыми файлами или Web-документами, заданными в виде URL.
Решение
Воспользуйтесь удобными средствами Perl для работы с каналами и замените имена входных файлов каналами перед тем, как открывать их.
Например, следующий фрагмент автоматически восстанавливает архивные файлы, обработанные утилитой gzip:
@ARGV = map { /\.(gz|Z)$/ ? "gzip -de $_ |" : $_ } @ARGV;
while (<>) { # .......
}
А чтобы получить содержимое URL перед его обработкой, воспользуйтесь программой GET из модуля LWP (см. главу 20 "Автоматизация в Web"):
@ARGV = mар { mft"\w+://# ? "GET $_ |" : $_ } @ARGV;
while (<>) { # .......
} Конечно, вместо HTML-кода можно принять простой текст. Для этого достаточно воспользоваться другой командой (например, lynx -dump).
Комментарий
Как показано в рецепте 16.1, встроенная функция Perl open очень удобна: каналы открываются в Perl так же, как и обычные файлы. Если то, что вы открываете, похоже на канал, Perl открывает его как канал. Мы используем эту особенность и включаем в имя файла
Эта методика применима и в других ситуациях. Допустим, вы хотите прочитать/etc/passwd, если компьютер не использует NIS, и вывод ypcat passwd в противном случае. Мы определяем факт использования NIS по выходным данным программы domainname, после чего выби
$pwdinfo = 'domainname' =~ /"(\(none\))?$/ ? '< /etc/passwd' : 'ypcat passwd |';
open(PWD, $pwdinfo) or die "can't open $pwdinfo: $!"; Но и это еще не все! Даже если вы не собирались встраивать подобные возможности в свою программу, Perl делает это за вас! Представьте себе фрагмент вида:
print "File, please? ";
chomp($file = <>);
open (FH, $file) or die "can't open $file: $!"; Пользователь может ввести как обычное имя файла, так и строку вида "webget http://www. perl. corn |" - и ваша программа вдруг начинает получать выходные данные от webget! А если ввести всего один символ, дефис (-), то при открытии для чтения будет инт
В рецепте 7.7 эта методика использовалась для автоматизации обработки ARGV.
> Смотри также --------------------------------
Рецепты 7.7; 16.4.
Вы хотите выполнить программу с помощью system, '. . . ' или open, но содержимое ее STDERR не должно выводиться в ваш STDERR. Необходимо либо игнорировать содержимое STDERR, либо сохранять его отдельно.
Решение
Воспользуйтесь числовым синтаксисом перенаправления и дублирования для файловых дескрипторов. Для упрощения примеров мы не проверяем возвращаемое значение open, но вы обязательно должны делать это в своих программах! Одновременное сохранение STDERR и STDO
$output = 'cmd 2>&1'; # Для '... '
# или
$pid = open(PH, "cmd 2>&1 |"); # Для open
while () { } # Чтение Сохранение STDOUT с игнорированием STDERR:
$output = 'cmd 2>/dev/null'; # Для '...'
# или
$pid = open(PH, "cmd 2>/dev/null |"); # Для open
while () { } # Чтение
Сохранение STDERR с игнорированием STDOUT:
$output = 'cmd 2>&1 1>/dev/null'; # Для '...'
# или
$pid = open(PH, "cmd 2>&1 1>/dev/null |"); # Для open
while () { } # Чтение Замена STDOUT и STDERR команды, то есть сохранение STDERR и направление STDOUT в старый STDERR:
$output = 'cmd 3>&1 1>&2 2>&3 3>&-'; # Для '...'
# или
$pid = open(PH, "cmd 3>&1 1>&2 2>&3 3>&- "); # Для open
while () { } # Чтение Чтобы организовать раздельное чтение STDOUT и STDERR команды, проще и надежнее всего будет перенаправить их в разные файлы, а затем прочитать из этих файлов после завершения команды:
system("prog args 1>/tmp/program,stdout 2>/tmp/program.stderr");
Комментарий
При выполнении команды оператором '...', сцепленным вызовом open или syste' для одной строки Perl проверяет наличие символов, имеющих особый смысл для командного интерпретатора. Это позволяет перенаправить файловые дескрипторы повой программы. STDIN соотв
Ниже приведена таблица некоторых интересных перенаправлений файловых дескрипторов.
Значение
01>/dev/null Игнорировать STDOUT
2>/dev/null Игнорировать STDERR
2>&1 Направить STDERR в STDOUT
2>&- Закрыть STDERR (не рекомендуется)
З<>/dev/tty Связать файловый дескриптор 3 с /dev/tty в режиме чтения/записи
На основании этой таблицы мы рассмотрим самый сложный вариант перена-правления в решении:
$output = 'cmd 3>&1 1>&2 2>&3 3>&-'; Он состоит из четырех этапов.
Этап 1: 3>&1
Скопировать файловый дескриптор 1 в новый дескриптор 3. Прежнее место назначения STDOUT сохраняется в только что открытом дескрипторе.
Этап 2: 1>&2
Направить STDOUT по месту назначения STDERR. В дескрипторе 3 остается
прежнее значение STDOUT.
Этап 3: 2>&3
Скопировать файловый дескриптор 3 в дескриптор 2. Данные STDERR будут
поступать туда, куда раньше поступали данные STDOUT.
Этап 4: 3>&-
Перемещение потоков закончено, и мы закрываем временный файловый дескриптор. Это позволяет избежать "утечки" дескрипторов.
Если подобные цепочки сбивают вас с толку, взгляните на них как на обычные переменные и операторы присваивания. Пусть переменная $fd1 соответствует STDOUT, a $fd2 - STDERR. Чтобы поменять значения двух переменных, понадобится временная переменная для хран
$fd3 = $fd1;
$fd1 = $fd2;
$fd2 = $fd3;
$fd3 = undef;
Когда все будет сказано и сделано, возвращаемая оператором '. . . ' строка будет соответствовать STDERR выполняемой команды, a STDOUT будет напран лен в прежний STDERR.
Во всех примерах важна последовательность выполнения. Это связано с тем что командный интерпретатор обрабатывает перенаправления файловых дескрипторов слева направо.
system("prog args 1>tmpfile 2>&1");
system("prog args 2>&1 1>tmpfile");