1目录1概述...........................................................................................................................................12监控Server................................................................................................................................13监控Client...............................................................................................................................104运行方法及配置文件.............................................................................................................201概述本监控程序用来监控RedHatLinux主机的系统状况,包括CPU负载、内存使用、网络状况、服务端口、磁盘空间等。本程序基于C/S结构,全部用Perl实现。基本原理是,Client运行在各个需要监控的主机上,并起一个Socket端口。Server定期轮询各个Client,根据配置文件里的选项发送扫描命令,取得客户端的状态,若有异常则发送邮件报警。这个程序属于偶的业余作品,只花了1天半的时间写成。Perl的优点就是快速开发,且自身有强大的类库,可实现很复杂的功能。这个程序除了Net::SMTP模块外,没有使用任何外部类库。代码仅做参考,可以修改它们作为己用。程序运行稳定,目前已监控了偶们广州公司的上百台Linux服务器。但代码至少有如下不足:1.没写任何安全控制的代码,包括进程ID切换,chroot,Socket会话加密及认证等。所以客户端默认只在内网IP上监听,且不要以root运行。2.客户端检查系统状态主要使用了外部系统调用,其实大部分系统状态,都可从POSIX函数或Linux自身的状态表里获取(如/proc下的文件)。偶不想花时间去研究那些,直接systemcall了。2监控Server#!/usr/bin/perlusestrict;useIO::Socket;usePOSIXqw(:signal_hWNOHANGsetsid);useNet::SMTP;useFcntlqw(:DEFAULT:flock);#电子邮件地址,用来接受报警my@emails=(sa@sample-inc.com,2dba@sample-inc.com,#其他email地址);#程序运行的主目录,这里是写死的,需要修改成自己的目录,或写成配置文件my$rundir='/home/afoo/monsvr';#程序运行的PID文件my$pid_file=$rundir./monsvr.pid;#配置文件,定义各客户端的IP及扫描参数my$cfg_file=$rundir./monsvr.cf;#2个日志文件,错误日志及运行日志my$err_log=$rundir./monsvr.err;my$log_file=$rundir./monsvr.log;#客户端的运行端口my$agent_port=7780;#主循环退出的条件,等于0不退出,大于0退出my$DONE=0;#每10分钟执行一次扫描my$scan_inter=10;#最后一次扫描的时间my$last_scan_time=0;#记录子进程ID的状态表my%status;#MTA主机的IP及端口,用来发送报警邮件。#Linux默认安装了sendmail,打开它即可。my$mtahost='127.0.0.1';my$mtaport=25;#安装自己的信号处理器#子进程退出时,从状态表里删掉子进程ID$SIG{CHLD}=sub{while((my$child=waitpid(-1,WNOHANG))0){delete$status{$child}}};#SIGTERM或SIGINT信号导致程序退出$SIG{TERM}=$SIG{INT}=sub{$DONE++};#发送HUP信号可让程序reload自身(kill–HUP`catmonsvr.pid`)$SIG{HUP}=\&do_hup;#die和warn调用时,将输出重定向到日志文件$SIG{__DIE__}=\&log_die;$SIG{__WARN__}=\&log_warn;#获取进程状态,首先从PID文件里获取已在运行的进程ID(如果有的话),如果kill0=$pid返回真,则表示进程已在运行,服务拒绝启动。如果PID文件不可写,服务也拒绝启动。3if(-e$pid_file){open(PIDFILE,$pid_file)ordie[EMERG]$!\n;my$pid=PIDFILE;closePIDFILE;die[EMERG]processisstillrun\nifkill0=$pid;die[EMERG]can'tremovepidfile\nunless-w$pid_file&&unlink$pid_file;}#程序进入后台,并将自身的进程ID写入PID文件。open(HDW,,$pid_file)ordie[EMERG]$!\n;my$pid=daemon();printHDW$pid;closeHDW;#主循环while(!$DONE){#如果距上一次扫描的时间间歇大于$scan_inter定义的分钟,并且没有扫描子进程存在,则启动一个扫描子进程if((time-$last_scan_time$scan_inter*60)&&!%status){#启动扫描后,将最后一次扫描的时间,更新为当前时间$last_scan_time=time;#设置信号掩码,屏蔽前面重载的几个信号,防止这几个信号在fork的时候进入my$signals=POSIX::SigSet-new(SIGHUP,SIGINT,SIGTERM,SIGCHLD);sigprocmask(SIG_BLOCK,$signals);#fork子进程my$child=fork();diecan'tfork$!unlessdefined$child;#父进程里取消信号掩码if($child){$status{$child}=1;sigprocmask(SIG_UNBLOCK,$signals);#子进程里先将HUP,INT,TERM,CHLD信号恢复成默认,然后也取消信号掩码。#然后子进程调用do_scan()函数进行扫描和报警,处理完后就写日志并退出4}else{$SIG{HUP}=$SIG{INT}=$SIG{TERM}=$SIG{CHLD}='DEFAULT';sigprocmask(SIG_UNBLOCK,$signals);my$results=do_scan();do_warn($results)if@$results;write_log([$$],Allscanfinished);exit0;}}#父进程休眠10秒,并继续循环sleep10;}#-------------------#下面定义子函数#-------------------#使程序进入后台的函数,原理很简单,就是fork一个子进程,父进程die掉,子进程调用setsid()使自己成为进程组的领导。然后重定向3个标准I/O设备到/dev/null。subdaemon{my$child=fork();die[EMERG]can'tfork\nunlessdefined$child;exit0if$child;setsid();open(STDIN,/dev/null);open(STDOUT,/dev/null);open(STDERR,&STDOUT);chdir$rundir;umask(022);$ENV{PATH}='/bin:/usr/bin:/sbin:/usr/sbin';return$$;}5#写日志的函数subwrite_log{my$time=scalarlocaltime;open(HDW,,$log_file);flock(HDW,LOCK_EX);printHDW$time,,join'',@_,\n;flock(HDW,LOCK_UN);closeHDW;}#当调用die时,会执行这个函数。也就是先将异常消息写入错误日志,再真正的die。sublog_die{my$time=scalarlocaltime;open(HDW,,$err_log);printHDW$time,,@_;closeHDW;die@_;}#当调用warn时,会执行这个函数。sublog_warn{my$time=scalarlocaltime;open(HDW,,$err_log);printHDW$time,,@_;closeHDW;}#扫描函数subdo_scan{#先读取配置文件,获取要扫描的IP,以及扫描哪些选项my$scan_cfg=get_config();#这个数组用来记录扫描结果my@results;#在for循环里逐台扫描formy$hid(keys%{$scan_cfg}){6#在eval里执行扫描,并设置超时30秒。如果30秒内客户端未返回结果(客户端所在的主机负载很重时,可能会这样),则记录扫描异常的结果。eval{local$SIG{ALRM}=sub{dieScanTimeout,$scan_cfg-{$hid}-{IP}iswrong\n};alarm30;my$re=scan_a_host($scan_cfg-{$hid});@results=(@results,@$re);alarm0;};push@results,ScanTimeout,$scan_cfg-{$hid}-{IP}iswrongif$@;}return\@results;}#单独扫描某台机的函数subscan_a_host{my$host=shift;#ahashrefmy@results;#创建到客户端的socketmy$sock=IO::Socket::INET-new(PeerAddr=$host-{IP},PeerPort=$agent_port,Proto='tcp');#如果创建socket不成功,则说明客户端的监听端口可能down了unless(defined$sock){push@results,$host-{IP}:monitorclientseemsdown;warn([WARN]$host-{IP}:monitorclientseemsdown\n);return\@results;}write_log([$$],preparetodo_scanfor$host-{IP});#把配置文件里定义的扫描选项,发送到clientformy$key(keys%{$host}){print$sock$key$host-{$key}\n}7#发送完后关闭写socket,这步很重要,否则client不知道server已写完,会阻塞在那里等待,并造成和server的交互阻塞。$sock-shutdown(1);#在while循环里读取客户端返回的扫描结果while($sock){chomp;push@results,$_;}#关闭socket并返回结果给调用者$sock-close;return\@results;}#该函数用来读取配置文件,并将结果放入一个Hash。纯文本处理,没有特殊的技巧。请对照配置文件