Приглашаем посетить
Древнерусская литература (drevne-rus-lit.niv.ru)

17.15. Создание сервера-демона

Назад
Глава 17 Сокеты
Вперед

17.15. Создание сервера-демона

Проблема

Вы хотите, чтобы ваша программа работала в качестве демона.

Решение

Если вы - параноик с правами привилегированного пользователя, для начала вызовите chroot для безопасного каталога:
chroot("/var/daemon")
or die "Couldn't chroot to /var/daemon: $!";

Вызовите fork и завершите родительский процесс.
$pid = fork;
exit if $pid;
die "Couldn't fork: $!" unless defined($pid);

Разорвите связь с управляющим терминалом, с которого был запущен процесс, - при этом процесс перестает входить в группу процессов, к которой он принадлежал.
use POSIX;
POSIX::setsxd()
or die "Can't start a new session: $!";

Перехватывайте фатальные сигналы и устанавливайте флаг, означающий, что мы хотим корректно завершиться:
$time_to_die = 0;
sub signal_handler { $time_to_die = 1;
}
$SIG{INT} = $SIG{TERM} - $SIG{HUP} = \&signal_handler;
# Перехватить или игнорировать $SIG{PIPE}

Настоящий код сервера включается в цикл следующего вида:
until ($time_to_die) {
# ...
}

Комментарий

До появления стандарта POSIX у каждой операционной системы были свои средства, с помощью которых процесс говорил системе: "Я работаю в одиночку; пожалуйста, не мешайте мне". Появление POSIX внесло в происходящее относительный порядок. Впрочем, это не мешает вам использовать любые специфические функции вашей операционной системы.
К числу этих функций принадлежит chroot, которая изменяет корневой каталог процесса (/). Например, после вызова chroot "/var/daemon" при попытке прочитать файл /etc/passwd процесс в действительности прочитает файл /var/ daemon/etc/passwd. Конечно, при вызове функции chroot необходимо скопировать все файлы, с которыми работает процесс, в новый каталог. Например, процессу может потребоваться файл /var/daemon/bin/csh. По соображениям безопасности вызов chroot разрешен только привилегированным пользователям. Он выполняется на серверах FTP при анонимной регистрации. На самом деле становиться демоном необязательно.
Операционная система предполагает, что родитель ожидает смерти потомка. Для нашего процесса-демона это не нужно, поэтому мы разрываем наследственные связи. Для этого программа вызывает fork и exit, чтобы потомок не был свя-tan с процессом, запустившем родителя. Затем потомок закрывает все файловые манипуляторы, полученные от родителя (STDIN, STDERR и STDOUT), и вызывает POSIX: : setsid, чтобы обеспечить полное отсоединение от родительского терминала.
Все почти готово. Сигналы типа SIGINT не должны немедленно убивать наш процесс (поведение по умолчанию), поэтому мы перехватываем их с помощью %SIG и устанавливаем флаг завершения. Далее главная программа работает гю принципу: "Пока не убили, что-то делаем". Сигнал SIGPIPE - особый случай. Получить его нетрудно (достаточно записать что-нибудь в манипулятор, закрытый с другого конца), а по умолчанию он ведет себя довольно сурово (завершает процесс). Вероятно, его желательно либо проигнорировать ($SIG{PIPE} = ' IGNORE'), либо определить собственный обработчик сигнала и организовать его обработку.

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

17.16. Перезапуск сервера по требованию

Проблема

При получении сигнала HUP сервер должен перезапускаться, по аналогии i inetd или httpd.

Решение

Перехватите сигнал SIGHUP и перезапустите свою программу:
$SELF.= "/usr/local/libexec/myd"; # Моя программа
@ARGS = qw(-l /var/log/myd -d); # Аргументы
$SIG{HUP} = \&phoenix;
sub phoenix {
# Закрыть все соединения, убить потомков и
# приготовиться к корректному возрождению.
exec($SELF, OARGS) or die "Couldn't restart: $!\n";
}

Комментарий

Внешне все выглядит просто ("Получил сигнал HUP - перезапустись"), но на самом деле проблем хватает. Вы должны знать имя своей программы, а определить его непросто. Конечно, можно воспользоваться переменной $0 модуля FindBin. Для нормальных программ этого достаточно, но важнейшие системные утилиты должны проявлять большую осторожность, поскольку правильность $0 не гарантирована. Имя программы и аргументы можно жестко закодировать в программе, как это сделано в нашем примере. Однако такое решение не всегда удобно, поэтому имя и аргументы можно читать из внешнего 4)айла (защищая подлинность его содержимого на уровне файловой системы).
Обработчик сигнала обязательно должен устанавливаться после определения $SELF и @ARGS, в противном случае может возникнуть ситуация перехвата - SIGHUP потребует перезапуска, а вы не будете знать, что запускать. Это приведет к гибели вашей программы. Некоторые серверы при получении SIGHUP не должны перезапускаться - они всего лишь заново читают свой конфигурационный файл:
$CONFIG_FILE = "/usr/local/etc/myprog/server_conf.pl";
$SIG{HUP} = \&read_config;
sub read_config { do $CONFIG_FILE;
}

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

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

17.17. Программа: backsniff

Программа backsniff регистрирует попытки подключения к портам. Она использует модуль Sys::Syslog, а ему, в свою очередь, нужна библиотека syslog.ph, которая не обязательно присутствует в вашей системе. Попытка подключения регистрируется с параметрами LOG_NOTICE и LOG_DAEMON. Функция getsocknai идентифицирует порт, к которому произошло подключение, a getpeername - компьютер, установивший соединение. Функция getservbyport преобразует локальный номер порта (например, 7) в название службы (например, "echo").
В системном журнале появляются записи:
May 25 15:50:22 coprolith snifter: Connection from 207.46.131.141 to 207.46.130.164-.echo
В файл inetd.conf включается строка следующего вида:
echo stream tcp nowait nobody /usr/scripts/snfsqrd snifter
Исходный текст программы приведен в примере 17.7. Пример 17.7. backsniff
#!/usr/bin/perl -w
# backsniff - регистрация попыток подключения к определенным портам
use Sys::Syslog;
use Socket;
# Идентифицировать порт и адрес
$sockname = getsockname(stdin)
or die "Couldn't identify myself: $!\n":
(Sport, $iaddr) = sockaddr_in($sockname);
$my_address = inet_ntoa($iaddr);
# Получить имя службы
$service = (getservbyport (sport, "tcp"))[oj || sport;
# now identify remote address
$sockname = getpeername(stdin)
or die "Couldn't identify other end: $!\n";
(Sport, $iaddr) = sockaddr_in($sockname);
$ex_address = inet_ntoa($iaddr);
# Занести информацию в журнал openlog("sniffer", "ndelay", "daemon");
syslog("notice", "Connection from %s to %s:%s\n", $ex_address,
$my_address, $service);
closelog();
exit;

17.18. Программа: fwdport

Предположим, у вас имеется защитный брандмауэр (firewall). Где-то в окружаю щем мире есть сервер, к которому обращаются внутренние компьютеры, но дос-туп к серверу разрешен лишь процессам, работающим на брандмауэре. Вы не хо тите, чтобы при каждом обращении к внешнему серверу приходилось заново регистрироваться на компьютере брандмауэра. Например, такая ситуация возникает, когда Интернет-провайдер вашей ком пании позволяет читать новости при поступлении запроса с брандмауэра, но от вергает все подключения NNTP с остальных адресов. Вы как администратор брандмауэра не хотите, чтобы на нем регистрировались десятки пользователей -лучше разрешить им читать и отправлять новости со своих рабочих станций.
Программа fwdport из примера 17.8 содержит общее решение этой проблемы. Вы можете запустить любое количество экземпляров, по одному для каждого внешнего запроса. Работая на брандмауэре, она общается с обоими мирами. Когда кто-то хочет воспользоваться внешней службой, он связывается с нашим про-кси-сервером, который далее действует по его поручению. Для внешней службы подключение устанавливается с брандмауэра и потому является допустимым. Затем программа ответвляет два процесса: первый читает данные с внешнего сервера и передает их внутреннему клиенту, а второй читает данные от внутреннего клиента и передает их внешнему серверу.
Например, командная строка может выглядеть так:
% fwdport -s nntp -I fw.oursite.com -r news.bigorg.com

Это означает, что программа выполняет функции сервера NNTP, прослушивая локальные подключения на порте NNTP компьютера fw.oursite.com. При поступлении запроса она связывается с news.bigorg.com (на том же порте) и организует обмен данными между удаленным сервером и локальным клиентом. Рассмотрим другой пример:
% fwdport -I myname:9191 -г news.bigorg.com:nntp

На этот раз мы прослушиваем локальные подключения на порте 9191 хоста myname и связываем клиентов с удаленным сервером news.bigorg.com через порт NNTP.
В некотором смысле fwdport действует и как сервер, и как клиент. Для внешнего сервера программа является клиентом, а для компьютеров за брандмауэром - сервером. Эта программа завершает данную главу, поскольку в ней продемонстрирован практически весь изложенный материал: серверные операции, клиентские операции, удаление зомби, разветвление и управление процессами, а также многое другое. Пример 17.8. fwdport
#!/usr/bin/perl -w
# fwdport - прокси-сервер для внешних служб
use strict; # Обязательные объявления
use Getopt::Long; # Для обработки параметров
use Net::hostent; # Именованный интерфейс для информации о хосте
use IO::Socket; # Для создания серверных и клиентских сокетов
use POSIX ":sys_wait_h"; # Для уничтожения зомби
mу (
%Children,
$REMOTE,
$LOCAL,
$SERVICE,
$proxy_server,
$ME,
# Хэш порожденных процессов
# Внешнее соединение
# Для внутреннего прослушивания
# Имя службы или номер порта
# Сокет, для которого вызывается accept()
# Базовое имя программы
);
($МЕ = $0) =~ s,,*/,,; # Сохранить базовое имя сценария
check_args(), # Обработать параметры
start_proxy(); # Запустить наш сервер
service_clients(); # Ждать входящих подключений
die "NOT REACHED"; # Сюда попасть невозможно
# Обработать командную строку с применением расширенной версии
# библиотеки
getopts. sub check_args { Get0ptions(
"remote=s" => \$REMOTE,
"local=s" => \$LOCAL,
"service=s" => \$SERVICE, )
or die "EOUSAGE;
usage: $0
[ --remote host ]
[ --local interface ]
[ --service service ] EOUSAGE
die "Need remote" unless $REMOTE;
die "Need local or service" unless $LOCAL || $SERVICE;
}
# Запустить наш сервер
sub start_proxy {
my @proxy_server_config = (
Proto => 'tcp',
Reuse => 1,
Listen => $OMAXCONN,
):
push @proxy_server_config, LocalPort => $SERVICE if SSERVICE:
push @proxy_server_config, LocalAddr => $LOCAL if $LOCAL;
$proxy_server = IO::socket::inet->new(@proxy_server_config)
or die "can't create proxy server: $@";
print "[Proxy server on ", ($LOCAL || $SERVICE), " initialized.]\n'
}
sub service_clients { my (
$local_client, # Клиент, обращающийся к внешней службе
$lc_info, # Имя/порт локального клиента
$remote_server, # Сокет для внешнего соединения
@rs_config, # Временный массив параметров удаленного сокета
$rs_info, # Имя/порт удаленного сервера
$kidpid, # Порожденный процесс для каждого подключения
}
$SIG{CHLD} = \&reaper; ft Уничтожить зомби acceptingo:
# Принятое подключение означает, что внутренний клиент Н хочет выйти наружу
while ($lpcal_client = $proxy_server->accept()) { $lc_info = peennfo($local_client);
set_state("servicing local $lc_info"):
printf "[Connect from $lc_info]\n";
(ars_config = (
Proto => 'tcp',
PeerAddr => $REMOTE, );
push(@rs_conflg, PeerPort => $SERVICE) if SSERVICE:
print "[Connecting to $REMOTE...":
set_state("connecting to $REMOTE"): # См. ниже
$remote_server =

I0::Socket::INET->new(@rs_config) or die "remote server: $@":
print "done]\n":
$rs_info = peerinfo($remote_server);
set_state("connected to $rs_info"):
$kidpid = fork();
die "Cannot fork" unless defined $kidpid;
if ($kidpid) {
$Children{$kidpid} = time(); . # Запомнить время запуска
close $remote_server; # Не нужно главному процессу
close $local_client; # Тоже
next; # Перейти к другому клиенту
}
# В этой точке программа представляет собой ответвленный
# порожденный процесс, созданный специально для входящего
# клиента, но для упрощения ввода/вывода нам понадобится близнец.
close $proxy_server; # He нужно потомку
$kidpid = fork():
die "Cannot fork" unless defined $kidpid;
# Теперь каждый близнец сидит на своем месте и переправляет
# строки данных. Видите, как многозадачность упрощает алгоритм
# Родитель ответвленного процесса, потомок главного процесса
if ($kidpid) {
set_state("$rs_info --> $lc_info");
select($local_client); $| = 1:
print while <$remote_server>;
kill('TERM', $kidpid); # Работа закончена,
} # убить близнеца
# Потомок потомка, внук главного процесса
else {
set_state("$rs_info <-- $lc_info");
select($remote_server); $| = 1:
print while <$local_client>:
kill('TERM', getppidO); # Работа закончена,
} # убить близнеца
exit; # Тот, кто еще жив, умирает
} continue {
accepting( );
}
}
# Вспомогательная функция для получения строки в формате ХОСТ:ПОРТ
sub peerinfo {
my $sock = shift;
my $hostinfo = gethostbyaddr($sock->peeraddr);
return sprintf("%s:%s",
$hostinfo->name || $sock->peerhost, $sock->peerport):
}
# Сбросить $0, при этом в некоторых системах "ps" выдает и нечто интересное: строку, которую мы присвоили $0! sub set_state { $0 = "$МЕ [@]" }
# Вспомогательная функция для вызова set_state sub accepting {
set_state("accepting proxy for " . ($REMOTE || $SERVICE)):
}
# Кто-то умер. Уничтожать зомби, пока они остаются.
# Проверить время их работы. sub REAPER { my $child;
my $start;
while (($child = waitpid(-1,wnohang)) > 0)
{ if ($start = $children{$child}) { my $runtime = time() - $start;
printf "Child $child ran %dm%ss\n", $runtime / 60, $runtime % 60;
delete $Children{$child};
} else {
print "Bizarre kid $child exited $?\n";
}
}
# Если бы мне пришлось выбирать между System V и 4.2, # я бы уволился. - Питер Ханиман
$SIG{CHLD} = \&reaper;
};


> Смотри также
Getopt::Long(3), Net::hostent(3), IO::Socket(3), POSIX(3), глава 16, раздел "Написание двусторонних клиентов" этой главы.
Назад
Вперед