<<< Headline | Index | http | bash | basics | x11 | net | vpn | humor | sles | cXX | php | db | perl | soft | unsorted | hw | ppp | tips | linux | fbsd | mail
[Timeline] [View Photos] [rtfm] [Search] [Index by Title] [Index by Date]
perl => pb16: == post:perl/posts/pb16


  Управление процессами и межпроцессные взаимодействия

Введение
Многие из нас относятся к Perl по-своему, но большинство считает его чем-то вроде "клея", объединяющего разнородные компоненты. Эта глава посвящена командам и отдельным процессам - их созданию, взаимодействию и завершению. Итак, речь пойдет о системном программировании. В области системного программирования на Perl, как обычно, все простое упрощается, а все сложное становится доступным. Если вы хотите работать на высоком уровне, Perl с радостью вам поможет. Если вы собираетесь закатать рукава и заняться низкоуровневым программированием, уподобившись хакерам С, -что ж, возможно и это.
Perl позволяет очень близко подобраться к системе, но при этом могут возникнуть некоторые проблемы переносимости. Из всей книги эта глава в наибольшей степени ориентирована на UNIX. Изложенный материал чрезвычайно полезен для тех, кто работает в UNIX-системах, и в меньшей степени - для всех остальных. Рассматриваемые в ней возможности не являются универсальными, как, например, строки, числа или базовая арифметика. Большинство базовых операций более или менее одинаково работает повсюду. Но если вы не работаете в системе семейства UNIX или другой POSIX-совместимой системе, многие интересные возможности у вас будут работать иначе (или не будут работать вообще). В сомнительных ситуациях обращайтесь к документации, прилагаемой к вашей версии Perl.
Создание процессов
В этой главе рассматриваются порожденные процессы. Иногда вы просто выполняете автономную команду (с помощью system) и оставляете созданный процесс на произвол судьбы. В других случаях приходится сохранять тесную связь с созданным процессом, скармливать ему тщательно отфильтрованные данные или управлять его потоком вывода ('...' и конвейерные вызовы open). Наконец, даже без запуска нового процесса вызов ехес позволяет заменить текущую программу чем-то совершенно новым.
Сначала мы рассмотрим самые переносимые и распространенные операции управления процессами: '...', system, open и операции с хэшем %SIG. Здесь нет ничего сложного, но мы не остановимся па этом и покажем, что делать, когда простые решения не подходят. Допустим, вы хотите прервать свою программу в тот момент, когда она запустила другую программу. Или вам захотелось отделить стандартный поток ошибок порожденного процесса от его стандартного вывода. Или вы собираетесь одновременно управлять и как вводом, так и выводом программы. Или вы решили воспользоваться преимуществами многозадачности и разбить свою программу на несколько одновременно работающих процессов, взаимодействующих друг с другом. В подобных ситуациях приходится обращаться к системным функциям: pipe, fork и ехес. Функция pipe создает два взаимосвязанных манипулятора, записывающий и читающий; при этом все данные, записываемые в первый, могут быть прочитаны из первого. Функция fork является основой многозадачности, по, к сожалению, она не поддерживается некоторыми системами, не входящими в семейство UNIX. Функция создает процесс-дубликат, который практически во всех отношениях идентичен своему родителю, включая значения переменных и открытые файлы. Самые заметные изменения - идентификатор процесса и идентификатор родительского процесса. Новые программы запускаются функцией fork, после чего функция ехес заменяет программу порожденного процесса чем-то другим. Функции fork и ехес не всегда используются вместе, поэтому наличие отдельных примитивов оказывается более выразительным и мощным по сравнению с ситуацией, когда ваши возможности ограничиваются выполнением system. На практике fork по отдельности используется чаще, чем с ехес.
При уничтожении порожденного процесса его память возвращается операционной системе, но соответствующий элемент таблицы процессов не освобождается. Благодаря этому родитель может проверить статус завершения всех порожденных процессов. Процессы, которые умерли, но не были удалены из таблицы процессов, называются зомби; их следует своевременно удалять, чтобы они не заполнили всю таблицу процессов. Оператор '.,,', а также функции system и open автоматически следят за этим и работают в большинстве систем, не входящих в семейство UNIX. При выходе за рамки этих простых переносимых функций и запуске программ с помощью низкоуровневых примитивов возникают дополнительные хлопоты. Кроме того, не стоит забывать и о сигналах.
Сигналы
Ваш процесс узнает о смерти созданного им порожденного процесса с помощью сигнала. Сигналы представляют собой нечто вроде оповещений, доставляемых операционной системой. Они сообщают о произошедших ошибках (когда ядро говорит: "Не трогай эту область памяти!") и событиях (смерть порожденного процесса, тайм-аут процесса, прерывание по Ctrl+C). При ручном запуске процесса обычно указывается подпрограмма, которая должна вызываться при завершении потомка.
Каждый процесс имеет стандартные обработчики для всех возможных сигналов. Вы можете установить свой собственный обработчик или изменить отношение программы к большинству сигналов. Не изменяются только 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 приведены в рецепте 16.19.
Имена сигналов существуют лишь для удобства программистов. С каждым сигналом связано определенное число, используемое операционной системой вместо имени. Хотя мы говорим о сигнале SIGCHLD, операционная система опознает его по номеру - например, 20 (в зависимости от операционной системы). Perl преобразует номера сигналов в имена, поэтому вы можете работать с именами сигналов.
Обработка сигналов рассматривается в рецептах 16.7, 16.15, 16.18, 16.20 и 16.21.


  16.1. Получение вывода от программы


Проблема

Требуется запустить программу и сохранить ее вывод в переменной.

Решение

Воспользуйтесь либо оператором '...':

$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.


  16.2. Запуск другой программы


Проблема

Вы хотите запустить другую программу из своей, дождаться ее завершения и затем продолжить работу. Другая программа должна использовать те же STDIN и STDOUT, что и основная.

Решение

Вызовите функцию system со строковым аргументом, который интерпретируется как командная строка:

$status = system("vi $myfile");

Если вы не хотите привлекать командный интерпретатор, передайте syster список:

$status = system("vi", $myfile);
Комментарий

Функция system обеспечивает самую простую и универсальную возможность запуска других программ в Perl. Она не возвращает выходные данные внешней программы, как '. . . ' или open. Вместо этого ее возвращаемое значение (фактически) совпадает с кодом завершения программы. Во время работы новой программы основная приостанавливается, поэтому новая программа может взаимодейство-вать с пользователем посредством чтения данных из STDIN и записи в STDOUT. При вызове с одним аргументом функция 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 или переменную $9:

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 работает иначе; такая ситуация возникает при создании двух ссылок на файл (см. описание ехрп). По этой причине не следует полагать, что $0 всегда содержит имя вызванной программы.
Если вы хотите подсунуть запускаемой программе другое имя, укажите настоящий путь в виде "косвенного объекта" перед списком, передаваемым 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.


  16.3. Замена текущей программы

Проблема

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

Решение

Воспользуйтесь встроенной функцией 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), которая заменяет текущую программу другой без изменения идентификатор процесса. Программа, вызвавшая ехес, стирается, а ее место в таблице процессов операционной системы занимает программа, указанная в качестве аргумента ехес. В результате новая программа сохраняет тот же идентификатор процесса ($$), что и у исходной программы. Если указанную программу запустить не удалось, ехес возвращает false, а исходная программа продолжает работу. Не забывайте проверять такую ситуацию.
При переходе к другой программе с помощью ехес не будут автоматически вызваны ни блоки END, ни деструкторы объектов, как бы это произошло при нормальном завершении процесса.

> Смотри также --------------------------------
Описание функции ехес в perlfunc(1) страница руководства ехес1р(2) вашей системы (если есть); рецепт 16.2.


  16.4. Чтение или запись в другой программе

Проблема

Вы хотите запустить другую программу и либо прочитать ее вывод, либо предоставить входные данные.

Решение

Вызовите 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 показывает, как имитировать эффект конвейерных вызовов open без риска, связанного с использованием командного интерпретатора.
Обратите внимание на явный вызов close для файлового манипулятора. Когда функция open используется для подключения файлового манипулятора к порожденному процессу, Perl запоминает этот факт и при закрытии манипулятора автоматически переходит в ожидание. Если порожденный процесс к этому моменту не завершился, 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 порожденного процесса. Все,что вы запишете в этот манипулятор, может быть прочитано процессом из STDIN. После закрытия манипулятора (close) при следующей попытке чтения из STDIN порожденный процесс получит eof. Описанная методика может применяться для изменения нормального вывода вашей программы. Например, для автоматической обработки всех данных утнли-_ той постраничного вывода используется фрагмент вида:

$pager = $env{pager} || '/usr/bin/less'; # xxx: может не существовать
open(STDOUT, "| $радег");


Теперь все данные, направленные в стандартный вывод, будут автоматически проходить через утилиту постраничного вывода. Вам не придется исправлять дру-гие части программы.
Как и при открытии процесса для чтения, в тексте, передаваемом командному интерпретатору, происходит расширение метасимволов. Чтобы избежать обращения к интерпретатору, следует воспользоваться
Решение
м, аналогичным приведенному выше. Как и прежде, родитель должен помнить о close. При закрытии файлового манипулятора, подключенного к порожденному процессу, родитель блокируется до завершения потомка. Если порожденный процесс не завершается, то и закрытие не произойдет. Приходится либо заранее убивать порожденный процесс, либо использовать низкоуровневый сценарий pipe-fork-exec.
При использовании сцепленных открытий всегда проверяйте значения, возвращаемые open и close, не ограничиваясь одним open. Дело в том, что возвращаемое значение open не говорит о том, была ли команда успешно запущена. При сцепленном открытии команда выполняется вызовом fork для порожденного процесса. Если возможности создания процессов в системе не исчерпаны, fork немедленно возвращает PID порожденного процесса.
К тому моменту, когда порожденный процесс пытается выполнить команду ехес, он уже является самостоятельно планируемым. Следовательно, если команда не будет найдена, практически не существует возможности сообщить об этом функции open, поскольку она принадлежит другому процессу!
Проверка значения, возвращаемого close, позволяет узнать, успешно ли выполнилась команда. Если порожденный процесс завершается с ненулевым кодом (что произойдет в случае, если команда не найдена), то close возвращает false, a переменной $? присваивается статус ожидания процесса. Об интерпретации содержимого этой переменной рассказано в рецепте 16.2.

> Смотри также --------------------------------
Описание функции open в perlfunc(1); рецепты 16.10; 16.15; 16.19; 19.6.


  16.5. Фильтрация выходных данных

Проблема

Требуется обработать выходные данные вашей программы без написания отдельного фильтра.

Решение

Присоедините фильтр с помощью разветвляющего (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.


  16.6. Предварительная обработка ввода

Проблема

Ваша программа умеет работать лишь с обычным текстом в локальных файлах. Однако возникла необходимость работать с экзотическими файловыми форматами - например, сжатыми файлами или 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 открывает его как канал. Мы используем эту особенность и включаем в имя файла восстановление архива или иную предварительную обработку. Например, файл "OQtails.gz" превращается .в "gzcat -de 09tails.gz|". Эта методика применима и в других ситуациях. Допустим, вы хотите прочитать/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.


  16.7. Чтение содержимого STDERR

Проблема

Вы хотите выполнить программу с помощью system, '. . . ' или open, но содержимое ее STDERR не должно выводиться в ваш STDERR. Необходимо либо игнорировать содержимое STDERR, либо сохранять его отдельно.

Решение

Воспользуйтесь числовым синтаксисом перенаправления и дублирования для файловых дескрипторов. Для упрощения примеров мы не проверяем возвращаемое значение open, но вы обязательно должны делать это в своих программах! Одновременное сохранение STDERR и STDOUT:

$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 соответствует файловому дескриптору с номером О, STDOUT - 1, a STDERR - 2. Например, конструкция 2>файл перенаправляет STDERR в файл. Для перенаправления в файловый дескриптор используется специальная конструкция &N, где N - номер файлового дескриптора. Следовательно, 2>&1 направляет STDERR в STDOUT.
Ниже приведена таблица некоторых интересных перенаправлений файловых дескрипторов.

Значение
0
1>/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");



  16.8. Управление потоками ввода и вывода другой программы

Проблема

Вы хотите управлять как входными, так и выходными данными другой программы. Функция open позволяет решить одну из этих задач, но не обе сразу.

Решение

Воспользуйтесь стандартным модулем 1РС::Ореп2:

use IPC::0реп2;
open2(*README, *WRITEME, $program);
print WRITEME "here's your input\n";
$output = ;
close(WRITEME);
close(README);

Комментарий

Желание управлять вводом и выводом другой программы возникает очень часто, однако за ним таится на удивление много опасностей. Поэтому вам не удастся вызвать open в виде:

open(DOUBLE_HANDLE, "| программа аргументы |") # НЕВЕРНО

Большая часть трудностей связана с буферизацией. Поскольку в общем случае нельзя заставить другую программу использовать небуферизованный вывод, нет гарантии, что операции чтения не будут блокироваться. Если блокировка ввода устанавливается одновременно с тем, как другой процесс заблокируется в ожидании вывода, возникает состояние взаимной блокировки (deadlock). Процессы входят в клинч, пока кто-нибудь не убьет их или не перезагрузит компьютер. Если вы можете управлять буферизацией другого процесса (потому что вы сами написали программу и знаете, как она работает), возможно, вам поможет модуль 1РС::Ореп2. Первые два аргумента функции ореп2, экспортируемой 1РС::Ореп2 в ваше пространство имен, представляют собой файловые манипуляторы. Либо передавайте ссылки на typeglobs, как это сделано в решении, либо создайте собственные объекты IO::Handle и передайте их:

use IPC::0реn2;
use 10::Handle;
($reader, $writer) = (10: :Handle->new, 10: :Handle->new);
open2($reader, $writer, $program);

Чтобы передать объекты, необходимо предварительно создать их (например, функцией 10: :Handle->new). Если передаваемые переменные не содержат файловых манипуляторов, функция ореп2 не создаст их за вас.
Другой вариант - передать аргументы вида
 "<&OTHERFILEHANDLE" или ">&OTHERFILEHANDLE" 
, определяющие существующие файловые манипуляторы для порожденных процессов. Эти файловые манипуляторы не обязаны находиться под контролем вашей программы; они могут быть подключены к другим программам, файлам или сокетам.
Программа может задаваться в виде списка (где первый элемент определяет имя программы, а остальные элементы - аргументы программы) или в виде отдельной строки (передаваемой интерпретатору в качестве команды запуска программы). Если вы также хотите управлять потоком STDERR программы, воспользуйтесь модулем IPC::Open3 (см. следующий рецепт). Если произойдет ошибка, возврат из ореп2 и орепЗ не происходит. Вместо этого вызывается die с сообщением об ошибке, которое начинается с "о pen 2" или "орепЗ". Для проверки ошибок следует использовать конструкцию eval БЛОК:

eval {
open2($readme, $writeme, @program_and_arguments);
};
if ($@) {
if ($@ =- /~open2/) {
warn "open2 failed: $!\n$@\n";
return;
} die; # Заново инициировать непредвиденное исключение
1





> Смотри также -------------------------------- Документация по стандартным модулям 1РС::Ореn2 и IPC::Open3; рецепт 10.12; описание функции eval в perlfunc(1), описание переменной $@ в разделе "Special Global Variables" perlvar(1).


  16.9. Управление потоками ввода, вывода и ошибок другой программы


Проблема

Вы хотите полностью управлять потоками ввода, вывода и ошибок запускаемой команды.

Решение

Аккуратно воспользуйтесь стандартным модулем IPC::Open3, возможно - в сочетании с модулем IO::Select (появившимся в версии 5.004).

Комментарий

Если вас интересует лишь один из потоков STDIN, STDOUT или STDOUT программы, задача решается просто. Но если потребуется управлять двумя и более потоками, сложность резко возрастает. Мультиплексирование нескольких потоков ввода/вывода всегда выглядело довольно уродливо. Существует простое обходное
Решение
:

@аll = '($cmd sed -e 's/"/stdout: /' ) 2>&1';
for (@all) { push @{ s/stdout: // ? \(@out lines : \@errlines }, $_ }
print "STDOUT:\n", @outlines, "\n";
print "STDERR;\n", @errlines, "\n";

Если утилита sed не установлена в вашей системе, то в простых случаях вроде показанного можно обойтись командой perl -ре, которая работает практически так же. Однако то, что здесь происходит, в действительности нельзя считать параллельными вычислениями. Мы всего лишь помечаем строки STDOUT префиксом "stdout:" и затем удаляем их после чтения всего содержимого STDOUT и STDERR, сгенерированного программой.
Кроме того, можно воспользоваться стандартным модулем IPC::Open3. Как ни странно, аргументы функции IPC::Open3 следуют в другом порядке, нежели в 1РС::Ореп2.
open3("WRITEHANDLE, *READHANDLE, *ERRHANDLE, "ЗАПУСКАЕМАЯ ПРОГРАММА"):

Открываются широкие потенциальные возможности для создания хаоса - еще более широкие, чем при использовании о pen 2. Если попытаться прочитать STDERR программы, когда она пытается записать несколько буферов в STDOUT, процесс записи будет заблокирован из-за заполнения буферов, а чтение заблоки-|)уется из-за отсутствия данных.
Чтобы избежать взаимной блокировки, можно имитировать ореnЗ с помощью ork, open и ехес; сделать все файловые манипуляторы небуферизованными и использовать sysread, syswrite и select, чтобы решить, из какого доступного для чтения манипулятора следует прочитать байт. Однако ваша программа становится медленной и громоздкой, к тому же при этом ие решается классическая проблема взаимной блокировки ореп2, при которой каждая программа ждет поступления данных от другой стороны:

use IPC::0реn3;
$pid - open3(*HIS_IN, *HIS_OUT, *HIS_ERR, $cmd);
close(HIS_IN); # Передать порожденному процессу EOF или данные
@outlines = ; # Читать до конца файла
@errlines = ; # XXX: Возможная блокировка
# при больших объемах
print "STDOUT:\n", @outlines, "\n'
print "STDERR:\n", @errlines, "\n'

Кроме того (как будто одной взаимной блокировки недостаточно), такое
Решение
чревато нетривиальными ошибками. Существуют по крайней мере три неприятных ситуации: первая - когда и родитель и потомок пытаются читать одновременно, вызывая взаимную блокировку. Вторая - когда заполнение буферов заставляет потомка блокироваться при попытке записи в STDERR, тогда как родитель блокируется при попытке чтения из STDOUT потомка. Третья - когда заполнение буферов заставляет родителя блокировать запись в STDIN потомка, а потомок блокируется при записи в STDOUT или STDERR. Первая проблема в общем случае не решается, хотя ее можно обойти, создавая таймеры функцией alarm и предотвращая перезапуск блокирующих операций при получении сигнала SIGALRM.
Мы используем модуль IO::Select, чтобы узнать, из каких файловых манипуляторов можно прочитать данные (для этой цели можно использовать встроенную функцию select). Это решает вторую, но не третью проблему. Для решения третьей проблемы также потребуются alarm и SIGALRM.
Если вы хотите отправить программе входные данные, прочитать ее вывод и затем либо прочитать, либо проигнорировать ошибки, работы заметно прибавится (см. пример 16.2). Пример 16.2. cmd3sel

#!/usr/bin/perl
# cmd3sel - управление всеми тремя потоками порожденного процесса
# (ввод, вывод и ошибки).
use IPC::0pen3;
use 10::Select;
$cmd = "grep vt33 /none/such - /etc/termcap";
$pid = open3(*cmd_in, *cmd_out, *cmd_err, $cmd);
$SIG{CHLD} = sub {
print "REAPER: status $? on $pid\n" if waitpid($pid, 0) > 0
};
print CMD_IN "This line has a vt33 lurking in it\n";
close(CMD_IN);
$selector = 10::select->new();
$selector->add(*CMD_ERR, *CMD_OUT);
while (@ready = $selector->can_read) { foreach $fh (@ready) {
if (fileno($fh) == fileno(cmd_err))
{print "STDERR: ", scalar } else
{print "STDOUT: ", scalar } $selector->remove($fh) if eof($fh);
}
}
close(CMD_CUT);
close(CMD_ERR);


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

> Смотри также -------------------------------
Документация по стандартным модулям IO::Select; IPC::Open2 и IPC::Open3: описание функции alarm в perlfunc(1); рецепты 16.8; 16.15-16.16.


  16.10. Взаимодействие между родственными процессами


Проблема

Имеются два взаимосвязанных процесса, которые должны обмениваться данными. Вам требуется более высокая степень контроля по сравнению с той, что обеспечивают open, system и '...'.

Решение
Воспользуйтесь pipe, а затем - fork:
pipe(READER, WRITER);
if (fork) {
# Выполнить родительский код, в котором происходит либо чтение,
# либо запись (что-то одно).
} else {
# Выполнить код потомка, в котором происходит либо чтение,
# либо запись (что-то одно).
}
Либо используйте особую форму open:
if ($pid = open(child, "|-")) {
# Выполнить родительский код, передающий данные потомку
} else {
die "cannot fork: $!" unless defined $pid;
# Иначе выполнить код потомка, принимающий данные от родителя
}
Или по-другому:
if ($pid = open(child, "-|")) {
# Выполнить родительский код, принимающий данные от потомка
} else {
die ''cannot fork: $!" unless defined $pid;
# Иначе выполнить код потомка, передающий данные родителю
}

Комментарий

Канал представляет собой два файловых манипулятора, связанных так, что записанные в один файловый манипулятор данные могут быть прочитаны из другого. Функция pipe создает два манипулятора, связанных в канал; первый (приемник) предназначен для чтения, а второй (передатчик) - для записи. Хотя вы не сможете взять два существующих манипулятора и объединить их в канал, функция pipe часто используется при обмене данными между процессами. Один процесс создает пару манипуляторов функцией pipe, после чего создает потомка с помощью fork; в результате возникают два разных процесса, выполняющих одну и ту же программу, каждый из которых обладает копией связанных манипуляторов.
Неважно, какой процесс будет приемником, а какой - передатчиком; когда процесс начинает играть одну из этих ролей, его напарнику достается другая. Такой обмен данными может быть только односторонним (но не бросайте читать!)
Мы воспользуемся модулем IO::Handle, в котором нас интересует метод autoflushO (если вы предпочитаете более эффективные решения, воспользуйтесь
Решение
м с select, описанным в главе 7). Если этого не сделать, наша отдельная строка вывода застрянет в канале и не доберется до другого конца до закрытия канала. Версия родителя, передающего данные потомку, приведена в примере 16.3. Пример 16.3. pipel

#!/usr/bin/perl -w
# pipel - применение pipe и fork для отправки данных родителем потомку
use 10::Handle;
pipe(READER, WRITER);
WRITER->autoflush(1);
if ($pid = fork) {
close READER;
print WRITER "Parent Pid $$ is sending this\n":
close WRITER;
waitpid($pid,0);
} else {
die "cannot fork: $!" unless defined $pid;
close WRITER;
chomp($line = );
print "Child Pid $$ ]ust read this: '$line'\n";
close READER; # Все равно это произойдет exit;
}

В примерах этого рецепта основная проверка ошибок была оставлена читателю для самостоятельной работы. Мы так поступили для того, чтобы взаимодействие функции стало более наглядным. В реальной жизни проверяются возвращаемые значения всех вызовов системных функции.
В примере 16.4 показана версия потомка, передающего данные родителю. Пример 16.4. pipe2
#!/usr/bin/perl -w
# pipe2 - применение pipe и fork для передачи данных потомком родителю
use 10::Handle;
pipe(READER, WRITER);
WRITER->autoflush(1);
if ($pid = fork) {
close WRITER;
chomp($line = );
print "Parent Pid $$ just read this: '$line'\n";
close READER;
waitpid($pid,0);
} else {
die "cannot fork: $!" unless defined $pid;
close READER:
print WRITER "Child Pid $$ is sending this\n";
close WRITER; # Все равно это произойдет
exit;
}

Обычно обе половины входят в цикл и приемник продолжает читать до конца файла. Это происходит до тех пор, пока передатчик не закроет канал или не завершится.
Поскольку манипуляторы каналов работают лишь в одном направлении, каждый процесс использует лишь один канал из пары и закрывает неиспользуемый манипулятор. Причина, по которой это делается, нетривиальна; представьте себе ситуацию, при которой принимающий процесс не закрыл передающий манипулятор. Если после этого передающий процесс завершится, пока принимающий процесс пытается что-нибудь прочитать, последний намертво "зависнет". Система не может сообщить приемнику о том, что данных для чтения больше не будет, пока не будут закрыты все копии передающего манипулятора. Функция open, получая в качестве второго аргумента "-1" или " | =", неявно вызывает pipe и fork. Это несколько упрощает приведенный выше фрагмент. Порожденный процесс общается с родителем через stdin или stdout в зависимости от того, какая строка была использована, "- " или " | -". При подобном применении open, когда родитель хочет передать данные потомку, он использует нечто похожее на пример 16.5. Пример 16.5. pipe3

#!/usr/bin/perl -w
# pipe3 - применение разветвляющего вызова open
# для передачи данных от родителя к потомку
use 10::Handle;
if ($pid = open(child, "|-")) {
CHILD->autoflush(1);
print CHILD "Parent Pid $$ is sending this\n";
close(CHILD);
} else {
die "cannot fork: $!" unless defined $pid;
chomp($line = );
print "Child Pid $$ just read this: '$line'\n";
exit;
}


Поскольку STDIN потомка уже подключен к родителю, потомок может запустить через ехес другую программу, читающую данные из стандартного ввода - например, Ipr. Это полезная и часто используемая возможность.
Если потомок захочет передать данные родителю, он делает нечто похожее на пример 16.6. Пример 16.6. pipe4

#!/usr/bin/perl -w
# pipe4 - применение разветвляющего вызова open
# для передачи данных от потомка к родителю
use 10::Handle;
if ($pid = open(child, "-|")) {
chomp($line = );
print "Parent Pid $$ just read this: '$line'\n";
close(CHILD);
} else {
die "cannot fork: $!" unless defined $pid;
STDOUT->autoflush(1);
print STDOUT "Child Pid $$ is sending this\n";
exit;
}

И снова, поскольку STDOUT потомка уже подключен к родителю, потомок может запустить через ехес другую программу, выдающую нечто интересное в его стандартный вывод. Эти данные также будут переданы родителю как ввод от .
При подобном использовании open мы не обязаны вручную вызывать waitpid, поскольку не было явного вызова fork. Однако close вызвать все же надо. В обоих случаях переменная $? содержит статус ожидания порожденного процесса (о том, как интерпретировать это значение, рассказано в рецепте 16.19).
В предыдущих примерах рассматривалась однонаправленная связь. Что делать, если вы хотите, чтобы данные передавались в обе стороны? Дважды вызовите pipe перед вызовом fork. Вам придется следить за тем, кто, что и когда передает, иначе может возникнуть взаимная блокировка (см. пример 16.7). Пример 16.7. pipe5

#!/usr/bin/perl -w
# pipe5 - двусторонний обмен данными с использованием двух каналов
# без применения socketpair
use 10::Handle;
pipe(PARENT_RDR, CHILDJJTR);
plpe(CHILD_RDR, PARENT_WTR);
CHILD_WTR->autoflush(1);
PARENT_WTR->autoflush(1);
if ($pid = fork) {
close PARENT_RDR; close PARENT_WTR;
print CHILD_WTR "Parent Pid $$ is sending this\n";
chomp($line = );
print "Parent Pid $$ just read this: '$line'\n";
close CHILD_RDR;. close CHILD_WTR;
waitpid($pid,0);
} else {
die "cannot fork: $!" unless defined $pid;
close CHILD_RDR; close CHILD_WTR;
chomp($line = );
print "Child Pid $$ just read this: '$line'\n";
print PARENT_WTR "Child Pid $$ is sending this\n":
close PARENT_RDR; close PARENT_WTR;
exit;
Ситуация усложняется. Оказывается, существует специальная системная функция socketpair (см. пример 16.8), которая упрощает предыдущий пример. Она работает аналогично pipe, за исключением того, что оба манипулятора могут использоваться как для приема, так и для передачи.
Пример 16.8. pipe6

#!/usr/bin/perl -w
# pipe6 - двусторонний обмен дпинмми г. применением socketpair
use Socket;
use 10::Handle;
# Мы говорим AF_UNIX, потому что хотя константа *_1_ОСА1_
# соответствует POSIX 1003.1g, на многих компьютерах
# она еще не поддерживается.
socketpair(CHILO, PARENT, AF_UNIX, SOCK_STREAM, PF_UNSPEC) or die "socketpair: $!";
CHILD->autoflush(1);
PARENT->autoflush(1);
if ($pid = fork) {
close PARENT;
print CHILD "Parent Pid $$ is sending this\n";
chomp($line = );
print "Parent Pid $$ just read this: '$line'\n";
close CHILD;
waitpid($pld,0);
} else {
die "cannot fork: $!" unless defined $pid;
close CHILD;
chomp($line = );
print "Child Pid $$ just read this: '$line'\n";
print PARENT "Child Pid $$ is sending this\n";
close PARENT;
exit;
}

В некоторых системах каналы исторически были реализованы как два полузакрытых конца пары сокетов. Фактически реализация pipe(READER, WRITER) выглядела так:

socketpair(READER, WRITER, AF_UNIX, SOCK_STREAM, PF_UNSPEC);
shutdown(READER, 1); # Запретить запись для READER shutdown(WRITER, 0);
# Запретить чтение для WRITER
В ядрах Linux до версии 2.0.34 системная функция shutdown(2) работала неверно. Приходилось запрещать чтение для READER и запись для WRITER.

> Смотри также -------------------------------- Описания всех использованных функций в perlfunc{1) документация по стандартному модулю
IРС::Ореn2; рецепт 16.8.


  16.11. Имитация файла на базе именованного канала


Проблема

Вы хотите, чтобы процесс перехватывал все обращения к файлу. Например, файл -/.plan должен превратиться в программу, которая будет возвращать случайную цитату.

Решение

Воспользуйтесь именованными каналами. Сначала создайте канал (вероятно, г командном интерпретаторе):

% mkfifo /path/to/named.pipe Принимающий фрагмент выглядит так:
open(FIFO, "< /path/to/named.pipe")
or die $!;
while () { print "Got: $_";
} close(FIFO);

Передающий фрагмент выглядит так:
open(FIFO, "> /path/to/named.pipe")
or die $1:
print FIFO "Smoke this.\n";
close(FIFO):

Комментарий

Именованный канал (также встречается термин FIFO) представляет собой специальный файл, используемый в качестве буфера для взаимодействия процессов на одном компьютере. Обычные каналы также позволяют процессам обмениваться данными, но они должны наследовать файловые манипуляторы от своих родителей. Для работы с именованным каналом процессу достаточно знать его имя. В большинстве случаев процессы даже не обязаны сознавать, что они читают данные из FIFO.
Операции чтения и записи для именованных каналов выполняются точно так же, как и для обычных файлов (в отличие от сокетов UNIX, рассматриваемых в главе 17). Данные, записанные в FIFO, буферизуются операционной системой, а затем читаются обратно в порядке записи. Поскольку FIFO играет роль буфера для взаимодействия процессов, открытие канала для чтения блокирует его до тех нор, пока другой процесс не откроет его для записи, и наоборот. Если открыть канал для чтения и записи с помощью режима +< функции open, блокировки (в большинстве систем) не будет, поскольку ваш процесс сможет и принимать, и передавать данные. Давайте посмотрим, как использовать именованный канал, чтобы при каждом запуске finger люди получали разные данные. Чтобы создать именованный канал с именем .plan в основном каталоге, воспользуйтесь mkfifo или mknod:

% mkfifo '/.plan # Есть практически везде
% mknod '/.plan p # На случай, если у вас все же нет mkfifo
В некоторых системах приходится использовать }nknod(8). Имена и местонахождение этих программ могут быть другими - обращайтесь к системной документации. Затем необходимо написать программу, которая будет поставлять данные программам, читающим из файла -/.plan. Мы ограничимся выводом текущей даты и времени (см. пример 16.9). Пример 16.9. dateplan
#!/usr/bin/perl -w
# dateplan - вывод текущей даты и времени в файл .plan
while (1) {
open(FIFO, "> $ENV{HOME}/.plan")
or die "Couldn't open $ENV{HOME}/.plan for writing: $!\n";
print FIFO "The current time is ", scalar(localtime), "\n";
close FIFO;
sleep 1;
}

К сожалению, такое
Решение
работает не всегда, потому что некоторые варианты finger и соответствующие демоны проверяют размер файла .plan перед тем, как пытаться читать из него. Поскольку именованные каналы в файловой системе представлены в виде специальных файлов нулевого размера, некоторые клиенты и серверы не станут открывать именованный канал и читать из него, и наш фокус не удастся.
В примере с .plan демон был передатчиком. Приемники-демоны тоже встречаются не так уж редко. Например, именованный канал может применяться для ведения централизованного журнала, собирающего данные от нескольких процессов. Программа-сервер читает сообщения из именованного канала и записывает их в базу данных или файл. Клиенты передают сообщения в именованный канал. Такая схема избавляет клиентов от хлопот, связанных с логикой передачи данных, и позволяет легко внести необходимые изменения в реализацию механизма передачи. В примере 16.10 приведена простая программа для чтения двухстрочных блоков, где первая строка определяет процесс, а вторая - текст сообщения. Все сообщения от httpd игнорируются, а сообщения от login сохраняются в /var/log/login. Пример 16.10. fifolog

#!/usr/bin/perl -w
# fifolog - чтение и сохранение сообщений из FIFO
use 10::File;
$SIG{ALRM} = sub { close(fifo) }; # Переход к следующему
# процессу в очереди
while (1) {
alarm(O); # Отключить таймер
open(FIFO, "< /Imp/log") or die* "Can't open /Imp/log : $!\n":
alarm(1); # 1 секунда на регистрацию
$service = ;
next unless defined $service; # Прерывание или нечего регистрировать
chomp $service;
$message = ;
next unless defined $message; # Прерывание или нечего регистрировать
chomp $message;
alarm(O); # Отключить таймеры
# для обработки сообщений
if ($service eq "http") {
# Игнорировать
} elsif ($service eq "login") { # Сохранить в /var/log/login
if ( open(LOG, "" /tmp/login") ) {
print LOG scalar(localtime), " $service $message\n";
close(LOG);
} else {
warn "Couldn't log $service $message to /var/log/login : $!\n":
}
}
}

Программа получилась сложнее предыдущей по нескольким причинам. Прежде всего, мы не хотим, чтобы наш сервер ведения журнала надолго блокировал передатчики. Нетрудно представить ситуацию, при которой злонамеренный или бестолковый передатчик открывает именованный канал для записи, по не передает полного сообщения. По этой причине мы используем alarm и SIGALRM для передачи сигналов о нарушениях во время чтения.
При использовании именованных каналов могут возникнуть лишь два исклю чительных состояния: когда у приемника исчезает передатчик, и наоборот. Если процесс читает из именованного канала, а передатчик закрывает его со своего конца, то принимающий процесс получит признак конца файла (о возвращает undef). Однако если приемник отключается от канала, то при следующей попытке записи передатчик получит сигнал SIGPIPE. Если игнорировать сигналы о нарушении канала конструкцией $SIG{PIPE}=' ignore', print возвращает false, a' переменной $! присваивается значение epipe:

use POSIX qw(:errno_h);
$SIG{PIPE} = 'ignore';
# . . .
$status = print fifo "are you there?\n";
if (!$status && $! == epipe) {
' warn "My reader has forsaken me!\n";
next;
}

Возможно, у вас возник вопрос: "Если сто процессов одновременно пытаются передать данные серверу, как можно быть уверенным в том, что я получу сто разных сообщений, а не хаотическую мешанину из символов или строк разных процессов?" Хороший вопрос. Согласно стандарту POSIX, запись менее PIPE_BUF байт будет доставлена автоматически, то есть не перепутается с другими. Значение константы PIPE_BUF можно узнать из модуля POSIX:
use POSIX:
print _POSIX_PIPE_BUF, "\n":

К счастью, стандарт POSIX также требует, чтобы значение PIPE_BUF было не менее 512 байт. Следовательно, остается лишь позаботиться о том, чтобы клиенты не пытались передавать более 512 байт за раз.
Но что если вам понадобилось зарегистрировать более 512 байт? Разделите каждое большое сообщение на несколько маленьких (менее 512 байт), снабдите каждое сообщение уникальным идентификатором клиента (например, идентификатором процесса) и организуйте их сборку на сервере. Нечто похожее происходит при разделении и сборке сообщений TCP/IP. Один именованный канал не обеспечивает двухстороннего обмена данными между передатчиком и приемником, что усложняет аутентификацию и другие способы борьбы с передачей ложных сообщений (если не делает их невозможными). Вместо того чтобы упрямо втискивать эти возможности в модель, в которой они неуместны, лучше ограничить доступ к именованному каналу средствами файловой системы (на уровне прав владельца и группы).

> Смотри также --------------------------------
Страницы руководтва mkfifo(8) или mknod(8) (если они есть); рецепт 17.6.


  16.12. Совместное использование переменных в разных процессах


Проблема

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

Решение

Используйте средства SysV IPC, если ваша система их поддерживает.

Комментарий

Хотя средства SysV IPC (общая память, семафоры и т. д.) реже используются в межпроцессных коммуникациях, нежели каналы, именованные каналы и сокеты, они все же обладают рядом интересных свойств. Тем не менее для совместного использования переменной несколькими процессами обычно нельзя рассчитывать на работу с общей памятью через shmget или mmap(2). Дело в том, что Perl заново выделит память под строку тогда, когда вы этого совсем не ждете.
Проблема
решается с помощью модуля IPC::Shareable с CPAN. Умный модуль tie, общая память SysV н модуль Shareable с CPAN позволяют организовать совместный доступ к структурам данных произвольной сложности для процессов на одном компьютере. При этом процессы даже не обязаны быть родственными. В примере 16.11 продемонстрирован несложный случай применения этого модуля. Пример 16.11. sharetest

#!/usr/bin/perl
# sharetest - совместный доступ к общим переменным в разветвлениях
use IPC::Shareable;
$handle = tie $buffer, 'ipc::shareable', undef, { destroy => 1 };
$SIG{INT} = sub { die "$$ dying\n" };
for (1 .. 10) {
unless ($child = fork) { # Я - потомок
die "cannot fork: $!" unless defined $child;
squabble();
exit;
} push Okids, $child; # Если нас интересуют идентификаторы процессов
}
while (1) {
print "Buffer is $buffer\n";
sleep 1;
} die "Not reached";
sub squabble { my $i = 0;
while (1) {
next if $buffer =~ /"$$\b/o;
$handle->shlock();
$i++;
$buffer = "$$ $i";
$handle->shunlock();
}
}

Исходный процесс создает общую переменную, разветвляется на 10 потомков, а затем выводит значение буфера примерно каждую секунду в бесконечном цикле или до тех пор, пока вы не нажмете Ctrl+C.
Поскольку обработчик SIGINT был установлен до всех вызовов fork, его наследуют все потомки, которые также уничтожаются при прерывании группы процессов. Сигналы с клавиатуры передаются целой группе процессов, а не одному процессу.
Что же происходит в squabble? Потомки разбираются, кому из них удастся обновить общую переменную. Каждый порожденный процесс смотрит, изменилось ли состояние переменной с момента последнего визита. Если буфер начинается с его собственной сигнатуры (идентификатора процесса), процесс не трогает его. Если буфер был изменен кем-то другим, процесс блокирует общую переменную вызовом специального метода для манипулятора, полученного от tie, обновляет ее и снимает блокировку.
Программа заработает намного быстрее, если закомментировать строку, начинающуюся с next, где каждый процесс проверяет, кто последним прикасался к буферу. Шаблон /"$$\Ь/о выглядит подозрительно, поскольку /о указывает на однократную компиляцию шаблона, а переменная $$ меняется при разветвлении. Впрочем значение фиксируется не во время компиляции программы, а при первой компиляции шаблона в каждом процессе, во время жизни которого $$ остается постоянным.
Модуль IPC::Shareable также поддерживает совместное использование переменных неродственными процессами на одном компьютере. За подробностями обращайтесь к документации.

> Смотри также --------------------------------
Описание функций semcti, semget, semop, shmcti, shmget, shmread и shmwrite в perlfunc(1); документация по модулю IPC::Shareable с CPAN.


  16.13. Получение списка сигналов


Проблема

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

Решение

Если ваш командный интерпретатор поддерживает встроенную команду kill -/, используйте ее: % kill -1 HUP INT QUIT ILL TRAP ABRT BUS FPE KILL USR1 SEGV USR2 PIPE ALRM TERM CHLD CONT STOP TSTP TTIN TTOU URG XCPU XFSZ VTALRM PROF WINCH POLL PWR
Чтобы сделать то же самое только па Perl версии 5.004 и выше, выведите ключи хэша %SIG: % perl -e 'print join(" ", keys %SIG), "\n"' XCPU ILL QUIT STOP EMT ABRT BUS USR1 XFSZ TSTP INT IOT USR2 INFO TTOU ALRM KILL HUP URG PIPE CONT SEGV VTALRM PROF TRAP 10 TERM WINCH CHLD FPE TTIN SYS
До выхода версии 5.004 приходилось использовать модуль Config:
% perl -MConfig -e 'print $Config{sig_name}' 
ZERO HUP INT QUIT ILL TRAP ABRT EMT FPE KILL BUS SEGV SYS PIPE ALRM TERM URG STOP TSTP CONT CHLD TTIN TTOU 10 XCPU XFSZ VTALRM PROF WINCH INFO USR1 USR2 IOT

Комментарий

Если вы работаете в Perl версии младше 5.004, для получения списка сигналов вам также придется использовать Osigname и %signo модуля Config, поскольку конструкция keys %SIG в ранних версиях еще не реализована.
Следующий фрагмент извлекает имена и номера доступных сигналов из стандартного модуля Config.pm. Индексирование @signame по номеру дает имя сигнала, ? индексирование %signo по имени - номер сигнала.

use Config;
defined $Conrig{sig_name} or die "No sigs?";
$1=0; # config добавляет ложный сигнал О
# с именем "ZERO".
foreach $name (split(' ', $Config{sig_name})) {
$signo{$name} = $i;
$signame[$i] = $name;
$i++;
}

> Смотри также -------------------------------
Документация по стандартному модулю Config; раздел "Signals" perlipc(1).


  16.14. Посылка сигнала


Проблема

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

Решение

Функция kill отправляет сигнал с заданным именем или номером процессам, идентификаторы которых перечисляются в качестве остальных аргументов:

kill 9 => $pid; , # Послать $pid сигнал 9
kill -1 => $рgrр; # Послать всему заданию сигнал 1
kill USR1 => $$; # Послать себе SIGUSR1
kill HUP => @pids; # Послать SIGHUP процессам из @pids

Комментарий

Функция Perl kill обеспечивает интерфейс к системной функции с тем же именем. Первый аргумент определяет посылаемый сигнал и задается по номеру или по имени; остальные аргументы определяют идентификаторы процессов, которым отправляется сигнал. Функция возвращает количество процессов, успешно получивших сигнал. Сигналы можно отправлять только процессам, для которых реальный или сохраненный идентификатор пользователя совпадает с вашим реальным или текущим идентификатором - если только вы не являетесь привилегированным пользователем. Если номер сигнала отрицателен, Perl интерпретирует остальные аргументы как идентификаторы групп процессов и отправляет сигнал процессам, входящим в эти группы, с помощью системной функции killpg(2).
Группа процессов фактически представляет собой задание. Именно так операционная система объединяет родственные процессы. Например, когда вы с помощью командного интерпретатора сцепляете две команды, при этом запускаются два процесса, но лишь одно задание. Когда текущее задание прерывается по Ctrl+C или приостанавливается по Ctrl+Z, соответствующие сигналы отправляются всему заданию, которое может состоять из нескольких процессов. Функция kill также позволяет проверить, жив ли процесс. Посылка специального псевдосигнала с номером 0 сообщает, можно ли послать сигнал процессу - хотя сам сигнал при этом не передается. Если функция возвращает true, процесс жив. Если возвращается false, процесс либо сменил свой действующий идентификатор (в этом случае переменной $! присваивается EPERM), либо прекратил существование ($! присваивается ESRCH). Для процессов-зомби (см. рецепт 16.19) также возвращается ESRCH.

use POSIX qw(:errno_h);
if (kill 0 => $minion) {
print "$minion is alive!\n";
} elsif ($! == eperm) { # Изменился uid
print "$minion has escaped my control!\n";
} elsif ($! == esrch) {
print "Sminion is deceased.\n"; # Или зомби
} else {
warn "Odd; I couldn't check on the status of $minion: $!\n";
} 


  16.15. Установка обработчика сигнала


Проблема

Вы хотите управлять реакцией программы на сигналы. Это может понадобиться для перехвата Ctrl+C, избежания накопления завершившихся подпроцессов или предотвращения гибели вашего процесса при попытке передать данные исчезнувшему потомку.

Решение

Воспользуйтесь хэшем %SIG для установки обработчика по имени или ссылке на код:

$SIG{QUIT} = \&got_sig_quit; # Вызвать
&got_sig_quit # для каждого
SIGQUIT $S1G{PIPE} = 'got_sig_pipe'; # Вызвать
main::got_sig_pipe
# для каждого
SIGPIPE $SIG{INT} = sub { $ouch++ }; # Увеличить $ouch для каждого SIGINT
Хэш %SIG также позволяет игнорировать сигнал:
$SIG{INT} = 'IGNORE';
# Игнорировать сигнал INT Также есть возможность восстановить стандартный обработчик сигнала:
$SIG{STOP} = 'DEFAULT'; # Восстановить стандартный обработчик
# сигнала STOP

Комментарий

Хэш %SIG используется в Perl для управления тем, что происходит при получении сигналов. Каждый ключ %SIG соответствует определенному сигналу, а значение - действию, которое должно предприниматься при его получении. В Perl предусмотрены два особых ассоциированных значения: "IGNORE" означает, что при получении сигнала не следует выполнять никаких действий, a "DEFAULT" выполняет стандартные действия UNIX для данного сигнала.
Хотя программисты на С привыкли к термину SIGINT, в Perl используется только INT. Предполагается, что имена сигналов используются только в функциях, связанных с обработкой сигналов, поэтому префикс SIG оказывается лишним. Следовательно, чтобы изменить действия вашего процесса при получении сигнала SIGCHLD, следует присвоить значение $SIG{CHLD}. Чтобы ваш код выполнялся при получении конкретного сигнала, в хэш заносится либо ссылка на код, либо имя функции (следовательно, при сохранении строки вам не удастся использовать обработчик с именем IGNORE или DEFAULT впрочем, для обработчика сигнала эти имена выглядят довольно странно). Если имя функции не содержит информации о пакете, Perl считает, что функция принадлежит пакету main: :, а не тому пакету, в котором обработчик был установлен. Ссылка на код относится к конкретному пакету, и этот вариант считается предпочтительным. Perl передает коду обработчика один аргумент: имя сигнала, по которому он вызывается (например, "INT" или "USR1"). При выходе из обработчика продолжается выполнение действий, выполнявшихся в момент поступления сигнала.
Perl определяет два специальных сигнала, __DIE__ и __WARN__. Обработчики этих сигналов вызываются каждый раз, когда программа на Perl выводит предупреждение (warn) или умирает (die). Это позволяет нам перехватывать предупреждения и по своему усмотрению обрабатывать их или передавать дальше. На время своего выполнения обработчики die и warn отключаются, поэтому вы можете спокойно вызвать die в обработчике __DIE__ или warn в обработчике __WARN__, не опасаясь рекурсии.
> Смотри также --------------------------------
Раздел "Signals" perlipc(1); страницы руководства sigaction(1), signal(3) и kill(2) вашей системы (если есть).


  16.16. Временное переопределение обработчика сигнала


Проблема

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

Решение

Используйте local для временного переопределения обработчика:
# Обработчик сигнала

sub ding {
$SIG{INT} = \&ding;
warn "\aEnter your name!\n";
}
# Запросить имя с переопределением SIGINT
sub get_name {
local $SIG{INT} = \&ding;
my $name;
print "Kindly Stranger, please enter your name:
chomp( $name = <> );
return $name;
}

Комментарий

Для временного сохранения одного элемента %SIG необходимо использовать local, а не ту. Изменения продолжают действовать во время выполнения блока, включая все, что может быть вызвано из него. В приведенном примере это подпрограмма get_name. Если сигнал будет доставлен во время работы другой функции, вызванной вашей функцией, сработает ваш обработчик сигнала - если только вызванная подпрограмма не установила собственный обработчик. Предыдущее значение элемента хэша автоматически восстанавливается при выходе из блока. Это один из немногочисленных случаев, когда динамическая область действия оказывается скорее удобной, нежели запутанной.

Смотри также: Рецепты 10.13; 16.15; 16.18.


  16.17. Написание обработчика сигнала


Проблема

Требуется написать подпрограмму, которая будет вызываться программой при каждом получении сигнала.

Решение

Обработчик сигнала представляет собой обычную подпрограмму. С некоторой степенью риска в обработчике можно делать все, что допустимо в любой другой подпрограмме Perl, но чем больше вы делаете, тем больше рискуете.
В некоторых системах обработчик должен переустанавливаться после каждого сигнала:

$SIG{INT} = \&got_int;
sub got_int {
$SIG{INT} = \&got_int: # Но не для sigchld!
# ...
}
Некоторые системы перезапускают блокирующие операции (например, чтение данных). В таких случаях необходимо вызвать в обработчике die и перехватить вызов eval:

my $interrupted = 0;
# или 'IGNORE'
sub got_int {
$interrupted = 1;
$SIG{INT} = -default' die;
}
eval {
$SIG{INT} = \&got_int;
# ... Длинный код, который нежелательно перезапускать
}
If ($interrupted) {
# Разобраться с сигналом
}

Комментарий

Установка собственного обработчика сигнала напоминает игру с огнем: это очень интересно, но без исключительной осторожности вы рано или поздно обожжетесь. Создание кода Perl, предназначенного для обработки сигналов, чревато двумя опасностями. Во-первых, многие библиотечные функции нереентерабельны. Если сигнал прерывает выполнение какой-то 4)ункции (например, malloc(3) или printf(3)). а ваш обработчик сигнала снова вызовет ее, результат окажется непредсказуемым - обычно работа программы прерывается с выводом в файл содержимого памяти (core dump). Во-вторых, на нижних уровнях переентерабелен сам Perl (версия 5.005 будет поддерживать облегченные процессы, называемые нитями (threads), но на момент издания этой книги она еще не вышла). Если сигнал прерывает Perl в момент изменения его собственных внутренних структур данных, результат тоже непредсказуем - как правило, выдаются случайные дампы.
Перед вами открываются два пути: параноидальный и практический. Параноик постарается ничего не делать внутри обработчика сигнала; примером служит код с eval и die в решении - мы присваиваем значение переменной и тут же выходим из обработчика. Но даже это покажется слишком рискованным настоящему параноику, который избегает die в обработчиках - вдруг система на что-нибудь обидится? Практический подход - вы говорите: "Кто не рискует, тот не выигрывает", - и делаете в обработчике все, что заблагорассудится.
Сигналы были реализованы во многих операционных системах, причем не всегда одинаково. Отличия в реализации сигналов чаще всего проявляются в двух ситуациях: когда сигнал происходит во время активности обработчика (надежность) и когда сигнал прерывает блокирующий вызов системной функции типа read или accept (перезапуск).
Первоначальная реализация сигналов была ненадежной. Это означало, что во время работы обработчика при других поступлениях сигнала происходило некоторое стандартное действие (обычно аварийное завершение программы). Новые системы решают эту проблему (конечно, каждая - в своем, слегка особом стиле), позволяя подавлять другие экземпляры сигналов с данным номером до завершения обработчика. Если Perl обнаружит, что ваша система может использовать надежные сигналы, он генерирует соответствующие вызовы системных функций, чтобы программы вели себя более логично и безопасно. Система сигналов POSIX позволяет запретить доставку сигналов и в другие моменты времени (см. рецепт 16.20).
Чтобы получить по-настоящему переносимый код, программист-параноик заранее предполагает самое худшее (ненадежные сигналы) и вручную переустанавливает обработчик сигналов, обычно в самом начале функции:

$SIG{INT} = \&catcher;
sub catcher {
# ...
$SIG{INT} = \&catcher;
}

Особый случай перехвата SIGCHLD описан в рецепте 16.19. System V ведет себя очень странно и может сбить с толку,
Чтобы узнать, располагаете ли вы надежными сигналами, воспользуйтесь модулем Config:

use Config;
print "Htirrah!\n"
if $Config{d_sigaction};


Наличие надежных сигналов еще не означает, что вы автоматически получаете надежную программу. Впрочем, без них программа заведомо окажется ненадежной.
Первые реализации сигналов прерывали медленные вызовы системных функций, которые требовали взаимодействия со стороны других процессов или драйверов устройств. Если сигнал поступает во время выполнения этих функций, они (и их аналоги в Perl) возвращают признак ошибки и присваивают коду ошибки EINTR, "Interrupted system call". Проверка этого условия настолько усложняет программу, что во многих случаях это вообще не делается, поэтому при прерывании сигналом медленных системных функций программа начинает вести себя неверно или аварийно завершается. Большинство современных версий UNIX позволяет изменить ход событий. Perl всегда делает системные функции перезапускаемыми, если эта возможность поддерживается системой. В системах POSIX можно управлять перезапуском с помощью модуля POSIX (см. рецепт 16.20).
Чтобы узнать, будет ли прерванная системная функция автоматически перезапущена, загляните в заголовочный файл signal.h нашей системы: % egrep oS[AV:L(RESTART| INTERRUPT) o /usr/include/./bnal. h 16.18. Перехват Ctrl+С 593
Два сигнала не перехватываются и не игнорируются: SIGKILL и SIGSTOP. Полная информация о сигналах вашей системы и об их значении приведена в странице руководства signal(3).
> Смотри также -------------------------------
Раздел "Signals" perlipc(1); страницы руководства sigaction(2), signal(1) и kill(2) вашей системы (если есть).


  16.18. Перехват Ctrl+C


Проблема

Требуется перехватить нажатие Ctrl+C, приводящее к остановке работы программы. Вы хотите либо игнорировать его, либо выполнить свою собственную функцию при получении сигнала.

Решение

Установите обработчик для SIGINT. Присвойте ему "IGNORE", чтобы нажатие Ctrl+C игнорировалось:

$SIG{INT} = -ignore';

Или установите собственную подпрограмму, которая должна реагировать на Ctrl+C:

$SIG{INT} = \&tsktsk;
sub tsktsk {
$SIG{INT} = \&tsktsk; # См. "Написание обработчика сигнала"
warn "\aThe long habit of living indisposeth us for dying.\n";
}

Комментарий

Ctrl+C не влияет на вашу программу напрямую. Драйвер терминала, обрабатывающий нажатия клавиш, опознает комбинацию Ctrl+C (или другую комбинацию, заданную вами в качестве символа прерывания при настройке параметров терминала) и посылает SIGINT каждому процессу активной группы (активного задания) данного терминала. Активное задание обычно состоит из всех программ, запущенных отдельной строкой в командном интерпретаторе, а также всех программ, запущенных этими программами. За подробностями обращайтесь к разделу введения "Сигналы". Символ прерывания - не единственный служебный символ, интерпретируемый драйвером терминала. Текущие параметры терминала можно узнать с помощью команды stty -a:

% stty -а speed 9600 baud; 38 rows; 80 columns;
Iflags: icanon isig iexten echo echoe -echok echoke -echoni echocti
-echoprt -altwerase -noflsh -tostop -flusho pendin -nokerninfo
-extproc iflags: -istrip icrni -inlcr -igncr ixon -ixoff ixany imaxbel -ignbrk
brkint -inpck -ignpar -parmrk oflags: opost onlcr oxtabs cflags: cread cs8 -parenb
-parodd hupcl -clocal -cstopb -crtscts -dsrflow
-dtrflow -mdmbuf cchars: discard = ~0; dsusp = ~y; eof = ~d; eol =
eol2 =
stop = "s; susp = "z; time = 0; werase = ~w;

В последней секции, cchars:, перечисляются служебные символы. В рецепте 15.8 показано, как изменить в сценарии без вызова программы stty.

> Смотри также -------------------------------
Страница руководства stty( 1) вашей системы (если есть); рецепты 15.8; 16.17.


  16.19. Уничтожение процессов-зомби


Проблема

Программа создает порожденные процессы с помощью fork. Зомби накапливаются, забивают таблицу процессов и раздражают системного администратора.

Решение

Если вам не нужно регистрировать завершившихся потомков, используйте:

$SIG{CHLD} = 'ignore';

Чтобы следить за умирающими потомками, установите обработчик SIGCHLD с вызовом waitpid:

use POSIX ":sys_wait_h";
$SIG{CHLD} = \&reaper:
sub REAPER {
my $stiff;
while ($stiff = waitpid(-1, &wnohang) > 0) {
# Обработать $stiff, если нужно
}
$SIG{CHLD} = \&reaper: # Установить *после* вызова waitpid
}

Комментарий

Когда процесс завершается, система оставляет его в таблице процессов, чтобы родитель мог проверить его статус, то есть узнать, как завершился потомок, нормально или аварийно. Определение статуса потомка (после которого он получает возможность навсегда покинуть систему) называется "чисткой" (reaping). В этом рецепте приведены различные рекомендации по чистке зомби. В процессе чистки используется вызов wait или waitpid. Некоторые функции Perl (конвейерные вызовы open, system и '...') автоматически вычищают созданных ими потомков, но при запуске другого процесса с помощью fork вам придется дожидаться его завершения. Чтобы избежать накопления зомби, достаточно сообщить системе, что они вас не интересуют. Для этого $SIG{.CHLD} присваивается значение "IGNORE". Если вы хотите узнать, когда скончался тот или иней потомок, необходимо использовать waitpid.
Функция waitpid вычищает один процесс. Ее первый аргумент определяет идентификатор процесса (-1 означает любой процесс), а второй - набор флагов. Флаг WNOHANG заставляет waitpid немедленно вернуть 0, если нет ни одного мертвого потомка. Флаг 0 поддерживается всеми системами и означает блокирующий вызов. Вызов waitpid в обработчике SIGCHLD (см.
Решение
) вычищает потомков сразу после их смерти.
Функция wait тоже вычищает потомков, но она вызывается только в блокирующем режиме. Если случайно вызвать ее при наличии работающих потомков, ни один из которых не умер, программа приостанавливается до появления зомби.
Поскольку ядро следит за недоставленными сигналами посредством битового вектора (по одному биту на сигнал), если до перехода вашего процесса в активное состояние умрут два потомка, процесс все равно получит один сигнал SIGCHLD. Чистка в обработчике SIGCHLD всегда выполняется в цикле, поэтому wait использовать нельзя.
И wait и waitpid возвращают идентификатор только что вычищенного процесса и Присваивают $? его статус ожидания. Код статуса в действительности состоит из двух 8-разрядных значений, объединенных в одном 16-разрядном числе. Старший байт определяет код возврата процесса. Младшие 7 бит определяют номер сигнала, убившего процесс, а 8-й бит показывает, произошла ли критическая ошибка. Составляющие можно выделить следующим образом:

$exit_value = $? " 8;
$signal_num \= $? & 127;
$dumped_core = $? & 128;

Стандартный модуль POSIX содержит специальные макросы для выделения составляющих статуса: WIFEXITED, WEXITSTATUS, WIFSIGNALLED и WTERMSIG. Как ни странно, POSIX не содержит макроса для определения того, произошла ли критическая ошибка.
При использовании SIGCHLD необходимо помнить о двух обстоятельствах. Во-первых, сигнал SIGCHLD посылается системой не только при завершении потомка; сигнал также посылается при остановке. Процесс может остановиться по многим причинам - он может ожидать перехода в активное состояние для выполнения терминального ввода/вывода, получить сигнал SIGSTOP (после чего будет ожидать SIGCONT для продолжения работы) или быть приостановленным с терминала. Проверьте статус функцией WIFEXITED* модуля POSIX, чтобы убедиться, что процесс действительно умер, а не был остановлен:

use POSIX qw(:signal_h :errno_h);
$SIG{CHLD} = \&reaper;
sub REAPER { my $pid;
$pid = waitpid(-1, &wnohang);
if ($pid == -1) {
# Ожидающих потомков нет. Игнорировать.
} elsif (WIFEXITED($?)) {
print "Process $pid exited.\n";
} else {
print "False alarm on $pid.\n";
} $SIG{CHLD} = \&reaper; # На случай ненадежных сигналов
}
Вторая ловушка, связанная с SIGCHLD, относится к Perl, а не к операционной системе. Поскольку system, open и '. . . ' запускают подпроцессы через fork, а операционная система отправляет процессу SIGCHLD при выходе из любого подпро-цесса, вызов обработчика может быть и непредвиденным. Встроенные операции сами ожидают завершения потомков, поэтому иногда SIGCHLD прибывает до того, как вызов close для манипулятора заблокирует его для чистки. Если первым до него доберется обработчик сигнала, то к моменту нормального закрытия зомби уже не будет. В результате close вернет false и присвоит $! значение "No child processes". Если вызов close первым доберется до умершего потомка, waitpid возвращает 0.
В большинстве систем поддерживается неблокирующий режим waitpid. Об этом можно узнать из стандартного модуля Perl Config.pm:

use Config;
$has_nonblocking = $config{d_waitpid} eq "define" || $config{d_wait4} eq "define";
System V определяет сигнал SIGCLD, который имеет тот же номер, что и SIGCHLD, но слегка отличается по семантике. Чтобы избежать путаницы, используйте SIGCHLD.

> Смотри также -------------------------------
Раздел "Signals" perlipc(1) описание функций wait и waitpid в perlfunc(1); документация по стандартному модулю POSIX; страницы руководства sigaction(T), signal(3) и kill(2) вашей системы (если есть); рецепт 16.17.


  16.20. Блокировка сигналов


Проблема

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

Решение

Воспользуйтесь интерфейсом модуля POSIX к системной функции sigprocmask (только в POSIX-совместимых системах).
Блокировка сигнала на время выполнения операции выполняется так:

use POSIX qw(:signal_h);
$sigset = posix::sig8et->new(SIGINT): # Определить блокируемые сигналы
$old_sigset = posix::sigset->new; # Для хранения старой маски
unless (defined sigprocmask(SIG_BLOCK, $slgset, $old_sigset))
{ die "Could not block SIGINT\n";
}

Снятие блокировки выполняется так:

unless (defined sigprocmask(SIG_UNBLOCK, $old_sigset))
{ die "Could not unblock SIGINT\n":
}


Комментарий

В стандарт POSIX входят функции sigaction и sigprocmask, которые позволяют лучше управлять доставкой сигналов. Функция sigprocmask управляет отложенной доставкой сигналов, a sigaction устанавливает обработчики. При изменении %SIG Perl по возможности использует sigaction. Чтобы использовать sigprocmask, сначала постройте набор сигналов методом POSIX: :SigSet->new. В качестве аргумента передается список номеров сигналов. Модуль POSIX экспортирует функции, возвращающие номера сигналов; имена функций совпадают с именами сигналов:

use POSIX qw(:signal_h);
$sigset = posix::sigset->new( SIGINT, SIGKILL );

Передайте объект POSIX::SigSet функции sigprocmask с нужным флагом. Флаг SIG_BLOCK откладывает доставку сигнала. Флаг SIG_UNBLOCK восстанавливает нормальную доставку сигналов, a SIG_GETMASK блокирует только сигналы, содержащиеся в POSIX::SigSet. Самые отчаянные перестраховщики блокируют сигналы при вызове fork, чтобы предотвратить вызов обработчика сигнала в порожденном процессе перед тем, как Perl обновит его переменную $$ (идентификатор процесса). Если обработчик сигнала вызывается немедленно и сообщает значение $$, то вместо своего собственного $$ он может использовать родительское значение. Такая проблема возникает очень редко.

> Смотри также -------------------------------
Страница руководства sigprocmask(2) вашей системы (если есть); документация по стандартному модулю POSIX.


  16.21. Тайм-аут


Проблема

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

Решение

Чтобы прервать затянувшуюся операцию, используйте обработчик SIGALRM и вызовите в нем die. Установите таймер функцией alarm и включите код в eval:

$SIG{ALRM} = sub { die "timeout" };
eval {
alarm(3600);
# Продолжительные операции alarm(O);
}
if ($@) {
if ($@ =~ /timeout/) {
# Тайм-аут; сделайте то, что считаете нужным
} else {
die; # Передать дальше неожиданное исключение
}
}

Комментарий

Функция alarm получает один аргумент: целое число секунд, после истечения которых ваш процесс получит SIGALRM. В сильно загруженных системах с разделением времени сигнал может быть доставлен позже указанного времени. По умолчанию SIGALRM завершает программу, поэтому вы должны установить собственный обработчик сигнала.
Функции alarm нельзя (с пользой) передать дробное число секунд; если вы попытаетесь это сделать, число секунд будет округлено до целого. Создание более точных таймеров рассматривается в рецепте 3.9.

Смотри также: Раздел "Signals" perlipc(1); описание функции alarm в perlfunc(1); рецепт 3.9.


  16.22. Программа: sigrand

Следующая программа выдает случайные подписи с применением именованных каналов. Предполагается, что файл подписей хранится в формате программы fortune - то есть каждый многострочный блок завершается последовательностью "%%\n". Приведем-пример:

Make is like Pascal: everybody likes it, so they go in and change it. --Dennis Ritchie %%
I eschew embedded capital letters in names; to my prose-oriented eyes, they are too awkward to read comfortably. They jangle like bad typography. --Rob Pike %%
God made the integers; all else is the work of Man. --Kronecker %%
I'd rather have :rofix than const. --Dennis Ritchie %%
If you want to program in C, program in C. It's a nice language. I use it occasionally... :-) --Larry Wall %%
Twisted cleverness is my only skill as a programmer. --Elizabeth Zwicky %%
Basically, avoid comments. If your code needs a comment to be understood, it would be better to rewrite it so it's easier to understand. --Rob Pike %%
Comments on data are usually much more helpful than on algorithms, --Rob Pike %%
Programs that write programs are the happiest programs in the wor1'! --Andrew Hume %%
Мы проверяем, не была ли программа запущена ранее - для этого используется файл с идентификатором процесса. Если посылка сигнала с номером 0 показывает, что идентификатор процесса все еще существует (или, что случается редко - что им воспользовался кто-то другой), программа просто завершается. Также мы проверяем текущую отправку Usenet и решаем, следует ли искать специализированные файлы подписей для конкретных конференций. В этом случае можно завести разные подписи для каждой конференции, в которую вы пишете. Для большего разнообразия глобальный файл подписей иногда применяется даже при наличии специализированного файла.
Программа sigrand может использоваться даже в системах без именованных каналов - достаточно удалить код создания именованного капала и увеличить паузу перед обновлениями файла. После этого .signature может быть обычным файлом. Другая проблема переносимости возникает при переходе программы в фоновый режим (при котором она почти становится демоном). Если функция fork недоступна, просто закомментируйте ее. Полный текст программы приведен в примере 16.12. Пример 16.12. sigrand

#!/usr/bin/perl -w
# sigrand - выдача случайных подписей для файла .signature
use strict;
# Конфигурационные переменные
use vars qw( $NG_IS_DIR $MKNOD $FULLNAME
$FIFO $ART $NEWS $SIGS $SEMA $GLOBRAND $NAME );
# Глобальные имена
use vars qw( $Home $Fortune_Path @Pwd );
##############
# Начало секции конфигурации
# В действительности следует читать из '/.sigrandrc
gethome();
# rес/humor/funny вместо rec.humor.funny $NG_IS_DIR = 1;
$MKNOD = "/bin/mknod";
$FULLNAME = "$home/.fullname";
$FIFO = "$home/.signature";
$ART = "$home/.article";
$NEWS = "$home/news";
$SIGS = "smews/signatures";
$SEMA = "$home/.sigrandpid";
$GLOBRAND = 1/4; # Вероятность использования глобальных
# подписей при наличии специализированного файла
# $NAME следует: (1) оставить неопределенным, чтобы программа
# попыталась угадать адрес подписи (возможно, заглянув
# в '/.fullname, (2) присвоить точный адрес, или (3) присвоить
# пустую строку, чтобы отказаться от использования имени.
$NAME = ''; # Означает, что имя не используется
# $NAME = "me\@home.org\n";
# Конец секции конфигурации -- HOME и FORTUNE # настраиваются автоматически
###################
setup(); # Выполнить инициализацию
justme(); # Убедиться, что программа еще не работает
fork && exit; # Перейти в фоновый режим
open (SEMA, "> $SEMA") or die "can't write $SEMA: $!";
print SEMA "$$\n";
close(SEMA) or die "can't close $SEMA: $!";
# В бесконечном цикле записывать подпись в FIFO.
# Если именованные каналы у вас не поддерживаются, измените
# паузу в конце цикла (например, 10, чтобы обновление
# происходило только каждые 10 секунд).
for (:;) {
open (FIFO, "> $FIFO") or die "can't write $FIFO: $!";
my $sig = pick_quote();
for ($sig) {
s/"(( :'?["\n].\n){4}). *$/$1/s; # Ограничиться 4 строками
s/"(.{1,80}).*? *$/$1/gm; # Обрезать длинные строки
}
# Вывести подпись с именем, если оно присутствует,
# и дополнить до 4 строк
if ($NAME) {
print FIFO $NAME, "\n" x (3 - ($sig =~ tr/\n//)), $sig;
} else {
print FIFO $sig;
} close FIFO: o
# Без небольшой паузы приемник не закончит чтение к моменту,
# когда передатчик снова попытается открыть FIFO;
# поскольку приемник существует, попытка окажется успешной.
# В итоге появятся сразу несколько подписей.
# Небольшая пауза между открытиями дает приемникам возможность и завершить чтение и закрыть канал.
select(undef, undef, undef, 0.2); # Выждать 1/5 секунды
} die "XXX: NOT REACHED"; # На эту строку вы никогда не попадете #########################################
# Игнорировать SIGPIPE на случай, если кто-то открыл FIFO и
# снова закрыл, не читая данных; взять имя пользователя из файла
# .fullname. Попытаться определить полное имя хоста. Следить за
# амперсандами в паролях. Убедиться, что у нас есть подписи или
# цитаты. При необходимости построить FIFO.
sub setup {
$SIG{PIPE} = -ignore';
unless (defined $NAME) { # Если $NAME не определено
if (-e $FULLNAME) { # при конфигурации
$NAME = 'cat $fullname';
die "$FULLNAME should contain only 1 line, aborting" if $NAME =~ tr/\n// > 1;
} else { my($user, $host);
chop($host = 'hostname');
($host) = gethostbyname($host)
unless $host =~ /\./\ $user = $env{user} || $env{logname} || $pwd[0]
or die "intruder alert";
($NAME = $pwd[6]) =~ s/,.*//;
$NAME =~ s/&/\u\l$user/g; # До сих пор встречается
$NAME = "\t$name\t$user\@$host\n";
}
}
check_fortunes() if !-e $SIGN
unless (-p $FI,FO) { # -p проверяет, является ли операнд
# именованным каналом if (!-e _) {
system("$MKNOD $FIFO p") && die "can't mknod $FIFO";
warn "created $FIFO as a named pipe\n";
} else {
die "$0: won't overwrite file .signature\n";
} eise {
warn "$0: using existing named pipe $FIFO\n";
}
# Получить хорошее начальное значение для раскрутки генератора.
# Не нужно в версиях 5.004 и выше.
srand(time() " ($$ + ($$ " 15)));
}
# Выбрать случайную подпись
sub pick_quote {
my $sigfile = signame();
if (!-e $sigfile) { return fortune();
}
open (SIGS, "< $sigfile" ) or die "can't open $sigfile'
local $/ = "%%\n";
local $_;
my $quip;
rand($.) < 1 && ($quip = $_) while ;
close SIGS:
chomp $quip;
return $quip || "ENOSIG: This signature file is empty.\n";
}
# проверить, содержит ли "/.article строку Newsgroups. Если содержи],
# найти первую конференцию и узнать, существует ли для нее
# специализированный набор цитат; в противном случае вернуть глобальный
# набор. Кроме того, время от времени возвращать глобальный набор
# для внесения большего разнообразия в подписи.
sub signame {
(rand(-I.O) > ($GLOBRAND) && open ART) || return $SIGS;
local $/ = ' ';
local $_ = ;
my($ng) = /newsgroups:\s.([",\s]*)/;
$ng =~ s'\.!/'g if $ng_is_dir; # if rn -/, or savedir=%p/%c $ng =
"$NEWS/$ng/SIGNATURES":
return -f $ng ? $ng : $SIGS;
}
# вызывать программу fortune с параметром -s до тех пор,
# пока мы не получим достаточно короткую цитату или не Я превысим лимит попыток,
sub fortune {
local $_;
my $tries = 0;
do {
$_ = '$fortune_path -s';
} until tr/\n// < 5 || $tries++ > 20;
s/7 /mg:
$_ 11 " SIGRAND: deliver random signals to all processes.\n";
}
# Проверить наличие программы fortune. Определить полный путь
# и занести его в глобальную переменную. sub check_fortunes {
return if $Fortune_Path; # Уже найден
for my $dir (split(/:/, $ENV{PATH}), '/usr/games') { return if -x ($Fortune_Path =
"$dir/fortune"):
}
die "Need either $SIGS or a fortune program, bailing out":
}
# Определение каталога
sub gethome {
@Pwd = getpwuid($<);
$Home = $env{home} || $env{logdir} || $pwd[7]
or die "no home directory for user $<";
}
# "Останется только один" -- из фильма "Горец" sub justme {
if (open SEMA) { my $pid;
chop($pid = );
kill(0, $pid) and die "$0 already
running (pid $pid), bailing out' close SEMA;
}
} ticle строку Newsgroups. Если содержи],
# найти первую конференцию и узнать, существует ли для нее
# специализированный набор цитат; в противном случае вернуть глобальный
# набор. Кроме того, время от времени возвращать глобальный набор
# для внесения большего разнообразия в подписи.
sub signame {
(rand(-I.O) > ($GLOBRAND) && open ART) || return $SIGS;
local $/ = ' ';
local $_ = ;
my($ng) = /newsgroups:\s.([",\s]*)/;
$ng =~ s'\.!/'g if $ng_is_dir; # if rn -/, or savedir=%p/%c $ng =
"$NEWS/$ng/SIGNATURES":
return -f $ng ? $ng : $SIGS;
}
# вызывать программу fortune с параметром -s до тех пор,
# пока мы не получим достаточно короткую цитату или не Я превысим лимит попыток,
sub fortune {
local $_;
my $tries = 0;
do {
$_ = '$fortune_path -s';
} until tr/\n// < 5 || $tries++ > 20;
s/7 /mg:
$_ 11 " SIGRAND: deliver random signals to all processes.\n";
}
# Проверить наличие программы fortune. Определить полный путь
# и занести его в глобальную переменную. sub check_fortunes {
return if $Fortune_Path; # Уже найден
for my $dir (split(/:/, $ENV{PATH}), '/usr/games') { return if -x ($Fortune_Path =
"$dir/fortune"):
}
die "Need either $SIGS or a fortune program, bailing out":
}
# Определение каталога
sub gethome {
@Pwd = getpwuid($<);
$Home = $env{home} || $env{logdir} || $pwd[7]
or die "no home directory for user $<";
}
# "Останется только один" -- из фильма "Горец" sub justme {
if (open SEMA) { my $pid;
chop($pid = );
kill(0, $pid) and die "$0 already
running (pid $pid), bailing out' close SEMA;
}
}

perl/posts/pb16 -- Last updated 2010-06-01 Tuesday 10:57:58 Edit

© copyright 2010
Design by: lev