Портал для веб-мастера

Инструменты для Бизнеса

Вход пользователей
Поиск статей
WoWeb.ru » Статьи » Программирование для Web » PERL/CGI

Avanetd - следим за сетью
Исходный код: avanetd-0.1-nix.tar.gz (2003-03-22 15:43:19/1594/169)

Ничего не происходит просто так. Вот и avanetd появился не просто так, а из-за того, что меня в очередной раз достали. И ладно бы наши офисные юзеры – на них хоть поорать можно. :) Так ведь стали приходить посетители интернет-салона, а на них уже не покричишь. Как говорится – клиент всегда прав. Устал я объяснять, что да почему и когда будет работать. Пришлось почесать в затылке и что нибудь придумать.

На шлюзе у меня, как и положено крутится Апач. Там же есть фича, показывающая состояние соединения с провайдером. Но вот в чем фишка, работает она, анализируя вывод ifconfig, а, следовательно, медленна и потенциально небезопасна. Все руки не доходят снести ее. Да к тому же порой ppp-watch так зависнет, что хрен поймешь: вроде интерфейс поднят, а связи нет. На тот момент чесание затылка в поисках решения уже довело меня до маленькой плеши. :) Тут нужно что-то до тупости элементарное, что бы работало как часы. Так, так... Что использует наш брат админ в первую очередь, когда что нибудь в сети валится? Правильно – ping. Хм... Юзать вывод от стандартного ping – ну нет уж, увольте. Так, смотрим стандартную перловую документацию. О! Есть такая буква – Net::Ping нас спасет.

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

Пора определиться с текущей задачей. Перво-наперво нас интересует доступны ли провайдерская подсеть и Интернет. Даю установку забыть о том, что конечной целью является нотификация юзеров, например интернет-салона. Если забыть не получается, посидите помедитируйте – должно помочь :) На данный момент нам нужен надежный монитор, который эффективно будет сигнализировать о том, доступен указанный хост или нет.

Пингуем хост

Net::Ping прост как два рубля. Работать с ним сможет и детсадник. Посему сначала я приведу пример, а затем приступим к разбору полетов.
my $host = '127.0.0.1'; my ($refresh_count,$if_succ) = (20,15); my $timeout_succ,$timeout_fail) = (6,1); my $ping = Net::Ping->new('icmp'); my $work = 1; $SIG{INT} = $SIG{HUP} = $SIG{TERM} = sub{$work = undef}; my ($succ,$fail,$r) = (0,0); while ($work){ 	last unless defined($r = $ping->ping($host)); 	$r ? $succ ++ : $fail ++; 	if ($succ + $fail == $refresh_count){ 		if (open STAT,">indicator.file"){ 			select((select(STAT),$|=1)[0]); 			flock STAT,2; 			print STAT $succ >= $if_succ ? 1 : 0; 			flock STAT,8; 			close STAT; 		} 		$succ = $fail = 0; 	} 	sleep($r ? $timeout_succ : $timeout_fail); } $ping->close(); 
Здесь есть несколько тонкостей, касающихся скорее не perl, а системы. В линухе, например, что бы отсылать icmp-пакеты, нужны привелегии рута. Винде хоть кол на голове чеши, но там нормального демона запаришся писать.

Итак, алгоритм... $refresh_count и $if_succ определяют пропорции удача/неудача, при которых считается, что связь с хостом есть. Иначе говоря, эти переменные определяют допустимый объем потерь пакетов. Если у вас какое-нибудь модное соединение с провайдером, то можно изменить значение $if_succ в сторону уменьшения допустимого объема потерь пакетов. Кроме того, переменная $refresh_count определяет количество проверок, через которое обновляется статистика. В нашем случае это отсылка 20 пакетов.

Переменные $timeout_succ и $timeout_fail определяют время задержки между повторными проверками в случаях, соответственно, удачного и проваленного результата ping. Зачем? А за тем, что есть такое понятие как таймаут соединения. Это значит, что попытка связаться с удаленным хостом признается проваленной, если прошло указанное время, а связи нет. Так вот, в нашем случае, если пропинговать хост не удалось, то ко времени ожидания между повторными попытками пинга прибавляется время таймаута. Дефолтный таймаут равен 5 секундам. Прибавим одну секунду на ожидание следующего пинга и получим время между удачными проверками, то есть 6, что полностью соответствует значению переменной $timeout_succ. Таким макаром мы пытаемся уравнять время обновления статистики для доступных и недоступных хостов. Хотя на самом деле, все эти усилия приведут к относительному выравниванию. То есть разница будет в любом случае, так как даже в случае удачи пинг не выполняется мгновенно.

Метод ping объекта Net::Ping возвращает неопределенное значение в случае если пропинговать хост не удалось. Это происходит тогда, когда не удается определить IP-адрес хоста. По этому сразу хочу предупредить, не ленитесь, определите IP-хостов и юзайте их. В противном случае программа будет пахать вхолостую.

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

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

Ну и после всего этого, выбираем соответствующий результату пинга таймаут и засыпаем на время. Цикл прерывается, когда программа получает сигналы INT, TERM или HUP. Вот такая незамысловатая схема работы.

Демон

Однако мы ведь собирались пинговать несколько хостов. А то как-то неудобно получается для каждого хоста запускать отдельную программу. Да и не привыкли мы писать такие простенькие программки - нас это оскорбляет. :) Ладно, ладно. Шагнем шире. Что вы думаете насчет демона, который будет запускать на проверку каждого хоста отдельный процесс, а при шатдауне корректно собирать весь мусор от дохлых (и не очень) потомков? Мне то же нравится эта идея. :) Сделаем программу ленивой: проверка на признак, не запущена ли копия демона, будет сигналом к отбою. Хотя по сути для нашего случая это не совсем оправдано – не та задача. Ну да ладно, все равно напишем, что бы знать как это делается.

Демонов мы уже писали не вспомню даже сколько раз, по этому весь нижеприведенный код не должен вас шокировать. Прежде всего определимся с используемыми переменными

#!/usr/bin/perl -w use strict; use POSIX qw/setsid/; use Net::Ping; use vars qw/$timeout_succ $timeout_fail $refresh_count $stat_dir $if_succ $waitp_timeout $pid_file $log_file @childs/; $stat_dir		= './stat'; $pid_file		= './avanetd.pid'; $log_file		= './avanetd.log'; $timeout_succ	= 6; # seconds $timeout_fail	= 1; # seconds $refresh_count	= 20; # * ping_timeout = refresh time $if_succ		= 15; $waitp_timeout	= 5; 
Здесь все должно быть понятно, за исключением быть может массива @childs и переменной $waitp_timeout. Эти переменные мы будем использовать для корректного пришибания потомков. Что дальше?
if ($#ARGV < 0){ print "Usage: ./avanetd.pl host_ip host2_ip ...\n"; exit; } if (&IsRunning){ Log("Already running!\n"); exit} -d $stat_dir or die "Stat directory not exists"; my $pid = fork; die "Couldn't fork!" unless defined($pid); exit if $pid; die "Can't start new session: $!" unless POSIX::setsid(); &StorePid; 
Дальше мы проверяем, есть ли входные аргументы. Уговор дороже денег, а мы договорились что использовать будем IP-адреса вместо имен хостов. Хотя на самом деле реализация алгоритма ничуть не препятствует настырным любителям FQDN-ов.

С помощью функции IsRunning() мы проверяем не запущена ли копия демона. Если запущена, то работать напрочь отказываемся, предварительно пожаловавшись функции Log() о попытке сверхурочного напряга.

Следующая проверка тестирует каталог, указанный в качестве каталога сохранения индикаторов. И это вроде бы логично – нафига работать, если записывать результаты некуда.

Последующие четыре строки глаза уже намозолили. Превращение в демона не меняется от решения к решению. С функцией StorePid() то же все ясно – она сохраняет идентификатор процесса в pid-файл, что бы нам потом не лазить по всяким там ps-ам, а просто глянуть в pid-файл и пришибить программу.

За-то дальше начинается уже что то более интересное

$SIG{CHLD} = 'IGNORE'; foreach my $host (@ARGV){ 	push @childs,StartMonitor($host); } my $wait = 1; $SIG{INT} = $SIG{HUP} = $SIG{TERM} = sub{$wait = undef}; sleep 1 while $wait; Log("Terminating childs...\n"); 
Первым делом нужно сообщить, что нас не интересует судьба потомков (гори оно синим пламенем :). Делать это нужно именно сейчас. Почему? Давайте представим, что в процессе ветвления потомок по какой то причине сдох. Например Net::Ping->new() не удался, а потом в цикле произошло обращение к методу ping для неопределенной переменной. Потомок сдохнет и сразу превратится в зомби, который забъется в таблицу процессов и будет сидеть там пока вы его не вытащите за уши (если найдете :). А все потому, что не установлен SIGCHLD. Так что не спорьте и устанавливайте. :)

Далее мы перебираем @ARGV, предполагая что в нем перечислены IP-адреса хостов, которые нужно пинговать. Для каждого хоста создается отдельный процесс. Выполняется это с помощью вызова функции StartMonitor() с адресом хоста в качестве аргумента. Идентификаторы потомков складываются в массив @childs, для того, что бы впоследствии родитель мог всех пришибить (прям как Иван Грозный :).

Ну и после всех этих манипуляций родительский процесс входит в цикл ожидания одного из сигналов INT, HUP или TERM.

На этом бы хотелось с демоном закончить... Нет шучу. Что самое главное в нашем деле? Правильно, чистоплотность. А что такое чистоплотность? Чисто масса на чисто объем? Нет, не в нашем случай. Программная чистоплотность – это когда после программы не остается хлама, который потом воняет на всю систему (ниче, что так откровенно?). :)

Однако, процесс завершения работы пока рассматривать рано. Давайте сначала глянем на монитор и на другие нерассмотренные функции, а вкусненькое оставим на потом. :)

Монитор

Сам процесс пинга нам уже известен, однако все же стоит взглянуть на функцию StartMonitor()
sub StartMonitor{ 	my $host = shift; 	return undef unless $host; 	my $pid = fork; 	return undef unless defined($pid); 	return $pid if $pid != 0; 	# Child code 	Log("Starting for $host\n"); 	my $ping = Net::Ping->new('icmp'); 	my $work = 1; 	$SIG{INT} = $SIG{HUP} = $SIG{TERM} = sub{$work = undef}; 	my ($succ,$fail,$r) = (0,0); 	while ($work){ 		last unless defined($r = $ping->ping($host)); 		$r ? $succ ++ : $fail ++; 		if ($succ + $fail == $refresh_count){ 			if (open STAT,">$stat_dir/avanetd.$host"){ Log("Refresh for $host: ". ($succ >= $if_succ ? "available" : "unreachable").".\n"); 				select((select(STAT),$|=1)[0]); 				flock STAT,2; 				print STAT $succ >= $if_succ ? 1 : 0; 				flock STAT,8; 				close STAT; 			} 			$succ = $fail = 0; 		} 		sleep($r ? $timeout_succ : $timeout_fail); 	} 	$ping->close(); 	exit; } 
Функция принимает в качестве параметра IP-адрес хоста. Если попытка ветвления не удалась, то функция возвращает неопределенное значение. В случае успешного ветвления, возвращается идентификатор порожденного процесса. Последующий код соответствует рассмотренному ранее варианту.

Другие функции

Функция IsRunning() выполняет проверку на повторный запуск.
sub IsRunning{ 	return 0 unless -f $pid_file; 	open PID,$pid_file or die "Can't open pid file"; 	chomp(my $prev_pid = <PID>); 	close PID; 	return 1 if kill 0 => $prev_pid; 	return 0; } 
Если обнаруживается pid-файл (а его по идее еще быть не должно, так как StorePid() не вызывалась), то мы интерпретируем его содержимое как идентификатор процесса другой копии демона. С помощью вызова kill с нулевым сигналом мы узнаем - активен ли указанный процесс. Если процесса с таким идентификатором нет, kill вернет ложь. В этом случае, функция IsRunning() возвращает значение 1 (истина), свидетельствующее о том, что демон уже запущен. Иначе возвращается значение 0 (ложь).

Оставшиеся две функции не заслуживают пристального внимания. По этому привожу их код, без каких либо комментариев.

sub StorePid{ 	open PID,">$pid_file" or die "Can't open pid file"; 	select((select(PID),$|=1)[0]); 	print PID $$; 	close PID; } sub Log{ return unless $_[0]; return unless open LOG,">>$log_file"; select((select(LOG),$|=1)[0]); flock LOG,2; print LOG scalar(localtime)," Avanetd $$: $_[0]"; flock LOG,8; close LOG; } 

Сборка мусора

Теперь мы подходим к наиболее важному (хотя и не основному) моменту программы. Почему я так заостряю внимание на сборке мусора? Ну, во-первых, я напоролся на пару багов, когда писал avanetd. И эти баги были связанны именно с некорректным завершением. Пару раз у меня оставались зомби. Было и такое, что родительский процесс завершался, а дочерние пахали, как ни в чем не бывало. Но больше всего я помучался с пришибанием потомков. Они все никак не хотели завершаться, и waitpid с нулевым вторым параметром вешала всю программу. Я уж не стал разбираться в чем дело: то ли это Net::Ping сюрпризы выкидывает, то ли еще что. Главное, что демон должен найти выход из любой ситуации. И не важно, какие причины заставили глючить потомка. В случае чего, родитель должен уметь применять крайние меры (пусть даже фатальные для потомка). Взгляните на сей шедевр
foreach $pid (@childs){ 	if ($pid){ 		Log("Killing $pid...\n"); 		kill INT => $pid; 		undef $@; 		eval { 		 local $SIG{ALRM} = sub{die"waitpid($pid) timeout\n"}; 		 alarm $waitp_timeout; 		 waitpid($pid,0); 		 alarm 0; 		}; 		if ($@){ 		 Log($@); 		 kill KILL => $pid; 		 waitpid $pid,0; 		} 	} } if (opendir STAT,$stat_dir){ 	while (my $file = readdir STAT){ 		next if !-f "$stat_dir/$file" || !$file =~ /^avanetd\./; 		Log("Unlinking $file\n"); 		unlink "$stat_dir/$file"; 	} 	closedir STAT; } unlink $pid_file; Log("Shutdown avanetd.\n"); exit; 
Цикл перебирает идентификаторы всех порожденных процессов и вытворяет над ними ужаснейшие экзекуции. Прежде всего, мы пытаемся по-хорошему сказать чилду, что бы он закруглялся, посылая ему сигнал INT. Однако мы ведь не дураки и не верим в добропорядочность потомка - кто их знает, молодежь эту. :) Чтобы не впасть в вечное ожидание, мы используем конструкцию alarm/eval. Если внутри блока eval произойдет прерывание по ALRM, то это значит, что потомок совсем отбился от рук, и отказывается завершаться. Ну что ж, берем в руки ремень и бьем потомка посильнее вторым вызовом kill, но уже с посылкой сигнала KILL, на которого у чилда нет никакой специфической реакции. А это значит, что вызов дефолтного обработчика SIGKILL безоговорочно завершит работу порожденного процесса. Мы все же дожидаемся когда это произойдет с помощью второго waitpid. Теперь после нас точно ничего не останется. :)

Вкуснятинка

Нет, нет. Это уже не сборка мусора. :) Хочу показать одну штучку, которая делает нашу программу более эффективной, а алгоритм засовывает в линейку профессиональных. Замените оператор sleep 1 while $wait в том месте, где родительский процесс входит в цикл ожидания на цикл, который чаще всего используется новичками, да и опытными программистами, которые не утруждают себя излишней мозговой активностью. Да я имею в виду конструкцию
0 while $wait; 
Замените, а потом запустите демона, с любыми аргументами. Теперь выполните
#top –p pid 
Где pid – это идентификатор родительского процесса (смотрите в pid-файле). Обратите внимание на статистику использования процессора. Запомните ее, и пришибите демона командой
#kill pid 
Где pid, опять же, идентификатор родительского процесса. Теперь восстановите цикл ожидания в родительском процессе в том виде, в каком он был изначально, и снова просмотрите статистику использования процессора. Ну, как, разница есть? Выводы делайте сами...

Резюме

Ну вот, программа готова. Можно вешать ее на автозагрузку. Теперь любая другая программа с помощью файлов-индикаторов может узнать доступен хост или нет. Как применять демона здесь рассматривать не будем. Придумайте чё нить сами, или дождитесь когда я дойду до кондиции и напишу сопутствующую статью. Пока же могу подкинуть вам пару идеек. Во-первых, у нас есть пока невостребованный админский пейджер (ищите в готовых решениях). Модуль для работы с пейджером у нас то же есть. Можно немного видоизменить программу и отправлять масяги прямо на админский пейджер. Это одно. Далее, можно написать скрипт, который будет ассоциировать статистику по хостам с определенными областями сети. Как я говорил в самом начале, это могут быть хосты из провайдерской подсети, хосты ваших подсетей и хосты Интернета. С помощью этого скрипта (который благодаря демону значительно упрощается) можно выдавать информацию посредством WEB. Вся задача сводится к правильному использованию файлов индикаторов. И в качестве напутствия – берегите нервы. :) -----------------------------7d41381a6c02aa Content-Disposition: form-data; name="allow_html_d" yes
Автор: Whirlwind · Добавлена: 2004-04-07
Просмотров: 3317 · Рейтинг: 5.0

Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]

Категории раздела
Flash
Apache
WWW
PhotoShop
Веб-дизайн
Раскрутка и реклама
Базы данных
3D графика
Хостинг
Истории веб-мастеров
Web-технологии
Сетевая безопасность
Программирование для Web
Операционные системы

Новые статьи
Лучшие статьи
Популярные статьи
Комментируемые статьи
Разделы сайта
Скрипты
Статьи
Шрифты
Флэш исходники
HTML шаблоны
Партнерки
Клипарты
Смайлы
Фоны
Гифы
Иконки
Опрос сайта
Ведете ли вы блог?
Всего ответов: 60296
Наша кнопка
WoWeb.ru - портал для веб-мастера