Смысл сервера с ветвлением в многопроцессной обработке клиентов. Представьте себе, что несколько клиентских программ попытаются обратиться к обычному серверу, такому, как мы создали в предыдущей главе. Что произойдет? Тот клиент, который обратился первым и будет первым обработан. А что со вторым? Второй будет ждать своей очереди. Согласитесь, что это не совсем удобно. Тем более, что мы работаем в полноценной многозадачной среде.
Механизм работы Давайте попробуем спроектировать подобный сервер. Итак, наш сервер дождался входящего подключения. Вместо того, что бы сразу приступить к работе с клиентом, мы будем ответвлять новый процесс. После ответвления родительский процесс будет снова переведен в состояние ожидания входящих подключений, а порожденный процесс будет работать с клиентом. Сразу хочу заметить, что такое решение не подойдет для сервера, который подразумевает совместное использование данных, поступающих от разных клиентов. То есть, например, для организации сервера обмена пользовательскими сообщениями (когда клиент отправляет данные, которые необходимо разделить между другими клиентами) более подходящим будет метод последовательной обработки клиентов в одном процессе. Но для нашего случая этого не требуется - каждый клиент работает только с сервером и никак не взаимодействует с другими клиентами.
За основу возьмем сервер из предыдущей главы. Общая схема работы теперь выглядит так:
#!/usr/bin/perl -w # sited.pl use Socket; my $host = "localhost"; my $port = 10000; my $work = 1; my $sock_name = GetSockName($host,$port) or die "Couldn't convert into an Internet address: $!\n"; socket(SERVER,PF_INET,SOCK_STREAM,getprotobyname('tcp')) or die "Couldn't create socket: $!\n"; setsockopt(SERVER,SOL_SOCKET,SO_REUSEADDR,1) or die "setsockopt() failed: $!\n"; bind(SERVER,$sock_name) or die "Couldn't bind to port $port: $!\n"; listen(SERVER,SOMAXCONN); while($work){ print "[$$] Awaiting incoming connections...\n"; my $rem_addr = accept(CLIENT,SERVER); my ($ip,$pt) = GetSockAddr($rem_addr); print "[$$] Connection from $ip:$pt\n"; Client(); } close(SERVER); print "[$$] Server shutdown\n";
Здесь нет ничего нового, за исключением нового вызова. Всю специфическую работу выполняет функция Client(). Давайте рассмотрим ее работу:
sub Client(){ my $pid = fork; unless(defined($pid)){ print "[$$] Fork failed\n"; return; } if ($pid != 0){ # Parent process print "[$$] New client process PID=$pid\n"; close(CLIENT); return; } print "[$$] Sleep...Z-z-z-zzzzz\n"; sleep(15); print "[$$] Send to client ",scalar(localtime),"\n"; print CLIENT scalar(localtime); close CLIENT; exit; }
Как видно из кода функции, наш сервер отправляет клиенту строку с текущим временем. В родительском процессе нам не нужен клиентский дескриптор, по этому мы сразу его закрываем. Если мы этого не сделаем, то сокет останется открытым даже после того, как порожденный процесс завершит свою работу. Из этой ситуации есть еще один выход: использовать функцию shutdown(), которая закрывает сокет в любом случае (независимо от количества дескрипторов). Но, мне кажется, это не совсем правильно - оставлять открытым дескриптор, когда этого совсем не требуется. По этому, не будем привыкать к дурному тону и закроем сокет сами.
В коде часто встречаются строки типа
print "[$$] ...";
Сделано это в отладочных целях, как и использование задержки на 15 секунд. Давайте протестируем наш сервер. Откройте несколько консолей, в одной из которых запустите сервер. На других запустите нескольких клиентов. Благодаря задержке мы имеем возможность наблюдать параллельную работу с несколькими клиентами.
Завершение работы
А теперь начинаются сложности. Во-первых, представьте, что сервер завершит работу во время обработки клиента. Это очень дурно, поэтому мы должны должным образом поработать с процессом завершения работы сервера. Прежде всего, вспомним как мы решали эту проблему в статье [Управление Интернет-трафиком] когда писали демона. Мы использовали сигналы. Если я еще не говорил об этом, то скажу сейчас - очень не советую увлекаться баловством с сигналами. Использование сигналов похоже на хождение по лезвию, когда не знаешь какая система и что тебе подкинет в очередной раз. Проблема в том, что Perl - язык интерпретируемый и программы очень часто переносятся на другие платформы. Вот здесь и обнаруживаются несоответствия. По этому, только в крайнизх случаях мы будем использовать сигналы. Сейчас как раз такой 'крайний' случай.
Как определять обработчики сигналов вы помните? Добавьте обработчик для сигналов HUP,INT иTERM сразу после вызова listen():
listen(SERVER,SOMAXCONN); $SIG{HUP} = $SIG{INT} = $SIG{TERM} = \&UsKill;
Будьте внимательны, я несколько раз попадался на расстановке скобок после ссылки, а это дает вызов процедуры вместо ссылки. Как вы уже наверное догадались, в обработчике мы выполняем сброс флага $work. Пятерка тому, кто спросит - ну а если в момент прихода сигнала мы будем заблокированы вызовом функции accept()? Совершенно верно - флаг то мы сбросим, но так и будем сидеть до первого входящего соединения. Предлагаю решить эту проблему созданием фиктивного подключения прямо из обработчика:
sub UsKill{ print "[$$] Interrupted by signal: ",shift(),"\n"; $work = undef; return unless socket(KILL,PF_INET, SOCK_STREAM,getprotobyname('tcp')); connect(KILL,$sock_name); close(KILL) }
Вот таким образом мы решаем задачу непременного выхода из цикла. Ладненько, первая проблема решена - теперь сервер управляется с помощью сигналов. Давайте проверим работоспособность сервера: запустите сервер и с другой консоли инициируйте подключение. Теперь, пока еще клиент не обработан полностью, в консоли с сервером нажмите Ctrl+C, таким обазом послав серверу сигнал INT. Все должно завершиться через 15 секунд (так как все клиентские подключения замораживаются на 15 секунд, в том числе и последнее фиктивное подключение). А теперь представьте, что время на обработку фиктивного соединения гораздо меньше, чем на обработку клиента. Ну и что - спросите вы. На реальном примере можно описать это следующим образом: вы заходите в магазин, подходите к продавцу, говорите "дайте мне булку хлеба", продавец берет с вас деньги и... исчезает в неопределенном направлении. Ну и как, вам бы так понравилось? То-то и оно. Посему, в нашем случае все усложняется. Мы должны каким-то образом дождаться завершения работы всех клиентских процессов. Каким образом? Ну, можно вспомнить про такую функцию как waitpid(). Так как waitpid() работает с идентификаторами процессов, то нам нужно где- то их запомнить. Объявите хэш %pid где-нибудь в начале программы (можно сразу после объявления переменной $work = 1). Почему хэш, а не массив? Да потому, что из хэша легче удалять элементы. Далее, сразу после закрытия серверной стороны сокета (сразу после цикла while) добавляем следующий код:
close(SERVER); print "[$$] Server shutdown\n"; my @pids = keys(%pid); foreach my $k (@pids){ print "[$$] Waiting process $k...\n"; waitpid($k,0); }
Вот таким образом мы дожидаемся завершение каждого клиентского процесса. Напомню, что функция waitpid() с идентификатором процесса в качестве первого аргумента и значением 0 в качестве второго ожидает завершение указанного процесса (если быть точнее, смену статуса).
Не расслабляйтесь - теперь еще одна проблема. После того, как клиентский процесс завершит свою работу, он превращается в зомби. Никакой мистики, просто система оставляет информацию о завершившемся процессе в таблице процессов для того, что бы родительский процесс смог проанализировать результат работы потомков. Представляете, что будет когда наш сервер проработает некоторое время. Правильно, таблица процессов будет переполнена записями о процессах-зомби. Не думаю, что после этого вам будет приятно выслушивать длинные нотации администратора вашей системы. Для решения этой проблемы можно указать системе что нас не интересуют потомки, то есть отменить регистрацию потомков. Достигается это игнорированием сигнала CHLD.
Но, помимо того, что остается зомби, хэш %pid, в который добавляется идентификаторы всех порожденных процессов тоже будет бесконечно заполняться. Его нужно как то чистить. Чистку хэша можно выполнять каждый раз после обработки нового входящего соединения. Давайте так и поступим.
Добавьте в код после переопределения обработчика сигналов HUP,INT и TERM следующий оператор:
$SIG{CHLD} = 'IGNORE';
А внутрь цикла while, после вызова функции Client() следующие строки:
my @pids = keys(%pid); foreach my $k (@pids){ if (kill 0 => $k){ print "[$$] $k is alive\n"; }else{ print "[$$] $k is deceased\n"; delete($pid{$k}); } }
Мы перебираем идентификаторы всех порожденных процессов и удаляем те, которые выполнили свою работу. Функция kill() посылает определенный сигнал процессу или группе процессов. Первым аргументом при вызове этой функции должен быть указан сигнал, а последующими - идентификаторы процессов. Если послать процессу сигнал 0, то функция kill() вернет статус процесса: true, если процесс работает, или же false в случае, если процесс умер или сменил действующий идентификатор. Во втором случае уточнить статус процесса можно с помощью модуля POSIX, в котором описаны константы EPERM и ESRCH:
use POSIX qw/:errno_h/; if (kill 0 => $pid){ # Процесс жив }elsif($! == EPERM){ # Сменил идентификатор }elsif($! == ESRCH){ # Зомби }
Для нашего случая этого делать не обязательно. В общем, вот таким образом мы контролируем своевременное удаление ненужных идентификаторов. Запустите и проверьте сервер - он должен дожидаться окончания выполнения каждого дочернего процесса, а так же своевременно вычищаеть хэш %pid.
Но есть и еще одно - более изящное решение этой проблемы. Мы можем переопределить обработчик сигнала CHLD. Сигнал CHLD приходит каждый раз, когда умирает дочерний процесс. Исправте код - вместо игнорирования сигнала CHLD определите ему новый обработчик - функцию ReapChld().
$SIG{CHLD} = \&ReapChld;
То, что мы добавили внутрь цикла while() для своевременной очистки хэша %pid то же нужно удалить. Давайте рассмотрим функцию ReapChld():
sub ReapChld{ while(my $kid = waitpid(-1,WNOHANG)){ last if $kid == -1; if (WIFEXITED($?)){ print "[$$] Reap child process $kid\n"; delete($pid{$kid}); } } $SIG{CHLD} = \&ReapChld; }
Итак, функция waitpid() со значением -1 (указывает на любой процесс) в качестве первого аргументаи флагом WNOHANG в качестве второго возвращает 0, в случае если нет ни одного процесса-зомби. Эта функция вычищает один процесс и по этому мы запускаем обработку в цикле - так, на всякий случай. Цикл нужен для того, что бы быть уверенным в том что вычищены все потомки. Так, например, если несколько потомков умрут когда родительский процесс был неактивен, после перехода в активное состояние родитель получает всего один сигнал CHLD при нескольких мертвых потомках.
Если функция waitpid() возвращает -1, то это означает что нет ни одного зомби. Поэтому, значение -1 является сигналом к прерыванию цикла. После вызова waitpid() встроенная переменная $? содержит статус ожидания - тот самый, из-за которого появляется зомби. Функция WIFEXITED модуля POSIX проверяет статус завершения потомка. Дело в том, что сигнал CHLD посылается не только в случае смерти потомка. Мы используем функцию WIFEXITED для определения - умер ли потомок, или же произошло другое событие.
Мне еще ни разу не встречалась ситуация, когда сбрасывался обработчик сигнала CHLD. Но в литературе допускается возможность сброса обработчика, поэтому, что бы не рисковать в ущерб стабильности мы восстанавливаем значение обработчика:
$SIG{CHLD} = \&ReapChld
Теперь добавте оператор подключения модуля POSIX (мы ведь используем его константы и функции). Можно сразу после подключения модуля Socket:
use POSIX qw/:signal_h :sys_wait_h :errno_h /;
Ну вот, вроде и все. Пора тестировать. Смоделируйте различные ситуации: поступление различных сигналов в процессе обработки клиентов и вне его. После тестирования с двумя клиентами у меня на экране появилось следующее:
[18539] Awaiting incoming connections... [18539] Connection from 127.0.0.1:3347 [18542] Sleep...Z-z-z-zzzzz [18542] Interrupted by signal: INT [18542] Send to client Thu Aug 22 15:00:04 2002 [18539] Awaiting incoming connections... [18539] Connection from 127.0.0.1:3347 [18539] New client process PID=18542 [18539] Awaiting incoming connections... [18539] Connection from 127.0.0.1:3348 [18544] Sleep...Z-z-z-zzzzz [18544] Interrupted by signal: INT [18544] Send to client Thu Aug 22 15:00:04 2002 [18539] Awaiting incoming connections... [18539] Connection from 127.0.0.1:3347 [18539] New client process PID=18542 [18539] Awaiting incoming connections... [18539] Connection from 127.0.0.1:3348 [18539] New client process PID=18544 [18539] Awaiting incoming connections... [18539] Reap child process 18542 [18539] Interrupted by signal: INT [18539] Connection from 127.0.0.1:3349 [18547] Sleep...Z-z-z-zzzzz [18547] Send to client Thu Aug 22 15:00:19 2002 [18539] Awaiting incoming connections... [18539] Connection from 127.0.0.1:3347 [18539] New client process PID=18542 [18539] Awaiting incoming connections... [18539] Connection from 127.0.0.1:3348 [18539] New client process PID=18544 [18539] Awaiting incoming connections... [18539] Reap child process 18542 [18539] Interrupted by signal: INT [18539] Connection from 127.0.0.1:3349 [18539] New client process PID=18547 [18539] Server shutdown [18539] Waiting process 18547... [18539] Reap child process 18544 [18539] Waiting process 18544...
Судя по идентификаторам процессов, сначала выполняется и завершается порожденный процесс и только после этого выводятся сообщения о порождении процесса. Как вы думаете, почему? Правильно, из-за буферизации. Давайте отключим буферизацию вывода. Для этого добавим сразу после подключения модулей Socket и POSIX:
$| = 1;
Запустите и проверьте. Ну, что опять не так? Подозрительные записи о тройной обработке сигнала INT при чем в различных процессах. О чем это говорит? Что сигнал INT получают и порожденные процессы. А так как мы переопределяем обработчик сигнала INT до разветвления, порожденный процесс наследует так же и обработчик. Для того, что бы потомок не реагировал на сигналы нужно просто отключить их обработку после того, как произойдет ветвление. Меняем функцию клиент:
sub Client(){ my $pid = fork; unless(defined($pid)){ print "[$$] Fork failed\n"; return; } if ($pid != 0){ # Parent process print "[$$] New client process PID=$pid\n"; close(CLIENT); return; } $SIG{INT} = $SIG{HUP} = $SIG{TERM} = 'IGNORE'; print "[$$] Sleep...Z-z-z-zzzzz\n"; sleep(15) if defined($work); print "[$$] Send to client ",scalar(localtime),"\n"; print CLIENT scalar(localtime); close CLIENT; exit; }
Теперь должно быть все в порядке:
[18661] Awaiting incoming connections... [18661] Connection from 127.0.0.1:3425 [18661] New client process PID=18663 [18661] Awaiting incoming connections... [18663] Sleep...Z-z-z-zzzzz [18661] Connection from 127.0.0.1:3426 [18661] New client process PID=18665 [18661] Awaiting incoming connections... [18665] Sleep...Z-z-z-zzzzz [18661] Interrupted by signal: INT [18661] Connection from 127.0.0.1:3427 [18661] New client process PID=18666 [18661] Server shutdown [18661] Waiting process 18663... [18666] Sleep...Z-z-z-zzzzz [18666] Send to client Thu Aug 22 15:26:49 2002 [18661] Reap child process 18666 [18663] Send to client Thu Aug 22 15:26:56 2002 [18661] Waiting process 18665... [18665] Send to client Thu Aug 22 15:26:59 2002 [18661] Waiting process 18666...
Ну вот, любо-дорого смотреть. Кажется все. Можете взять исходник на всякий случай.