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
 
Оригинал статьи: http://woweb.ru/publ/58-1-0-423
 
Рейтинг@Mail.ru
© Студия WEB-дизайна PM-Studio, 2005. Все права защищены.