守护进程模块的开发
目标:解决服务程序死机(挂起)的问题,守护进程将终止它,完成善后工作
此后,服务程序被终止后,调度程序将可以重启它
这将保证了系统的正常运行
-
心跳机制
每个服务程序在共享内存中占用一段空间作为自己的心跳,给出心跳结构
struct st_procinfo { int pid; //进程id char pname[51]; //进程名称 int timeout; //超时时间 time_t atime; //最后一次心跳的时间 }
系统周期巡检每个服务程序的心跳,若超时(超时时间=系统当前时间 - 最后一次心跳时间),则没有心跳的进程可被终止,调度程序将重启该进程。
-
heartBeat.cpp
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <signal.h> #include <sys/ipc.h> #include <sys/shm.h> #include "_public.h" #define MAXNUMP_ 1000 // 最大进程数量 #define SHMKEYP_ 0x5095 // 共享内存的key struct st_pinfo // 心跳结构 { int pid; // 进程id char pname[51]; // 进程名称 int timeout; // 超时时间 time_t atime; // 最后一次心跳的时间 }; int main(int argc, char *argv[]) { if (argc < 2) { printf("Using:./book procname\n"); return 0; } int m_shmid = 0; if ((m_shmid = shmget(MAXNUMP_, SHMKEYP_ * sizeof(struct st_pinfo), 0640 | IPC_CREAT)) == -1) // 创建/获取共享内存,大小为n*sizeof(struct st_pinfo) { printf("shmget(%x) failed\n", MAXNUMP_); return -1; } CSEM m_sem; if (m_sem.init(SHMKEYP_) == false) { printf("m_sem.init(%x) failed\n", SHMKEYP_); return -1; } struct st_pinfo *m_shm; // 1.将共享内存连接到当前进程的地址空间 m_shm = (struct st_pinfo *)shmat(m_shmid, 0, 0); struct st_pinfo stpinfo; memset(&stpinfo, 0, sizeof(struct st_pinfo)); stpinfo.pid = getpid(); STRNCPY(stpinfo.pname, sizeof(stpinfo.pname), argv[1], 50); // 进程名称 stpinfo.timeout = 30; // 超时时间30s stpinfo.atime = time(0); int m_pos = -1; // 2.共享内存中查找一个空位置,把当前进程的心跳信息存入共享内存中 for (int ii = 0; ii < SHMKEYP_; ii++)// [修复2]如果共享内存中存在当前进程编号,一定是其他进程残留的数据,当前进程就重用该位置。 { if (m_shm[ii].pid == stpinfo.pid) { m_pos = ii; break; } /* code */ } if (m_pos == -1) { for (int ii = 0; ii < SHMKEYP_; ii++) { if ((m_shm + ii)->pid == 0) // 3.找到空位置 { m_pos = ii; break; } } } if (m_pos == -1) { printf("共享内存空间已用完\n"); return -1; } memcpy(m_shm + m_pos, &stpinfo, sizeof(struct st_pinfo)); while (true) { m_shm[m_pos].atime = time(0);//更新共享内存中本进程的心跳时间 sleep(10); /* code */ } memset(m_shm + m_pos, 0, sizeof(struct st_pinfo));// m_shm[m_pos].pid=0 //4.把当前进程从共享内存中移除 shmdt(m_shm); // 5.把共享内存从当前进程中分离 return 0; }
下面进行gdb调试,进程1运行 pname =“aaa”,进程2运行pname=“bbb”,在3号窗口调试,断点设置在
**memcpy(m_shm + m_pos, &stpinfo, sizeof(struct st_pinfo));**处
可见,两服务程程序(进程1&进程2)的心跳信息已经录入
- 进程在独立访问共享内存的时候,为了保证其稳定占用这个空间(即心跳结构体为该进程开辟的一片内存)需要加锁,访问完共享内存解锁
给出互斥锁的定义
CSEM m_sem;
if ((m_sem.init(SHMKEYP_) )== false)
{
printf("m_sem.init(%x) failed\n", SHMKEYP_);
return -1;
}
heartBeat已经能完成对将服务程序的心跳记录写入共享内存
下面将遍历共享内存,将没有心跳的进程终止
/* checkproc.cpp 遍历共享内存,终止没有心跳的进程*/
#include "_public.h"
// 程序运行的日志。
CLogFile logfile;
int main(int argc,char *argv[])
{
// 程序的帮助。
if (argc != 2)
{
printf("\n");
printf("Using:./checkproc logfilename\n");
printf("Example:/project/tools1/bin/procctl 10 /project/tools1/bin/checkproc /tmp/log/checkproc.log\n\n");
printf("本程序用于检查后台服务程序是否超时,如果已超时,就终止它。\n");
printf("注意:\n");
printf(" 1)本程序由procctl启动,运行周期建议为10秒。\n");
printf(" 2)为了避免被普通用户误杀,本程序应该用root用户启动。\n");
printf(" 3)如果要停止本程序,只能用killall -9 终止。\n\n\n");
return 0;
}
// 忽略全部的信号和IO,不希望程序被干扰。
CloseIOAndSignal(true);
// 打开日志文件。
if (logfile.Open(argv[1],"a+")==false)
{ printf("logfile.Open(%s) failed.\n",argv[1]); return -1; }
int shmid=0;
// 创建/获取共享内存,键值为SHMKEYP,大小为MAXNUMP个st_procinfo结构体的大小。
if ( (shmid = shmget((key_t)SHMKEYP, MAXNUMP*sizeof(struct st_procinfo), 0666|IPC_CREAT)) == -1)
{
logfile.Write("创建/获取共享内存(%x)失败。\n",SHMKEYP); return false;
}
// 将共享内存连接到当前进程的地址空间。
struct st_procinfo *shm=(struct st_procinfo *)shmat(shmid, 0, 0);
// 遍历共享内存中全部的记录。
for (int ii=0;ii<MAXNUMP;ii++)
{
// 如果记录的pid==0,表示空记录,continue;
if (shm[ii].pid==0) continue;
// 如果记录的pid!=0,表示是服务程序的心跳记录。
// 程序稳定运行后,以下两行代码可以注释掉。
//logfile.Write("ii=%d,pid=%d,pname=%s,timeout=%d,atime=%d\n",\
// ii,shm[ii].pid,shm[ii].pname,shm[ii].timeout,shm[ii].atime);
// 向进程发送信号0,判断它是否还存在,如果不存在,从共享内存中删除该记录,continue;
int iret=kill(shm[ii].pid,0);
if (iret==-1)
{
logfile.Write("进程pid=%d(%s)已经不存在。\n",(shm+ii)->pid,(shm+ii)->pname);
memset(shm+ii,0,sizeof(struct st_procinfo)); // 从共享内存中删除该记录。
continue;
}
time_t now=time(0); // 取当前时间。
// 如果进程未超时,continue;
if (now-shm[ii].atime<shm[ii].timeout) continue;
// 如果已超时。
logfile.Write("进程pid=%d(%s)已经超时。\n",(shm+ii)->pid,(shm+ii)->pname);
// 发送信号15,尝试正常终止进程。
kill(shm[ii].pid,15);
// 每隔1秒判断一次进程是否存在,累计5秒,一般来说,5秒的时间足够让进程退出。
for (int jj=0;jj<5;jj++)
{
sleep(1);
iret=kill(shm[ii].pid,0); // 向进程发送信号0,判断它是否还存在。
if (iret==-1) break; // 进程已退出。
}
// 如果进程仍存在,就发送信号9,强制终止它。
if (iret==-1)
logfile.Write("进程pid=%d(%s)已经正常终止。\n",(shm+ii)->pid,(shm+ii)->pname);
else
{
kill(shm[ii].pid,9); // 如果进程仍存在,就发送信号9,强制终止它。
logfile.Write("进程pid=%d(%s)已经强制终止。\n",(shm+ii)->pid,(shm+ii)->pname);
}
// 从共享内存中删除已超时进程的心跳记录。
memset(shm+ii,0,sizeof(struct st_procinfo)); // 从共享内存中删除该记录。
}
// 把共享内存从当前进程中分离。
shmdt(shm);
return 0;
}
运行前,清空共享内存段
使用heartBeat创建三个进程,aaa,bbb,ccc,超时时间设置为50 ,60 ,70s
查看内存,共享内存中目前由三个进程
再利用checkproc遍历共享内存
查看日志,进程状态可见
开发小工具1:压缩文件
#include "_public.h"
// 程序退出和信号2、15的处理函数。
void EXIT(int sig);
int main(int argc,char *argv[])
{
// 程序的帮助。
if (argc != 4)
{
printf("\n");
printf("Using:/project/tools1/bin/gzipfiles pathname matchstr timeout\n\n");
printf("Example:/project/tools1/bin/gzipfiles /log/idc \"*.log.20*\" 0.02\n");
printf(" /project/tools1/bin/gzipfiles /tmp/idc/surfdata \"*.xml,*.json\" 0.01\n");
printf(" /project/tools1/bin/procctl 300 /project/tools1/bin/gzipfiles /log/idc \"*.log.20*\" 0.02\n");
printf(" /project/tools1/bin/procctl 300 /project/tools1/bin/gzipfiles /tmp/idc/surfdata \"*.xml,*.json\" 0.01\n\n");
printf("这是一个工具程序,用于压缩历史的数据文件或日志文件。\n");
printf("本程序把pathname目录及子目录中timeout天之前的匹配matchstr文件全部压缩,timeout可以是小数。\n");
printf("本程序不写日志文件,也不会在控制台输出任何信息。\n");
printf("本程序调用/usr/bin/gzip命令压缩文件。\n\n\n");
return -1;
}
// 关闭全部的信号和输入输出。
// 设置信号,在shell状态下可用 "kill + 进程号" 正常终止些进程。
// 但请不要用 "kill -9 +进程号" 强行终止。
CloseIOAndSignal(true); signal(SIGINT,EXIT); signal(SIGTERM,EXIT);
// 获取文件超时的时间点。
char strTimeOut[21];
LocalTime(strTimeOut,"yyyy-mm-dd hh24:mi:ss",0-(int)(atof(argv[3])*24*60*60));
CDir Dir;
// 打开目录,CDir.OpenDir()
if (Dir.OpenDir(argv[1],argv[2],10000,true)==false)
{
printf("Dir.OpenDir(%s) failed.\n",argv[1]); return -1;
}
char strCmd[1024]; // 存放gzip压缩文件的命令。
// 遍历目录中的文件名。
while (true)
{
// 得到一个文件的信息,CDir.ReadDir()
if (Dir.ReadDir()==false) break;
// 与超时的时间点比较,如果更早,就需要压缩
if ( (strcmp(Dir.m_ModifyTime,strTimeOut)<0) && (MatchStr(Dir.m_FileName,"*.gz")==false) )
{
// 压缩文件,调用操作系统的gzip命令。
SNPRINTF(strCmd,sizeof(strCmd),1000,"/usr/bin/gzip -f %s 1>/dev/null 2>/dev/null",Dir.m_FullFileName);
if (system(strCmd)==0)
printf("gzip %s ok.\n",Dir.m_FullFileName);
else
printf("gzip %s failed.\n",Dir.m_FullFileName);
}
}
return 0;
}
void EXIT(int sig)
{
printf("程序退出,sig=%d\n\n",sig);
exit(0);
}
- 启用自动化调用服务程序
- 再运行gzipfiles实现自动压缩
查看文件 /tmp/idc/surfdata目录,应该只有xml和json数据文件被压缩
开发小工具2:清理历史数据文件模块
#include "_public.h"
void EXIT(int sig);// 程序退出和信号2、15的处理函数。
int main(int argc,char *argv[])
{
if (argc != 4)
{
printf("\n");// 程序的帮助。
printf("Using:/project/tools1/bin/deletefiles pathname matchstr timeout\n\n");
printf("Example:/project/tools1/bin/deletefiles /log/idc \"*.log.20*\" 0.02\n");
printf(" /project/tools1/bin/deletefiles /tmp/idc/surfdata \"*.xml,*.json\" 0.01\n");
printf(" /project/tools1/bin/procctl 300 /project/tools1/bin/deletefiles /log/idc \"*.log.20*\" 0.02\n");
printf(" /project/tools1/bin/procctl 300 /project/tools1/bin/deletefiles /tmp/idc/surfdata \"*.xml,*.json\" 0.01\n\n");
printf("这是一个工具程序,用于压缩历史的数据文件或日志文件。\n");
printf("本程序把pathname目录及子目录中timeout天之前的匹配matchstr文件全部删除,timeout可以是小数。\n");
printf("本程序不写日志文件,也不会在控制台输出任何信息。\n");
return -1;
}
CloseIOAndSignal(true); signal(SIGINT,EXIT); signal(SIGTERM,EXIT); // 关闭全部的信号和输入输出。 设置信号,在shell状态下可用 "kill + 进程号" 正常终止些进程。但请不要用 "kill -9 +进程号" 强行终止。
char strTimeOut[21];// 获取文件超时的时间点。
LocalTime(strTimeOut,"yyyy-mm-dd hh24:mi:ss",0-(int)(atof(argv[3])*24*60*60));
CDir Dir;
if (Dir.OpenDir(argv[1],argv[2],10000,true)==false)// 打开目录,CDir.OpenDir()
{
printf("Dir.OpenDir(%s) failed.\n",argv[1]); return -1;
}
char strCmd[1024]; // 存放gzip压缩文件的命令。
while (true)// 遍历目录中的文件名。
{
if (Dir.ReadDir()==false) break;// 得到一个文件的信息,CDir.ReadDir()
if (strcmp(Dir.m_ModifyTime,strTimeOut)<0)
{
if (REMOVE(Dir.m_FullFileName)==true)
printf("REMOVE %s ok.\n",Dir.m_FullFileName);
else
printf("REMOVE %s failed.\n",Dir.m_FullFileName);
}
}
return 0;
}
void EXIT(int sig)
{
printf("程序退出,sig=%d\n\n",sig);
exit(0);
}
服务程序运行策略
封装启动脚本start.sh
###########################################################################################
#启动数据中心后台服务程序的脚本
###########################################################################################
#检查服务程序是否超时,配置在/etc/rc.local中由root用户执行
/project/tools1/bin/procct1 30 /project/tools1/bin/checkproc
#压缩数据中心后台服务程序的备份日志 压缩20年之后的,0.04天前的日志信息
/project/tools1/bin/procct1 300 /project/tools1/bin/gzipfiles /log/idc "*.log.20*" 0.04
#生成用于测试的全国气象站点观测的分钟数据
/project/tools1/bin/procct1 60 /project/idc/bin/crtsurfdata /project/idc/ini/stcode.ini /tmp/idc/surfdata /log/idc/crtsurfdata.log xml,json,csv
#清理原始的全国气象站点观测的分钟数据目录 /tmp/idc/surfdata中的历史数据文件
/project/tools1/bin/procct1 300 /project/tools1/bin/deletefiles /tmp/idc/surfdata "*" 0.04
结束脚本killall.sh
#先发送9的信号让procct1调度程序强制终止
killall -9 procct1
#再让服务程序退出
killall gzipfiles crtsurfdata deletefiles
sleep 3
#保证服务程序强制退出
killall -9 gzipfiles crtsurfdata deletefiles
配置/etc/rc.local 文件,实现开机自启
#!/bin/bash
# THIS FILE IS ADDED FOR COMPATIBILITY PURPOSES
#
# It is highly advisable to create own systemd services or udev rules
# to run scripts during boot instead of using this file.
#
# In contrast to previous versions due to parallel execution during boot
# this script will NOT be run after all other services.
#
# Please note that you must run 'chmod +x /etc/rc.d/rc.local' to ensure
# that this script will be executed during boot.
touch /var/lock/subsys/local
# 检查服务程序是否超时
/project/tools/bin/procct1 30 /project/tools/bin/checkproc
#启动数据中心的后台服务程序
su - kiana100 -c "/bin/sh /project/idc1/c/start.sh"
技术小结
- 生成测试数据
- 调度程序
- 守护程序
- 文件的压缩和清理
- 信号、多进程、共享信号量
工作态度
- 把课程内容当成工作
- 养成严谨的西瓜
- 稳定、高效、简洁