Исходный код: 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