После прочтения данной статьи Вы узнаете, как создавать серверы-демоны, что такое Интернет сокеты, процессы-зомби и сигналы.
Для начала необходимо отличать понятия обычного процесса, процесса – демона и системного процесса. Все сразу видно по таблице процессов. Запустите команду :
# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.1 0.1 2060 620 ? Ss 08:27 0:00 init [5]
root 2 0.0 0.0 0 0 ? S< 08:27 0:00 [migration/0]
root 5222 0.0 0.2 4532 1412 pts/1 Ss 08:30 0:00 -bash
root 5274 0.0 0.1 4244 928 pts/1 R+ 08:33 0:00 ps aux
Системные процессы - те у которых TTY = ? и VSZ = 0
Демоны – те у которых TTY = ? и VSZ ≠ 0
Пользовательские процессы – те у которых TTY = pts/1(к примеру) и VSZ ≠ 0
Если при запуске Вашей программы Вы не добились TTY = ? и VSZ ≠ 0, то Ваш процесс не является демоном.
Процессы демоны обычно запускаются при загрузке системы и завершаются при завершении работы системы.
Для создания демона необходимы следующие действия - сначала завершить родительский процесс:
$pid = fork();
exit() if $pid;
Затем разорвать связь с управляющим терминалом и создать связь с новым терминалом при помощи команды:
POSIX::setsid();
Описанный в данной статье сервер будет поддерживать Интернет сокет, поэтому дадим определения сокетам.
Сокеты являются "конечными пунктами" в процессе обмена данными.
Обмен данными через сокеты может осуществляться на одном компьютере или через Интернет.
Существуют два самых распространенных типа сокетов: потоковые и датаграмные. Потоковые сокеты обеспечивают двусторонние, последовательные и надежные коммуникации; они похожи на каналы (pipes). Датаграммные сокеты не обеспечивают последовательную, надежную доставку, но они гарантируют, что в процессе чтения сохранятся границы сообщений. (Описание сокетов взято из книги "PERL: Библиотека программиста" Т. Кристиансен, Н. Торкингтон)
Сокеты также делятся по областям(domain): сокеты Интернета и сокеты UNIX. Интернет сокет содержит в себе две составляющие: хост (IP-адрес в определенном формате) и номер порта. UNIX сокеты представляют собой файлы (пример сокета Unix применяется в сервере mysqld socket=/var/lib/mysql/mysql.sock ).
Для создания Интернет сокета на сервере с портом 23 необходима следующая команда:
$server = new IO::Socket::INET(LocalPort => 23, TYPE => SOCK_STREAM, Reuse => 1, Listen => 10);
Такой командой создается потоковый сокет, который будет слушать 23 порт, с 10 подключениями в очереди , и с возможностью использования того же адреса после перезапуска сервера.
Для обслуживания запросов от нескольких клиентов необходим сервер с ветвлением. Для этого при каждом входящем подключении от клиента необходимо делать копию родительского процесса (сервера) и "обслуживать" клиента непосредственно ответвленным (скопированным, дочерним) процессом. Делается это при помощи команды fork. Функция fork создает клон текущего процесса. В родительский процесс она возвращает $pid порожденного процесса, а в дочернем процессе возвращаемое значение равно нулю.
Поэтому для того чтобы создать самый простой сервер с ветвлением необходимо сделать следующее:
while($client = $server->accept()) { defined(my $child_pid=fork()) or die "Can't fork new child $!"; ###Родительский процесс идет в конец ### ###и ждет следующего подключения ### next if $child_pid; ###Дочернему процессу копия сокета не нужна ### if($child_pid == 0) { close($server); } ###Здесь идет обработка клиентского запроса, ### ###выполнение всех необходимых команд ### .... exit;### В конце завершаем порожденный процесс ### } continue { close($client); ### Не нужно родительскому процессу ### }
В сервере с ветвлением при завершении порождаемого процесса (exit) и не завершении родителя появляются процессы-зомби.
Процесс-зомби - дочерний процесс в Unix системе, завершивший свое выполнение, но еще присутствующий в таблице процессов.
Зомби можно узнать в списке процессов (выводимых утилитой ps) по флагу «Z» в колонке STAT.
Если родительский процесс игнорирует обработчик $SIG{CHLD}, то зомби остаются до завершения родителя.
Необходимо добавить функцию отслеживания сигнала $SIG{CHLD} :
sub REAPER { while ((my $waitedpid = waitpid(-1,WNOHANG)) > 0) { } $SIG{CHLD} = \&REAPER; }
И перед разветвлением вызвать обработчик
$SIG{CHLD} = \&REAPER;
defined(my $child_pid=fork()) or die "Can't fork new child $!";
Тогда наши наши уже не нужные отработавшие процессы будут корректно завершаться.
%SIG - это хэш ссылок на обработчики сигналов ( ссылки на функции).
Сигнал $SIG{INT} обычно возникает при нажатии Ctrl+C и требует, чтобы процесс завершил свою работу.
Сигнал $SIG{TERM} посылается командой kill при отсутствии явно заданного имени сигнала.
К примеру для обработки сигналов $SIG{INT} и $SIG{TERM} можно написать следующую функцию:
sub signal_handler{ $time_to_die = 1; close($server); } $SIG{INT}= $SIG{TERM} = \&signal_handler;
Также сервер должен обрабатывать сигнал HUP - который посылается процессу при при разрыве связи (hang-up) на управляющем терминале, либо когда программа должна перезапуститься или заново перечитать свою конфигурацию. В нашем случае когда сервер должен перечитать список разрешенных команд.
Напишем обработчик сигнала HUP следующим образом:
$SIG{HUP} = \&rereading_config; sub rereading_config{ @def_commands=(); open(FILECONF,$conf_name) or die "Can't open config file \n"; while(<FILECONF>){ chomp; push(@def_commands, $_); } close(FILECONF); }
Вызвать сигнал HUP для процесса сервера можно так:
kill -s HUP номер процесса
Наш сервер будет работать на 23 порту (слушать 23 порт). Он будет обрабатывать все входящие соединения на этом порту. Если пользователь будет посылать команду, указанную в конфигурационном файле, то сервер будет выполнять её, если же команда будет не из разрешенного списка, то сервер будет её пропускать и переходить к следующей команде. Для того чтобы обновить список разрешенных команд, необходимо серверу послать сигнал HUP.
Запускаем демона на сервере:
[root@server ~]# ./simple-telnetd.pl
Проверяем, что 23 порт прослушивается и ожидает соединения с клиентом:
[root@server ~]# netstat -an|grep LISTEN
tcp 0 0 0.0.0.0:23 0.0.0.0:* LISTEN
Теперь с машины клиента соединяемся по telnet на серевер и вводим команды, которые указаны во входных настройках (/etc/simple-telnetd.conf):
[root@client ~]# telnet 192.168.254.40
Trying 192.168.254.40...
Connected to localhost (192.168.254.40).
Escape character is '^]'.
Command :uname -a
Linux redhat2.ascon.ru 2.6.18-92.el5 #1 SMP Tue Apr 29 13:16:12 EDT 2008 i686 i686 i386 GNU/Linux
Command :uname
Linux
Command :who
root pts/1 2010-01-11 08:30 (192.168.254.1)
Command :
Видим, что программа реагирует корректно. Если ввести не корректную команду, то будет предложено ввести следующую команду.
Если в это время на сервере вывести список установленных соединений, то увидим соединение с клиентом:
[root@server ~]# netstat -an|grep EST
tcp 0 0 192.168.254.40:23 192.168.254.30:49598 ESTABLISHED
Если в это время на клиенте вывести список установленных соединений, то увидим соединение с сервером:
[root@client ~]# netstat -an|grep EST
tcp 0 0 192.168.254.30:49598 192.168.254.40:23 ESTABLISHED
После выхода клиентом из клиентской программы telnet соединение на обоих концах разорвется. Если вызывать клиентскую программу telnet несколько раз, то будут образовываться сразу несколько соединений с сервером, это реализуется с помощью распараллеливания процессов ( копирования самого себя с помощью fork).
#!/usr/bin/perl -w #################### #Shpatserman Maria #01.12.2009 #Simple Telnetd #################### ###Подключение всех необходимых модулей### use strict; use POSIX; use POSIX ":sys_wait_h"; use IO::Socket; use IO::Handle; ###Создаем процесс-демон### my $pid= fork(); exit() if $pid; die "Couldn't fork: $! " unless defined($pid); ###Создаем связь с новым терминалом### POSIX::setsid() or die "Can't start a new session $!"; ###Переменная - бесконечное время жизни сервера### my $time_to_die =0; ###Переменная - интернет-сокет или сервер### my $server; ###Функция обработчик сигналов INT и TERM### ###Она срабатывает перед этими сигналами### sub signal_handler{ $time_to_die = 1; close($server); }
$SIG{INT}= $SIG{TERM} = \&signal_handler; ###Файл конфигурации с набором команд, которые обрабатывает наш сервер### my $conf_name="/etc/simple-telnetd.conf"; ###Массив где хранится список этих команд ### my @def_commands; ###Функция обработчик сигнала HUP перечитывает конфигурационный файл### ###и обновляет массив @def_commands### $SIG{HUP} = \&rereading_config; sub rereading_config{ @def_commands=(); open(FILECONF,$conf_name) or die "Can't open config file \n"; while(<FILECONF>){ chomp; push(@def_commands, $_); } close(FILECONF); } ###Функция обработчик сигнала CHLD - для уборки процессов зомби ### sub REAPER { while ((my $waitedpid = waitpid(-1,WNOHANG)) > 0) { } $SIG{CHLD} = \&REAPER; } ###Заполняем массив разрешенных команд при старте сервера### rereading_config(); ###Создаем интернет сокет на порту 23### my $server_port=23; $server= new IO::Socket::INET(LocalPort => $server_port, TYPE => SOCK_STREAM, Reuse => 1, Listen => 10) or die "Couldn't be a tcp server on port $server_port: $@\n"; ###Сервер работает до бесконечности пока его не вырубит Term ###
until($time_to_die){ my $client; ###Обрабатываем входящие подключения while($client = $server->accept()){ ###Включаем обработку зомби### $SIG{CHLD} = \&REAPER; ###Тот который постучался, отделяем в отдельный процесс### defined(my $child_pid=fork()) or die "Can't fork new child $!"; ###Родительский процесс идет в конец и ждет следующего подключения### next if $child_pid; ###Дочернему процессу копия сокета не нужна, её закрываем### if($child_pid == 0) { close($server); } ###Очистка буфера### $client->autoflush(1); my $is_def_command=0; print $client "Command :"; ###Считываем комады от клиента построчно### while(<$client>){ ###Если строка пустая переходим в конец блока### next unless /\S/; ###Запоминаем полную введенную строку, к примеру df -h ### my $full_enter_str = $_; chomp($full_enter_str); ###Переменная – имя команды, к примеру df### my $enter_command=""; ###Переменная – набор параметров, к примеру -h### my $enter_params=""; ###Разбиваем введенную строку на имя команды и параметры### ########################################################### if($full_enter_str =~ /(\w+)(\s+)(.*)(\s*)/){ $enter_command = $1; $enter_params = $3; } elsif($full_enter_str =~ /(\w+)/){ $enter_command = $1; $enter_params = ""; } else { $enter_command = ""; $enter_params = ""; } ###Сравнение имени команды с набором разрешенных команд ### ###Просматриваем разрешенные команды в конфигурационном файле ### foreach (@def_commands) { if($enter_command eq $_) { $is_def_command=1;} } ###Если команда разрешена — выполняем её### ########################################### if($is_def_command){ my @lines = qx($enter_command $enter_params); foreach (@lines){ print $client $_; } } } continue { print $client "Command :"; $is_def_command=0; } exit; } continue { close($client); } }
Исходный текст программы: simple-telnetd.pl
Пример входных настроек: simple-telnetd.conf
ЗЫ хороший мануал perldoc perlipc