第二章:保证后台程序的运行
本章makefile文件,即编译指令:
#开发框架头文件路径
PUBINCL = -I/project/public
#开发框架cpp名字,直接包含进来,没有采用链接库,是为了方便调试
PUBCPP = /project/public/_public.cpp
#编译参数
CFLAGS = -g
all:procctl book checkproc gzipfiles deletefiles crtsurfdata
procctl:procctl.cpp
g++ $(CFLAGS) -o procctl procctl.cpp -lm -lc
cp procctl ../bin/.
bookyuan:bookyuan.cpp
g++ $(CFLAGS) -o bookyuan bookyuan.cpp $(PUBCPP) $(PUBINCL) -lm -lc
cp bookyuan ../bin/.
book:
g++ $(CFLAGS) -o book book.cpp $(PUBCPP) $(PUBINCL) -lm -lc
cp book ../bin/.
checkproc:
g++ $(CFLAGS) -o checkproc checkproc.cpp $(PUBCPP) $(PUBINCL) -lm -lc
cp checkproc ../bin/.
gzipfiles:
g++ $(CFLAGS) -o gzipfiles gzipfiles.cpp $(PUBCPP) $(PUBINCL) -lm -lc
cp gzipfiles ../bin/.
deletefiles:
g++ $(CFLAGS) -o deletefiles deletefiles.cpp $(PUBCPP) $(PUBINCL) -lm -lc
cp deletefiles ../bin/.
crtsurfdata:crtsurfdata.cpp
g++ $(CFLAGS) -o crtsurfdata crtsurfdata.cpp $(PUBINCL) $(PUBCPP) -lm -lc
cp crtsurfdata ../bin/.
clean:
rm -f procctl book checkproc bookyuan gzipfiles deletefiles crtsurfdata
2.1 生成测试数据文件
在使用stm32开发板获取数据之前,我们数据从哪来呢?全国有多个气象站点,每个气象站点都会产生气象观测数据。关于气象站点的相关数据,例如:省 站号 站名 纬度 经度 海拔高度。这些数据已经在文件/project/idc1/ini/stcode.ini中给出。气象数据我们使用随机数的方式模拟生成。假设多个站点每隔一分钟采集一次数据。我们记录下每一分钟的不同站点的所有采集数据。同时生成csv,xml,json三种格式的文件。那么整个程序的流程就很清楚了。
1.进行打开日志,打开站点参数信息的文件等准备工作。
2.把站点参数文件中加载到容器中
3.模拟生成全国气象站点分钟观测数据,存放在另一个容器中。
4.把第二个容器中的全国气象站点分钟观测数据写入文件。
下面我给出一些核心代码: 首先是最重要的结构体和容器的定义。关于头文件。这是开发框架中给出的一个头文件。里面封装好了很多常用的头文件,函数以及一些与项目开发相关的操作函数等。在项目学习的初期,不需要过多的纠结这些函数的实现。先学会怎么用。用熟练了,有能力了再去捉摸如何实现这些函数,再去写出自己的开发框架。此处CLogFile logfile;语句。就是开发框架中定义好的一个类,用来进行日志的相关操作。该文件为/project/idc1/c/crtsurfdata.cpp
#include "_public.h"
// 全国气象站点参数结构体。
struct st_stcode
{
char provname[31]; // 省
char obtid[11]; // 站号
char obtname[31]; // 站名
double lat; // 纬度
double lon; // 经度
double height; // 海拔高度
};
vector<struct st_stcode> vstcode; // 存放全国气象站点参数的容器。
// 全国气象站点分钟观测数据结构体
struct st_surfdata
{
char obtid[11]; // 站点代码。
char ddatetime[21]; // 数据时间:格式yyyymmddhh24miss
int t; // 气温:单位,0.1摄氏度。
int p; // 气压:0.1百帕。
int u; // 相对湿度,0-100之间的值。
int wd; // 风向,0-360之间的值。
int wf; // 风速:单位0.1m/s
int r; // 降雨量:0.1mm。
int vis; // 能见度:0.1米。
};
vector<struct st_surfdata> vsurfdata; // 存放全国气象站点分钟观测数据的容器
char strddatetime[21]; // 观测数据的时间。记录数据时间很重要
CLogFile logfile; // 日志类。
我们来看函数的主体流程,流程非常简单。不做过多赘述。
int main(int argc,char *argv[])
{
if (argc!=5)
{
// 如果参数非法,给出帮助文档。
printf("Using:./crtsurfdata5 inifile outpath logfile datafmt\n");
printf("Example:./project/idc1/bin/crtsurfdata5 /project/idc1/ini/stcode.ini /tmp/idc/surfdata /log/idc/crtsurfdata5.log xml,json,csv\n\n");
printf("inifile 全国气象站点参数文件名。\n");
printf("outpath 全国气象站点数据文件存放的目录。\n");
printf("logfile 本程序运行的日志文件名。\n");
printf("datafmt 生成数据文件的格式,支持xml、json和csv三种格式,中间用逗号分隔。\n\n");
return -1;
}
// 打开程序的日志文件。
if (logfile.Open(argv[3],"a+",false)==false)
{
printf("logfile.Open(%s) failed.\n",argv[3]); return -1;
}
logfile.Write("crtsurfdata5 开始运行。\n");
// 把站点参数文件中加载到vstcode容器中。
if (LoadSTCode(argv[1])==false) return -1;
// 模拟生成全国气象站点分钟观测数据,存放在vsurfdata容器中。
CrtSurfData();
// 把容器vsurfdata中的全国气象站点分钟观测数据写入文件。
if (strstr(argv[4],"xml")!=0) CrtSurfFile(argv[2],"xml");
if (strstr(argv[4],"json")!=0) CrtSurfFile(argv[2],"json");
if (strstr(argv[4],"csv")!=0) CrtSurfFile(argv[2],"csv");
logfile.WriteEx("crtsurfdata5 运行结束。\n");
return 0;
}
下面会解释一下所使用的日志函数。
// 打开日志文件。
// filename:日志文件名,建议采用绝对路径,如果文件名中的目录不存在,就先创建目录。
// openmode:日志文件的打开方式,与fopen库函数打开文件的方式相同,缺省值是"a+"。
// bBackup:是否自动切换,true-切换,false-不切换,在多进程的服务程序中,如果多个进程共用一个日志文件,bBackup必须为false。
// bEnBuffer:是否启用文件缓冲机制,true-启用,false-不启用,如果启用缓冲区,那么写进日志文件中的内容不会立即写入文件,缺省是不启用。
bool Open(const char* filename, const char* openmode = 0, bool bBackup = true, bool bEnBuffer = false);
// 把内容写入日志文件,fmt是可变参数,使用方法与printf库函数相同。
// Write方法会写入当前的时间,WriteEx方法不写时间。
bool Write(const char* fmt, ...);
bool WriteEx(const char* fmt, ...);
接下来是把站点参数文件中加载到vstcode容器中的LoadSTCode()函数。我们先看一下文件的数据格式:
省 站号 站名 纬度 经度 海拔高度
安徽,58015,砀山,34.27,116.2,44.2
安徽,58102,亳州,33.47,115.44,39.1
安徽,58118,蒙城,33.16,116.31,10.3
安徽,58122,宿州,33.38,116.59,25.9
安徽,58203,阜阳,32.52,115.44,32.7
...
我们需要一行一行地读取这个文件,接着以" , "为分隔符,那么每一行就会获取到6个字段,将这些字段放入结构体中,最后再放入容器中去。strBuffer在每次使用之前都要进行初始化,不然会溢出。这里在Fgets函数中已经进行了初始化。所以没有写。
// 把站点参数文件中加载到vstcode容器中。
bool LoadSTCode(const char *inifile)
{
CFile File;
// 打开站点参数文件。
if (File.Open(inifile,"r")==false)
{
logfile.Write("File.Open(%s) failed.\n",inifile); return false;
}
char strBuffer[301]; // 用来存放读取的字符串
CCmdStr CmdStr;
struct st_stcode stcode;
while (true)
{
// 从站点参数文件中读取一行,如果已读取完,跳出循环。
if (File.Fgets(strBuffer,300,true)==false) break;
// 把读取到的一行拆分。
CmdStr.SplitToCmd(strBuffer,",",true);
if (CmdStr.CmdCount()!=6) continue; // 扔掉无效的行。
// 把站点参数的每个数据项保存到站点参数结构体中。
memset(&stcode,0,sizeof(struct st_stcode));
CmdStr.GetValue(0, stcode.provname,30); // 省
CmdStr.GetValue(1, stcode.obtid,10); // 站号
CmdStr.GetValue(2, stcode.obtname,30); // 站名
CmdStr.GetValue(3,&stcode.lat); // 纬度
CmdStr.GetValue(4,&stcode.lon); // 经度
CmdStr.GetValue(5,&stcode.height); // 海拔高度
// 把站点参数结构体放入站点参数容器。
vstcode.push_back(stcode);
}
/*
for (int ii=0;ii<vstcode.size();ii++)
logfile.Write("provname=%s,obtid=%s,obtname=%s,lat=%.2f,lon=%.2f,height=%.2f\n",\
vstcode[ii].provname,vstcode[ii].obtid,vstcode[ii].obtname,vstcode[ii].lat,\
vstcode[ii].lon,vstcode[ii].height);
*/
return true;
}
// 下面是使用的框架函数
int CmdCount();
// 获取拆分后字段的个数,即m_vCmdStr容器的大小。
void SplitToCmd(const string& buffer, const char* sepstr, const bool bdelspace = false);
// 把字符串拆分到m_vCmdStr容器中。
// buffer:待拆分的字符串。
// sepstr:buffer中采用的分隔符,注意,sepstr参数的数据类型不是字符,是字符串,如","、" "、"|"、"~!~"。
// bdelspace:拆分后是否删除字段内容前后的空格,true-删除;false-不删除,缺省不删除。
bool GetValue(const int inum, char* value, const int ilen = 0); // 字符串,ilen缺省值为0。
bool GetValue(const int inum, int* value); // int整数。
bool GetValue(const int inum, unsigned int* value); // unsigned int整数。
bool GetValue(const int inum, long* value); // long整数。
bool GetValue(const int inum, unsigned long* value); // unsigned long整数。
bool GetValue(const int inum, double* value); // 双精度double。
bool GetValue(const int inum, bool* value); // bool型。
// 从m_vCmdStr容器获取字段内容。
// inum:字段的顺序号,类似数组的下标,从0开始。
// value:传入变量的地址,用于存放字段内容。
// 返回值:true-成功;如果inum的取值超出了m_vCmdStr容器的大小,返回失败。
再就是生成随机数的函数。这个函数主要说一下时间操作。UNIX操作系统根据计算机产生的年代和应用采用1970年1月1日作为UNIX的纪元时间,1970年1月1日0点作为计算机表示时间的是中间点,将从1970年1月1日开始经过的秒数用一个整数存放,这种高效简洁的时间表示方法被称为“Unix时间纪元”,向左和向右偏移都可以得到更早或者更后的时间。
// 模拟生成全国气象站点分钟观测数据,存放在vsurfdata容器中。
void CrtSurfData()
{
// 播随机数种子。
srand(time(0));
// 获取当前时间,当作观测时间。
memset(strddatetime,0,sizeof(strddatetime));
LocalTime(strddatetime,"yyyymmddhh24miss");
struct st_surfdata stsurfdata;
// 遍历气象站点参数的vstcode容器。
for (int ii=0;ii<vstcode.size();ii++)
{
memset(&stsurfdata,0,sizeof(struct st_surfdata));
// 用随机数填充分钟观测数据的结构体。
strncpy(stsurfdata.obtid,vstcode[ii].obtid,10); // 站点代码。
strncpy(stsurfdata.ddatetime,strddatetime,14); // 数据时间:格式yyyymmddhh24miss
stsurfdata.t=rand()%351; // 气温:单位,0.1摄氏度
stsurfdata.p=rand()%265+10000; // 气压:0.1百帕
stsurfdata.u=rand()%100+1; // 相对湿度,0-100之间的值。
stsurfdata.wd=rand()%360; // 风向,0-360之间的值。
stsurfdata.wf=rand()%150; // 风速:单位0.1m/s
stsurfdata.r=rand()%16; // 降雨量:0.1mm
stsurfdata.vis=rand()%5001+100000; // 能见度:0.1米
// 把观测数据的结构体放入vsurfdata容器。
vsurfdata.push_back(stsurfdata);
}
}
void LocalTime(char* stime, const char* fmt = 0, const int timetvl = 0);
/*
取操作系统的时间。
stime:用于存放获取到的时间字符串。
timetvl:时间的偏移量,单位:秒,0是缺省值,表示当前时间,30表示当前时间30秒之后的时间点,-30表示当前时间30秒之前的时间点。
fmt:输出时间的格式,fmt每部分的含义:yyyy-年份;mm-月份;dd-日期;hh24-小时;mi-分钟;ss-秒,
缺省是"yyyy-mm-dd hh24:mi:ss",目前支持以下格式:
"yyyy-mm-dd hh24:mi:ss"
"yyyymmddhh24miss"
"yyyy-mm-dd"
"yyyymmdd"
"hh24:mi:ss"
"hh24miss"
"hh24:mi"
"hh24mi"
"hh24"
"mi"
注意:
1)小时的表示方法是hh24,不是hh,这么做的目的是为了保持与数据库的时间表示方法一致;
2)以上列出了常用的时间格式,如果不能满足你应用开发的需求,请修改源代码timetostr函数增加更多的格式支持;
3)调用函数的时候,如果fmt与上述格式都匹配,stime的内容将为空。
4)时间的年份是四位,其它的可能是一位和两位,如果不足两位,在前面补0。
*/
最后就是把容器vsurfdata中的全国气象站点分钟观测数据写入文件的函数。为了避免文件在写入的过程中被读取,开发框架的OpenForRename和CloseAndRename函数解决了这个问题。原理是创建临时文件,往临时文件中写入数据,关闭临时文件,将临时文件改名为正式文件。
2.1.1CSV,XML,JSON文件格式
CSV(Comma-Separated Values)是一种简单的文本文件格式,用于存储表格数据。每行代表一条记录,每个字段之间使用逗号进行分隔。通常,第一行包含字段名,后续行包含相应字段的值。
Name, Age, City
John Doe, 25, New York
Jane Smith, 30, London
XML(eXtensible Markup Language)是一种标记语言,用于存储和传输结构化数据。它使用自定义标签来描述数据的各个部分,并支持嵌套和层次结构。
<users>
<user>
<name>John Doe</name>
<age>25</age>
<city>New York</city>
</user>
<user>
<name>Jane Smith</name>
<age>30</age>
<city>London</city>
</user>
</users>
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,常用于前后端数据传输。它使用键值对的形式表示数据,并支持嵌套和层次结构。
{
"users": [
{
"name": "John Doe",
"age": 25,
"city": "New York"
},
{
"name": "Jane Smith",
"age": 30,
"city": "London"
}
]
}
了解三种文件格式以后,只要按格式向文件中写入数据就可以了。这是一个需要耐心的操作。至此生成测试数据的程序就基本完成了。
// 把容器vsurfdata中的全国气象站点分钟观测数据写入文件。
bool CrtSurfFile(const char *outpath,const char *datafmt)
{
CFile File;
// 拼接生成数据的文件名,例如:/tmp/idc/surfdata/SURF_ZH_20210629092200_2254.csv
char strFileName[301];
sprintf(strFileName,"%s/SURF_ZH_%s_%d.%s",outpath,strddatetime,getpid(),datafmt);
// 打开文件。
if (File.OpenForRename(strFileName,"w")==false)
{
logfile.Write("File.OpenForRename(%s) failed.\n",strFileName); return false;
}
if (strcmp(datafmt,"csv")==0) File.Fprintf("站点代码,数据时间,气温,气压,相对湿度,风向,风速,降雨量,能见度\n");
if (strcmp(datafmt,"xml")==0) File.Fprintf("<data>\n");
if (strcmp(datafmt,"json")==0) File.Fprintf("{\"data\":[\n");
// 遍历存放观测数据的vsurfdata容器。
for (int ii=0;ii<vsurfdata.size();ii++)
{
// 写入一条记录。
if (strcmp(datafmt,"csv")==0)
File.Fprintf("%s,%s,%.1f,%.1f,%d,%d,%.1f,%.1f,%.1f\n",\
vsurfdata[ii].obtid,vsurfdata[ii].ddatetime,vsurfdata[ii].t/10.0,vsurfdata[ii].p/10.0,\
vsurfdata[ii].u,vsurfdata[ii].wd,vsurfdata[ii].wf/10.0,vsurfdata[ii].r/10.0,vsurfdata[ii].vis/10.0);
if (strcmp(datafmt,"xml")==0)
File.Fprintf("<obtid>%s</obtid><ddatetime>%s</ddatetime><t>%.1f</t><p>%.1f</p>"\
"<u>%d</u><wd>%d</wd><wf>%.1f</wf><r>%.1f</r><vis>%.1f</vis><endl/>\n",\
vsurfdata[ii].obtid,vsurfdata[ii].ddatetime,vsurfdata[ii].t/10.0,vsurfdata[ii].p/10.0,\
vsurfdata[ii].u,vsurfdata[ii].wd,vsurfdata[ii].wf/10.0,vsurfdata[ii].r/10.0,vsurfdata[ii].vis/10.0);
if (strcmp(datafmt,"json")==0)
{
File.Fprintf("{\"obtid\":\"%s\",\"ddatetime\":\"%s\",\"t\":\"%.1f\",\"p\":\"%.1f\","\
"\"u\":\"%d\",\"wd\":\"%d\",\"wf\":\"%.1f\",\"r\":\"%.1f\",\"vis\":\"%.1f\"}",\
vsurfdata[ii].obtid,vsurfdata[ii].ddatetime,vsurfdata[ii].t/10.0,vsurfdata[ii].p/10.0,\
vsurfdata[ii].u,vsurfdata[ii].wd,vsurfdata[ii].wf/10.0,vsurfdata[ii].r/10.0,vsurfdata[ii].vis/10.0);
if (ii<vsurfdata.size()-1) File.Fprintf(",\n");
else File.Fprintf("\n");
}
}
if (strcmp(datafmt,"xml")==0) File.Fprintf("</data>\n");
if (strcmp(datafmt,"json")==0) File.Fprintf("]}\n");
// 关闭文件。
File.CloseAndRename();
logfile.Write("生成数据文件%s成功,数据时间%s,记录数%d。\n",strFileName,strddatetime,vsurfdata.size());
return true;
}
2.2Linux信号
信号(signal)是软件中断,是进程之间相互传递消息的一种方法,用于通知进程发生了事件,但是,不能给进程传递任何数据。信号产生的原因有很多,在Linux下,可以用kill和killall命令发送信号。一共有64种标准信号。一些常用的信号如下:
SIGNULL (0):没有实际的信号处理函数,而是用于检查特定进程是否存在。
SIGINT (2): 终止进程的中断信号,通常由终端产生,如按下Ctrl+C。
SIGKILL (9): 强制终止进程的信号,无法被捕获或忽略。
SIGTERM (15): 终止进程的信号,通常用于正常终止进程。
SIGSTOP (17): 暂停进程的信号,使进程进入暂停状态,可以通过SIGCONT信号恢复。
SIGCONT (18): 恢复进程的信号,用于从暂停状态恢复进程的执行。
SIGUSR1 (10): 用户定义的信号1,可用于进程间自定义通信。
SIGUSR2 (12): 用户定义的信号2,可用于进程间自定义通信。
SIGSEGV (11): 段错误信号,表示进程访问了无效的内存地址。
SIGPIPE (13): 管道破裂信号,用于向读取端已关闭的管道写入数据时产生。
SIGCHLD (20): 子进程状态改变信号,父进程接收到该信号后可以调用wait()或waitpid()获取子进程的退出状态。
2.2.1进程对信号的处理方法
1)对该信号的处理采用系统的默认操作,大部分的信号的默认操作是终止进程。
2)设置中断的处理函数,收到信号后,由该函数来处理。
3)忽略某个信号,对该信号不做任何处理,就像未发生过一样。
signal函数可以设置程序对信号的处理方式。函数声明:
sighandler_t signal(int signum, sighandler_t handler);
参数signum表示信号的编号。
参数handler表示信号的处理方式,有三种情况:
1)SIG_DFL:恢复参数signum所指信号的处理方法为默认值。
2)一个自定义的处理信号的函数,信号的编号为这个自定义函数的参数。
3)SIG_IGN:忽略参数signum所指的信号。
2.2.2信号有什么用?
服务程序运行在后台,如果想让中止它,杀掉不是个好办法,因为程序被杀的时候,程序突然死亡,没有安排善后工作。
如果向服务程序发送一个信号,服务程序收到这个信号后,调用一个函数,在函数中编写善后的代码,程序就可以有计划的退出。
向服务程序发送0的信号,可以检测程序是否存活。
2.2.3信号的使用
在实际开发中,在main函数开始的位置,程序员会先屏蔽掉全部的信号。
for (int ii=1;ii<=64;ii++) signal(ii,SIG_IGN);
这么做的目的是不希望程序被干扰。然后,再设置程序员关心的信号的处理函数。
程序在运行的进程中,如果按Ctrl+c,将向程序发出SIGINT信号,编号是2。
采用“kill 进程编号”或“killall 程序名”向程序发出的是SIGTERM信号,编号是15。
采用“kill -9 进程编号”向程序发出的是SIGKILL信号,编号是9,此信号不能被忽略,也无法捕获,程序将突然死亡。
设置SIGINT和SIGTERM两个信号的处理函数,这两个信号可以使用同一个处理函数,函数的代码是释放资源。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
void EXIT(int sig)
{
printf("收到了信号%d,程序退出。\n",sig);
// 在这里添加释放资源的代码
exit(0); // 程序退出。
}
int main()
{
for (int ii=1;ii<=64;ii++) signal(ii,SIG_IGN); // 屏蔽全部的信号
signal(SIGINT,EXIT); signal(SIGTERM,EXIT); // 设置SIGINT和SIGTERM的处理函数
while (true) // 每隔一秒执行一次任务。
{
printf("执行了一次任务。\n");
sleep(1);
}
}
运行如下:
[sxixia@localhost c_project]$ g++ -g -o signal signal.cpp
[sxixia@localhost c_project]$ ./signal
执行了一次任务。
执行了一次任务。
执行了一次任务。
执行了一次任务。
执行了一次任务。
^C收到了信号2,程序退出。
[sxixia@localhost c_project]$ ./signal
[sxixia@localhost c_project]$ killall -15 signal
执行了一次任务。
执行了一次任务。
执行了一次任务。
执行了一次任务。
收到了信号15,程序退出。
2.2.4发送信号
Linux操作系统提供了kill和killall命令向程序发送信号,C语言也提供了kill库函数,用于在程序中向其它进程或者线程发送信号。函数声明:
int kill(pid_t pid, int sig);
kill函数将参数sig指定的信号给参数pid 指定的进程。
参数pid 有几种情况:
1)pid>0 将信号传给进程号为pid 的进程。
2)pid=0 将信号传给和目前进程相同进程组的所有进程,常用于父进程给子进程发送信号,注意,发送信号者进程也会收到自己发出的信号。
3)pid=-1 将信号广播传送给系统内所有的进程,例如系统关机时,会向所有的登录窗口广播关机信息。
sig:准备发送的信号代码,假如其值为零则没有任何信号送出,但是系统会执行错误检查,通常会利用sig值为零来检验某个进程是否仍在运行。
返回值说明: 成功执行时,返回0;失败返回-1,errno被设为以下的某个值。
EINVAL:指定的信号码无效(参数 sig 不合法)。
EPERM:权限不够无法传送信号给指定进程。
ESRCH:参数 pid 所指定的进程或进程组。
关于信号的简单介绍就先到这,接下来是关于多进程的概念:
2.3Linux多进程
在Linux中,多进程是指一个程序在执行过程中创建了多个独立运行的进程。每个进程都有自己的程序计数器、寄存器集合、堆栈和文件描述符等资源,它们在内存中独立运行,互相之间不会干扰。
多进程的概念使得一个程序可以同时执行多个任务或并行处理多个操作。每个进程可以执行不同的代码段,拥有自己的数据和状态。进程之间通过进程间通信(Inter-Process Communication,IPC)机制进行数据交换和协调操作。
在Linux中,通过调用系统调用fork()
可以创建一个新的进程,新进程是原进程的复制,它们在不同的地址空间中运行。子进程可以继续执行原进程的代码,或者通过调用exec()
系列函数加载新的程序代码。父进程和子进程是并行运行的,它们具有不同的进程ID(PID),可以通过PID来区分和管理不同的进程。
多进程可以实现并发执行和任务分配,提高系统的吞吐量和响应能力。在Linux中,多进程常用于实现并发服务器、并行计算、后台服务等场景。然而,多进程也会带来一些开销,如进程间切换的开销和资源消耗,因此在设计和实现多进程应用时需要考虑资源管理、同步和通信等问题。
我们来看linux的0号,1号和2号进程:
0号进程:idle进程,系统创建的第一个进程,用于加载系统。
1号进程:systemd进程,系统初始化,是所有其他用户进程的祖先。又叫init进程。
2号进程:kthreadd进程:负责所有内核线程的调度和管理。
0号进程会演变成1号进程和2号进程。其他的进程如果父进程不是1号进程或者2号进程。一直追根溯源的话,总能找到父进程为1的进程。
2.3.1查看进程
使用命令ps -ef 可以查看所有的进程。附加grep指令可以查找指定的进程。例如:
[sxixia@localhost ~]$ ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 15:26 ? 00:00:02 /usr/lib/systemd/systemd --swi
root 2 0 0 15:26 ? 00:00:00 [kthreadd]
root 4 2 0 15:26 ? 00:00:00 [kworker/0:0H]
每一列的含义分别是启动该进程的用户,进程PID,该进程的父进程PID,cpu占用率,开始时间,启动进程的终端设备,进程运行总时间,启动该进程的命令,包括参数。
2.3.2进程标识
使用函数getpid()和getppid()可以获得进程的pid以及父进程pid。如果不清楚使用这些函数需要什么头文件的话,可以使用man命令来获得,例如:
[sxixia@localhost c_project]$ man getpid
SYNOPSIS
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("getpid()= %d\n",getpid());
printf("getppid()= %d\n",getppid());
sleep(100);
}
[sxixia@localhost c_project]$ g++ -g -o signal signal.cpp
[sxixia@localhost c_project]$ ./signal
getpid()= 4375
getppid()= 4081
[sxixia@localhost c_project]$ ps -ef | grep 4375
sxixia 4375 4081 0 16:04 pts/0 00:00:00 ./signal
sxixia 4437 4386 0 16:04 pts/1 00:00:00 grep --color=auto 4375
[sxixia@localhost c_project]$ ps -ef | grep 4081
sxixia 4081 4072 0 15:57 pts/0 00:00:00 bash
sxixia 4375 4081 0 16:04 pts/0 00:00:00 ./signal
sxixia 4445 4386 0 16:04 pts/1 00:00:00 grep --color=auto 4081
[sxixia@localhost c_project]$ ps -ef | grep 4072
sxixia 4072 1 0 15:57 ? 00:00:01 /usr/libexec/gnome-terminal-server
sxixia 4080 4072 0 15:57 ? 00:00:00 gnome-pty-helper
sxixia 4081 4072 0 15:57 pts/0 00:00:00 bash
sxixia 4386 4072 0 16:04 pts/1 00:00:00 bash
sxixia 4453 4386 0 16:04 pts/1 00:00:00 grep --color=auto 4072
2.3.3创建进程
在一个程序中,使用fork()函数来创建一个新的进程。fork()会返回两次,父进程会返回子进程的PID,子进程会返回0。可以用来区分进程。创建新的进程以后,父进程和子进程都会继续执行fork()后面的代码。子进程是父进程的副本。子进程获得了父进程的数据空间,堆,栈的副本。什么是副本呢?来看下面的演示:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
int a=1;
printf("这是父进程getpid()= %d\n",getpid());
if(fork()==0)
{
printf("这是子进程执行的,getppid()= %d\n",getppid());
a = 2;
printf("子进程的a= %d\n",a);
}
else
{
printf("这是父进程执行的,getpid= %d\n",getpid());
printf("父进程的a= %d\n",a);
sleep(3);
}
printf("a= %d,pid= %d\n",a,getpid());
sleep(50);
}
[sxixia@localhost c_project]$ g++ -g -o signal signal.cpp
[sxixia@localhost c_project]$ ./signal
这是父进程getpid()= 5672
这是父进程执行的,getpid= 5672
父进程的a= 1
这是子进程执行的,getppid()= 5672
子进程的a= 2
a= 2,pid= 5673
a= 1,pid= 5672
分析结果,创建新的进程后,父进程的pid为5672,子进程的pid为5673,在子进程中对a重新赋值为2,最后两个进程重新对a进行一次输出。发现在父进程中a的值依旧为1。这说明子进程将父进程的数据空间进行了复制,而不是共享。同时,在父进程中打开的文件描述符也会被复制到子进程中去。
当子进程被创建时,它会继承父进程的文件描述符表。这意味着子进程会拥有与父进程相同的文件描述符,这些文件描述符指向相同的打开文件。这样,父子进程可以独立地对文件进行读写操作,而不会相互干扰。
这种文件描述符的复制是为了方便子进程与父进程之间的通信和共享文件资源。子进程可以通过继承的文件描述符访问父进程打开的文件,也可以使用相同的文件描述符打开其他文件。
需要注意的是,当父进程或子进程关闭文件描述符时,并不会对另一个进程的文件描述符产生影响。每个进程都有自己的文件描述符表,因此关闭一个文件描述符只会影响关闭该文件描述符的进程自身。
接下来我们来看两个特殊的进程:
2.3.4僵尸进程和孤儿进程
僵尸进程(Zombie Process)是指一个子进程已经终止(执行完毕或收到终止信号),但其父进程尚未调用wait()
或waitpid()
系统调用来获取子进程的退出状态,因此子进程的进程表项仍然保留在系统进程表中,这种状态下的子进程被称为僵尸进程。僵尸进程不会占用系统资源,但会占用一定的进程表项。内核为每个子进程保留了一个数据结构,包括进程编号,终止状态,使用CPU时间等。父进程如果处理了子进程退出的信息,内核就会释放这个数据结构,如果父进程没有处理,这个数据结构就不会被释放。子进程的编号就会一直被占用。而系统可用的进程号有限,如果有大量的僵尸进程,会因为没有可用的进程号而不能产生新的进程。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
if(fork()==0)
{
printf("这是子进程执行的,getpid()= %d\n",getpid());
sleep(3);
}
else
{
printf("这是父进程执行的,getpid= %d\n",getpid());
sleep(10);
}
}
此程序让子进程休眠3秒,父进程休眠10秒,让子进程先于父进程退出。使用命令查看进程。
[sxixia@localhost c_project]$ ps -ef | grep signal
sxixia 3812 1 0 15:50 ? 00:00:00 /home/sxixia/c_project/signal
sxixia 7683 4081 0 18:40 pts/0 00:00:00 ./signal
sxixia 7684 7683 0 18:40 pts/0 00:00:00 [signal] <defunct>
sxixia 7686 7612 0 18:40 pts/1 00:00:00 grep --color=auto signal
[sxixia@localhost c_project]$ ps -ef | grep signal
sxixia 3812 1 0 15:50 ? 00:00:00 /home/sxixia/c_project/signal
sxixia 7700 7612 0 18:40 pts/1 00:00:00 grep --color=auto signal
可以看到出现了一列特殊的标识:这代表的就是僵尸进程。
sxixia 7684 7683 0 18:40 pts/0 00:00:00 [signal] <defunct>
⼀般情况下,⼦进程是由⽗进程创建,⽽⼦进程和⽗进程的退出是⽆顺序的,两者之间都不知道谁先退出。正常情况下⽗进程先结束会调⽤ wait 或者 waitpid 函数等待⼦进程完成再退出,⽽⼀旦⽗进程不等待直接退出,则 剩下的⼦进程会被init(pid=1)进程接收,成会孤⼉进程。(进程树中除了init都会有⽗进程)init进程成为孤儿进程的新的父进程,并负责回收子进程的资源。
将上述程序中的休眠时间对调,使用ps命令查看进程:可以看到父进程的PID变成1了。
[sxixia@localhost c_project]$ ps -ef | grep signal
sxixia 3812 1 0 15:50 ? 00:00:00 /home/sxixia/c_project/signal
sxixia 8093 1 0 19:01 pts/0 00:00:00 ./signal
sxixia 8117 7612 0 19:01 pts/1 00:00:00 grep --color=auto signal
孤儿进程和僵尸进程的产生通常是由于父子进程间的处理不当引起的。当子进程先于父进程退出时,父进程没有及时回收子进程的资源,就会导致僵尸进程的产生。而当父进程先于子进程退出时,子进程成为孤儿进程。
为了避免这种情况,通常可以采取以下措施:
1)父进程调用wait函数等待子进程的退出。但是调用wait函数会阻塞父进程的运行。
2)父进程在创建子进程前调用signal(SIGCHLD,SIG_IGN);函数,这会忽略内核发出的SIGCHLD信号,这样子进程退出时,操作系统会自动回收子进程的资源,不会产生僵尸进程。
3)使用SIGCHLD
信号处理:父进程可以注册SIGCHLD
信号的处理函数,当子进程退出时,会发送SIGCHLD
信号给父进程。在信号处理函数中,可以调用wait()
或waitpid()
系统调用来回收子进程的资源。当子进程退出时,相当于一个软中断,父进程会进入signal指定的信号处理函数中完成处理,再回到中断处。
2.4服务程序的调度
服务程序的调度可以归于两种:一是需要周期性启动的程序。二是常驻于内存的服务程序,如果发生了异常,需要尽快重启。我们已经完成了生成测试数据文件的程序。这个程序不可能每次都要我手动去输入参数启动。那么就需要我们编写一个调度程序来保证程序的周期性运行或者异常时重启。那么怎么在一个程序中调用另一个程序呢?在Linux中提供了exec()函数来进行操作。比较常用的就两个:
execl:用于执行一个可执行文件,它需要指定可执行文件的路径和命令行参数。函数原型为:
int execl(const char *path, const char *arg, ...);
execv:与execl
类似,用于执行一个可执行文件,但它接受一个参数数组来指定命令行参数。函数原型为:
int execv(const char *path, char *const argv[]);
这些exec
函数都是用来执行新的程序,一旦执行成功,当前进程的代码、数据和堆栈都会被新程序替换,新程序开始执行。它们会继承当前进程的文件描述符、信号处理方式等属性。
对于程序运行参数数量不定的情况下,使用execv函数更加方便。下面我将编写使用调度程序来周期性生成测试数据。先看该程序需要的帮助文档:这很容易理解,我们需要传入启动周期,以及被调度程序所需要的参数。所以调度程序运行需要的参数数量是不确定的。这个例子就是每隔60秒启动一次/project/idc1/bin/crtsurfdata5程序,同时启动被调度函数的参数是:/project/idc1/ini/stcode.ini /tmp/idc/surfdata /log/idc/crtsurfdata5.log xml,json,csv
if (argc<3)
{
printf("Using:./procctl timetvl program argv ...\n");
printf("Example: 60 /project/idc1/bin/crtsurfdata5 /project/idc1/ini/stcode.ini /tmp/idc/surfdata /log/idc/crtsurfdata5.log xml,json,csv\n\n");
printf("本程序是服务程序的调度程序,周期性启动服务程序或shell脚本。\n");
printf("timetvl 运行周期,单位:秒。被调度的程序运行结束后,在timetvl秒后会被procctl重新启动。\n");
printf("program 被调度的程序名,必须使用全路径。\n");
printf("argvs 被调度的程序的参数。\n");
printf("注意,本程序不会被kill杀死,但可以用kill -9强行杀死。\n\n\n");
return -1;
}
我们希望调度程序应该是一个后台程序,为此首先应该关闭所有的信号与IO,然后由父进程创建一个子进程,接着父进程退出。这样操作以后程序将由系统1号进程进行托管,变成后台程序。signal()函数将SIGCHLD
信号的处理方式设置为默认处理方式。默认处理方式是由操作系统提供的处理方式,即在子进程退出时会自动回收资源,而不会产生僵尸进程。父进程只需要调用wait()
或waitpid()
等函数来获取子进程的退出状态即可。
//关闭信号和IO,后台运行。本程序不希望被打扰。
for (int ii=0;ii<64;ii++)
{
signal(ii,SIG_IGN);
close(ii);
}
// 生成子进程,父进程退出,让程序运行在后台,由系统1号进程托管。
if (fork()!=0) exit(0);
// 子进程启用SIGCHLD信号,让父进程可以wait子进程退出的状态。
signal(SIGCHLD,SIG_DFL);
然后我们需要定义一个新的数组来保存传入的参数:
char *pargv[argc-1];
/*pargv[0] = "/project/idc1/bin/crtsurfdata5";
pargv[1] = "/project/idc1/ini/stcode.ini";
pargv[2] = "/tmp/idc/surfdata";
pargv[3] = "/log/idc/crtsurfdata5.log";
pargv[4] = "xml,json,csv";
pargv[5] = NULL;*/
for(int ii=2;ii<argc;ii++)
{
pargv[ii-2]=argv[ii];
}
pargv[argc-2]=NULL;
最后就是调度程序的实现,代码如下:
while (true)
{
if (fork()==0)
{
// 子进程使用fork()再次生成一个子进程,第二个子进程通过调用execv()来执行被调度的程序,并传递相应的参数。
execv(argv[2],pargv);
exit(0);
}
else
{
//父进程使用wait()等待子进程退出状态,并获取子进程的退出状态信息。
//父进程通过调用sleep()来等待一定的时间,以实现调度周期。
int status;
wait(&status);
sleep(atoi(argv[1]));
}
}
2.5Linux共享内存
Linux共享内存是一种进程间通信的机制,允许多个进程共享同一块内存区域,从而实现高效的数据交换。共享内存可以提供快速的数据传输,因为进程可以直接访问共享内存区域,而无需进行数据拷贝和系统调用。
在Linux中,通过使用shmget
系统调用来创建一个共享内存区域,并返回一个标识符(共享内存ID)。进程可以使用该标识符通过shmat
系统调用将共享内存区域附加到自己的地址空间中,从而使得进程可以访问该共享内存区域。
共享内存区域可以用于存储任意类型的数据,进程可以通过指针直接读取或写入共享内存中的数据。多个进程可以同时访问共享内存区域,可以进行读写操作,但需要注意同步和互斥,以避免数据一致性问题。
当进程不再需要访问共享内存区域时,可以使用shmdt
系统调用将其从进程的地址空间中分离。当不再需要共享内存区域时,可以使用shmctl
系统调用进行删除。
共享内存并未提供锁机制,也就是说,在某一个进程对共享内存的进行读写的时候,不会阻止其它的进程对它的读写。如果要对共享内存的读/写加锁,可以使用信号量。
2.5.1相关函数
头文件
#include <sys/ipc.h>
#include <sys/shm.h>
shmget函数,用于创建或者获取共享内存,原型:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数key是共享内存的键值,是一个整数,typedef unsigned int key_t,是共享内存在系统中的编号,不同共享内存的编号不能相同,这一点由程序员保证。key用十六进制表示比较好。
参数size是待创建的共享内存的大小,以字节为单位。
参数shmflg是共享内存的访问权限,与文件的权限一样,0666|IPC_CREAT表示全部用户对它可读写,如果共享内存不存在,就创建一个共享内存。
返回值为共享内存的id。
把共享内存连接到当前进程的地址空间。它的声明如下:
void *shmat(int shm_id, const void *shm_addr, int shmflg);
参数shm_id是由shmget函数返回的共享内存标识。
参数shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
参数shm_flg是一组标志位,通常为0。
调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1。
将共享内存从当前进程中分离函数,shmdt,声明如下:
int shmdt(const void *shmaddr);
参数shmaddr是shmat函数返回的地址。
调用成功时返回0,失败时返回-1。
删除共享内存,它的声明如下:
int shmctl(int shm_id, int command, struct shmid_ds *buf);
参数shm_id是shmget函数返回的共享内存标识符。
参数command填IPC_RMID。
参数buf填0。
注意,用root创建的共享内存,不管创建的权限是什么,普通用户无法删除。
使用示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
struct st_pid
{
int pid; // 进程编号。
char name[51]; // 进程名称。
};
int main(int argc,char *argv[])
{
// 共享内存的标志。
int shmid;
// 获取或者创建共享内存,键值为0x5005。
if ( (shmid=shmget(0x5005, sizeof(struct st_pid), 0640|IPC_CREAT))==-1)
{ printf("shmget(0x5005) failed\n"); return -1; }
// 用于指向共享内存的结构体变量。
struct st_pid *stpid=0;
// 把共享内存连接到当前进程的地址空间。
if ( (stpid=(struct st_pid *)shmat(shmid,0,0))==(void *)-1)
{ printf("shmat failed\n"); return -1; }
printf("pid=%d,name=%s\n",stpid->pid,stpid->name);
stpid->pid=getpid();
strcpy(stpid->name,argv[1]);
printf("pid=%d,name=%s\n",stpid->pid,stpid->name);
// 把共享内存从当前进程中分离。
shmdt(stpid);
// 删除共享内存。
// if (shmctl(shmid,IPC_RMID,0)==-1)
// { printf("shmctl failed\n"); return -1; }
return 0;
}
[sxixia@localhost c_project]$ ./signal aaa
pid=4387,name=
pid=4438,name=aaa
//查看共享内存
[sxixia@localhost c_project]$ ipcs -m
------------ 共享内存段 --------------
键 shmid 拥有者 权限 字节 nattch 状态
0x00000000 4 sxixia 777 16384 1 目标
0x00000000 8 sxixia 777 2129920 1 目标
0x00000000 11 sxixia 600 524288 2 目标
0x00000000 15 sxixia 600 524288 2 目标
0x00000000 16 sxixia 777 7864320 2 目标
0x00000000 17 sxixia 777 7864320 2 目标
0x00000000 18 sxixia 600 524288 2 目标
0x00000000 21 sxixia 600 524288 2 目标
0x00000000 22 sxixia 777 3407872 2 目标
0x00000000 23 sxixia 600 524288 2 目标
0x00000000 25 sxixia 600 524288 2 目标
0x00000000 26 sxixia 777 3407872 2 目标
0x00000000 28 sxixia 600 524288 2 目标
0x00000000 29 sxixia 600 524288 2 目标
0x00000000 38 sxixia 600 16777216 2 目标
0x00000000 39 sxixia 600 524288 2 目标
0x00000000 40 sxixia 777 1769472 2 目标
0x00005005 41 sxixia 640 56 0
0x00000000 44 sxixia 600 524288 2 目标
0x00000000 46 sxixia 777 3194880 2 目标
//删除共享内存
[sxixia@localhost c_project]$ ipcrm -m 41
2.6信号量的概念
信号量(信号灯)本质上是一个计数器,用于协调多个进程(包括但不限于父子进程)对共享数据对象的读/写。它不以传送数据为目的,主要是用来保护共享资源(共享内存、消息队列、socket连接池、数据库连接池等),保证共享资源在一个时刻只有一个进程独享。
信号量是一个特殊的变量,只允许进程对它进行等待信号和发送信号操作。最简单的信号量是取值0和1的二元信号量,这是信号量最常见的形式。
通用信号量(可以取多个正整数值)和信号量集方面的知识比较复杂,应用场景也比较少。本文只介绍二元信号量。
2.6.1相关函数
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
semget函数用来获取或创建信号量,它的原型如下:
int semget(key_t key, int nsems, int semflg);
1)参数key是信号量的键值,typedef unsigned int key_t,是信号量在系统中的编号,不同信号量的编号不能相同,这一点由程序员保证。key用十六进制表示比较好。
2)参数nsems是创建信号量集中信号量的个数,该参数只在创建信号量集时有效,这里固定填1。
3)参数sem_flags是一组标志,如果希望信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。如果没有设置IPC_CREAT标志并且信号量不存在,就会返错误(errno的值为2,No such file or directory)。
4)如果semget函数成功,返回信号量集的标识;失败返回-1,错误原因存于error中。
例如:
int semid=semget(0x5000,1,0640|IPC_CREAT);
semctl(),该函数用来控制信号量(常用于设置信号量的初始值和销毁信号量),它的原型如下:
int semctl(int semid, int sem_num, int command, ...);
1)参数semid是由semget函数返回的信号量标识。
2)参数sem_num是信号量集数组上的下标,表示某一个信号量,填0。
3)参数cmd是对信号量操作的命令种类,常用的有以下两个:
IPC_RMID:销毁信号量,不需要第四个参数;
SETVAL:初始化信号量的值(信号量成功创建后,需要设置初始值),这个值由第四个参数决定。第四参数是一个自定义的共同体,如下:
// 用于信号灯操作的共同体。
union semun
{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
4)如果semctl函数调用失败返回-1;如果成功,返回值比较复杂,暂时不关心它。
示例:
//销毁信号量
semctl(semid,0,IPC_RMID);
//初始化信号量的值为1,信号量可用。
union semun sem_union;
sem_union.val = 1;
semctl(semid,0,SETVAL,sem_union);
semop()函数,该函数有两个功能:1)等待信号量的值变为1,如果等待成功,立即把信号量的值置为0,这个过程也称之为等待锁;2)把信号量的值置为1,这个过程也称之为释放锁。
int semop(int semid, struct sembuf *sops, unsigned nsops);
1)参数semid是由semget函数返回的信号量标识。
2)参数nsops是操作信号量的个数,即sops结构变量的个数,设置它的为1(只对一个信号量的操作)。
3)参数sops是一个结构体,如下:
struct sembuf
{
short sem_num; // 信号量集的个数,单个信号量设置为0。
short sem_op; // 信号量在本次操作中需要改变的数据:-1-等待操作;1-发送操作。
short sem_flg; // 把此标志设置为SEM_UNDO,操作系统将跟踪这个信号量。
// 如果当前进程退出时没有释放信号量,操作系统将释放信号量,避免资源被死锁。
};
示例:
1)等待信号量的值变为1,如果等待成功,立即把信号量的值置为0;
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;
sem_b.sem_flg = SEM_UNDO;
semop(sem_id, &sem_b, 1);
2)把信号量的值置为1。
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;
sem_b.sem_flg = SEM_UNDO;
semop(sem_id, &sem_b, 1);
由于信号量的操作比较复杂,所以在开发框架中对这些操作进行了封装。封装成CSEM类,称之为信号灯,类似互斥锁,包括初始化信号灯、等待信号灯、挂出信号灯和销毁信号灯。我先不做该类的测试程序。直接在后续程序中使用。
我们已经完成了调度程序的开发,接下来应该了解守护进程。
一个程序由调度程序启动。如果服务程序死机,将由守护进程终止它,当服务程序被终止后,由调度程序将它重新启动。这样就能保证系统的稳定运行。那么守护进程是怎么样判断一个进程需要被终止呢?
2.7进程的心跳机制
进程的心跳机制是指进程通过定期发送心跳信号来表明自身的活动状态。这种机制通常用于进程间通信或监控系统中,用于检测进程是否处于正常运行状态。
在心跳机制中,进程定期发送心跳信号给其他进程或监控程序。这个信号可以是特定的消息、状态更新或简单的空消息,用于表示进程仍然在运行。接收到心跳信号的进程或监控程序可以根据心跳信号的频率和稳定性来判断目标进程的健康状况。
心跳机制有以下几个主要目的:
监测进程健康状态:通过定期接收心跳信号,监控程序可以判断进程是否仍然在运行,并及时采取相应的措施,如重新启动进程或发送警报。
保持进程间通信:心跳信号也可以用于进程间通信,作为一种保持连接的机制。进程可以定期发送心跳信号给其他进程,以确保网络连接的稳定性和持续性。
节省系统资源:通过心跳机制,可以及时检测到不正常的进程或失效的连接,并及时终止或重建,从而避免资源的浪费和系统的不稳定性。
具体的实现方法是在linux中创建一块共享内存。存放每一个进程的心跳信息结构体。每一个程序开始时会向共享内存的结构体数组中查找一个空白的位置,将自己的心跳信息放进去。并且程序的运行过程中还会不断地更新自己的心跳信息以告诉守护进程自己还“活着”。守护进程每隔一段时间遍历一次共享内存。下面是进程心跳信息的结构体。如果当前时间减去最后一次心跳的时间大于超时时间,那么守护进程将负责杀死该进程。
该程序位置为/project/tools1/c/bookyuan.cpp
//进程心跳信息的结构体
struct st_pinfo
{
int pid; //进程的pid
char pname[51]; //进程的名字,可以为空,为了方便
int timeout; //超时时间,单位为秒
time_t atime; //最后一次心跳时间,整数表示
};
程序的主体流程如下:
#include "_public.h"
int main(int argc,char *argv[])
{
if (argc!=3)
{
printf("using: procname updatetime\n");
printf("example: ./book aaa 20"); return 0;
}
// 创建/获取共享内存,大小为n*sizeof(strcut st_pinfo),n为进程最大数量
//创建信号量,如果存在则获取信号量,如果不存在,则创建信号量并初始化为value
// 将共享内存连接到当前进程的地址空间
// 创建当前进程的心跳信息结构体变量
// 在共享内存中查找一个空位置,把当前进程的心跳信息存进去
while(true)
{
// 更新心跳时间
sleep(10);
}
// 把当前进程从共享内存中移去
// 把共享内存从当前进程中分离
return 0;
}
因为这是一个通用的工具程序。为了使用的方便,我们将其封装为一个类。在构造函数里进行一些变量的初始化,在析构函数中实现删除心跳信息和分离共享内存的功能。
#include "_public.h"
#define MAXNUMP_ 1000 //最大的进程数量
#define SHMKEYP_ 0x5095 //共享内存的key
#define SEMKEYP_ 0x5095 //信号量的key
//进程心跳信息的结构体
struct st_pinfo
{
int pid; //进程的pid
char pname[51]; //进程的名字,可以为空,为了方便
int timeout; //超时时间,单位为秒
time_t atime; //最后一次心跳时间,整数表示
};
class PActive
{
private:
CSEM m_sem; //给共享内存加锁的信号量
int m_shmid; //共享内存的id
int m_pos; //当前进程在共享内存组中的位置
struct st_pinfo *m_shm; //指向共享内存的结构体指针
public:
PActive();
bool AddPInfo(const int timeout,const char *pname);
bool UptATime();
~PActive();
};
int main(int argc,char *argv[])
{
if (argc!=3)
{
printf("using: procname updatetime\n");
printf("example: ./book aaa 20");
return 0;
}
PActive Active;
Active.AddPInfo(30,argv[1]);
while(1)
{
Active.UptATime();
sleep(10);
}
return 0;
}
PActive::PActive()
{
m_shmid=-1;//共享内存的id
m_pos=-1; //当前进程在共享内存组中的位置
m_shm=0; //结构体指针
}
接下来来看AddPInfo方法。注意中间的注释文字。这就是为什么先要遍历一遍寻找有没有与该进程pid相同的纪录。关于信号量的加锁与解锁。什么时候加锁?什么时候解锁?一般来说遵循以下原则:
在进入临界区之前加锁:在进入访问临界资源的代码段之前加锁,以确保只有一个线程或进程可以进入临界区。这样可以避免多个线程或进程同时访问临界资源导致数据不一致或竞态条件的发生。
在访问共享资源之前加锁:如果多个线程或进程需要访问共享资源,应在访问该资源之前加锁,以确保同一时间只有一个线程或进程可以访问该资源。
在离开临界区之前解锁:在临界区的代码段执行完毕后,应该立即解锁信号量,以允许其他线程或进程访问临界资源。这样可以确保不会出现资源被长时间占用的情况。
在完成对共享资源的访问后解锁:如果多个线程或进程共享同一资源,在完成对该资源的访问后应该立即解锁,以允许其他线程或进程访问该资源。
加锁和解锁的时机要慎重考虑,确保临界区的长度尽可能短,避免不必要的阻塞和竞争。同时,要确保解锁操作一定会执行,避免出现死锁的情况。
//添加心跳信息
bool PActive::AddPInfo(const int timeout,const char *pname)
{
//如果已经找到位置,不需要再找
if(m_pos!=-1) return true;
//创建/获取共享内存,大小为n*sizeof(struct st_pinfo) 键值为16进制
if((m_shmid=shmget(SHMKEYP_,MAXNUMP_*sizeof(struct st_pinfo),0640|IPC_CREAT))==-1)
{ printf("shmget(%x) failed\n",SHMKEYP_); return false; }
//创建信号量,如果存在则获取信号量,如果不存在,则创建信号量并初始化为value
if(m_sem.init(SEMKEYP_)==false)
{ printf("m_sem.init(%x) failed\n",SEMKEYP_); return false; }
//共享内存连接到当前进程的地址空间
//struct st_pinfo *m_shm;
m_shm=(struct st_pinfo *)shmat(m_shmid,0,0);
//创建当前进程心跳信息结构体变量,把本进程的信息填进去
struct st_pinfo stpinfo;
memset(&stpinfo,0,sizeof(struct st_pinfo));
stpinfo.pid=getpid(); //进程id
STRNCPY(stpinfo.pname,sizeof(stpinfo.pname),pname,50); //进程名字
stpinfo.timeout=timeout; //超时时间
stpinfo.atime=time(0); //最后一次心跳时间,起始为当前时间
//在共享内存中查找一个空位置,把当前进程的心跳信息存入共享内存中
/*进程的id是循环使用的,如果曾经有一个进程异常退出,没有清理掉自己的心跳信息
*它的进程信息将残留到共享内存中,如果当前进程重用了上述进程的id
*那么共享内存中存在两个进程id相同的纪录,守护进程检查到残留进程的心跳时
*会向进程id发送杀死信号,这个信号将误杀当前进程
*先进行循环遍历,如果共享内存中存在当前进程的编号,那么就是残留
*/
for(int ii=0;ii<MAXNUMP_;ii++)
if(m_shm[ii].pid==stpinfo.pid) { m_pos=ii; break; }
m_sem.P(); //加锁
if(m_pos=-1)
{
for(int ii=0;ii<MAXNUMP_;ii++)
{
//if((m_shm+ii)->pid==0)
if(m_shm[ii].pid==0)
{
//找到了一个空位置
m_pos=ii; break;
}
}
}
if(m_pos==-1)
{
m_sem.V(); //解锁,异常退出之前需要进行解锁
printf("共享内存已用完\n"); return false;
}
//把当前进程的心跳信息存入共享内存的进程组中
memcpy(m_shm+m_pos,&stpinfo,sizeof(struct st_pinfo));
m_sem.V(); //解锁
return true;
}
最后是更新心跳信息的方法和析构函数。删除心跳信息的方法就将结构体清0就好了。这样一来守护进程的心跳模块就写完了。这一部分已经做了一部分优化后写入了开发框架。接着我们写守护进程的终止进程的功能。
//更新共享内存中本进程的心跳时间
bool PActive::UptATime()
{
if(m_pos==-1) return false;
m_shm[m_pos].atime=time(0);
return true;
}
PActive::~PActive()
{
//把当前进程从共享内存中删除
if(m_pos!=-1) memset(m_shm+m_pos,0,sizeof(struct st_pinfo));
//把共享内存从当前内存中分离
if(m_shm!=0) shmdt(m_shm);
}
2.8守护进程
该程序的思路是首先创建或者获取一块共享内存,之后遍历该共享内存,检查记录中的pid项,如果pid为0,表示是空记录,继续遍历。如果pid不为0,先不管该进程是否超时,首先向该进程发送信号0。在前面已经介绍过,信号0通常用于检查进程是否还存在。如果不存在了,直接终止。如果存在,继续判断该进程是否超时。如果已经超时,先发送信号15尝试正常终止,如果失败,则发送信号9强行终止程序。共享内存遍历完一遍以后就进行分离,结束程序。
该文件命名为:/project/tools1/c/checkproc.cpp
#include "_public.h"
CLogFile logfile;
int main(int argc,char *argv[])
{
// 程序的帮助。
if (argc != 3)
{
printf("\n");
printf("Using:./checkproc logfilename\n");
printf("Example:/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,不希望程序被干扰。
//for(int ii=0;ii<64;ii++)
//{
//signal(ii,SIG_IGN);
//close(ii);
//}
// 使用开发框架中的函数避免繁琐,缺省为false只关闭信号,不关闭io。
// 守护程序没有关心的信号,所以不用重新打开某些信号。
CloseIOAndSignal(true);
// 打开日志文件。
if(logfile.Open(argv[2],"a+")==false)
{ printf("logfile.Open(%s) failed",argv[2]); return -1; }
// 创建/获取共享内存,键值为SHMKEYP,大小为MAXNUMP个st_procinfo结构体的大小。
// SHMKEYP,MAXNUMP已在开发框架中定义好
int shmid=0;
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;
// kill函数,存在返回0,不存在返回-1.
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); // 取当前时间。
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;
}
2.9压缩和清理文件
本章最后两个小程序是压缩文件程序和清理历史数据的程序。程序位置为:/project/tools1/c/gzipfiles.cpp,/project/tools1/c/deletefiles.cpp。支持对指定目录下,指定的匹配文件名,指定的时间的文件进行处理。
#include "_public.h"
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(" /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);
//获取文件超时的时间点
//LocalTime函数,详情见本章第一节。
char strTimeOut[21];
LocalTime(strTimeOut,"yyyy-mm-dd hh24:mi:ss",0-(int)(atoi(argv[3])*24*60*60));
CDir Dir;
//打开目录
//OpenDir参数:文件位置,匹配规则,文件处理数量,是否处理子目录,缺省不进行排序
if(Dir.OpenDir(argv[1],argv[2],10000,true)==false)
{
printf("Dir.OpenDir(%s) failed\n",argv[1]); return -1;
}
char strCmd[1024]; //存放压缩命令的数组
//遍历目录中的文件名
while(1)
{
//得到一个文件的信息,返回错误代表获取完了
if(Dir.ReadDir()==false) break;
printf("FullFileName=%s\n",Dir.m_FullFileName);
//与超时的时间点比较,如果要早,就需要压缩
if((strcmp(Dir.m_ModifyTime,strTimeOut)<0) && (MatchStr(Dir.m_FileName,"*.gz")==false))
{
//压缩文件,调用系统的gzip命令
//1>/dev/null 2>/dev/null是把标准输出和标准输入定位到null里面去,也就是不输出任何东西
SNPRINTF(strCmd,sizeof(strCmd),1000,"gzip -f %s 1>/dev/null 2>/dev/null",Dir.m_FullFileName);
printf("%s\n",strCmd);
//system函数需要的参数是命令,也可以和exec函数族一样调用系统命令
//printf函数用于测试,关闭io以后不会显示,代码也没必要删除
if(system(strCmd)==0)
printf("gzip %s ok\n\n",Dir.m_FullFileName);
else
printf("gzip %s failed\n\n",Dir.m_FullFileName);
}
}
return 0;
}
void EXIT(int sig)
{
printf("程序退出:sig=%d",sig);
exit(0);
}
#include "_public.h"
void EXIT(int sig);
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(" /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/rm命令删除文件。\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)(atoi(argv[3])*24*60*60));
CDir Dir;
//打开目录
if(Dir.OpenDir(argv[1],argv[2],10000,true)==false)
{
printf("Dir.OpenDir(%s) failed\n",argv[1]); return -1;
}
char strCmd[1024]; //存放压缩命令的数组
//遍历目录中的文件名
while (true)
{
// 得到一个文件的信息,CDir.ReadDir()
if (Dir.ReadDir()==false) break;
printf("=%s=\n",Dir.m_FullFileName);
// 与超时的时间点比较,如果更早,就需要删除。
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",sig);
exit(0);
}
2.10脚本运行
服务程序写完以后就需要让它跑起来。那么这么多的服务程序肯定不可能由我们手动启动。所以需要写一个shell脚本来进行启动。该脚本通过启动调度程序,再由调度程序启动其他的服务程序。每隔60秒生成三份数据,每隔300秒压缩一次0.02天前的日志。每隔300秒删除所有生成的测试数据文件。脚本文件位于/project/idc1/c
把脚本添加到/etc/rc.local文件当中去,这样一来系统每次开机就会自动执行该脚本了。
[sxixia@localhost ~]$ vim start.sh
####################################################################
# 启动数据中心后台服务程序的脚本。
####################################################################
# 检查服务程序是否超时,配置在/etc/rc.local中由root用户执行。
#/project/tools1/bin/procctl 30 /project/tools1/bin/checkproc
# 压缩数据中心后台服务程序的备份日志。
/project/tools1/bin/procctl 300 /project/tools1/bin/gzipfiles /log/idc "*.log.20*" 0.02
# 生成用于测试的全国气象站点观测的分钟数据。
/project/tools1/bin/procctl 60 /project/idc1/bin/crtsurfdata /project/idc/ini/stcode.ini /tmp/idc/surfdata /log/idc/crtsurfdata.log xml,json,csv
# 清理原始的全国气象站点观测的分钟数据目录/tmp/idc/surfdata中的历史数据文件。
/project/tools1/bin/procctl 300 /project/tools1/bin/deletefiles /tmp/idc/surfdata "*" 0.02
[sxixia@localhost c]$ sudo vim /etc/rc.local
#检查服务程序是否超时
#/project/tools1/bin/procctl 30 /project/tools1/bin/checkproc
#启动数据中心的后台服务程序
#su - sxixia -c "/bin/sh /project/idc1/c/start.sh"
#在最后添加这两行,我进行了注释,需要删除#号,保存退出。
#添加可执行权限并重启。
[sxixia@localhost c]$ chmod +x /etc/rc.d/rc.local
[sxixia@localhost c]$ init 6
有了开始的脚本,自然也有结束的脚本。
[sxixia@localhost c]$ vim killall.sh
####################################################################
# 停止数据中心后台服务程序的脚本。
####################################################################
killall -9 procctl
killall gzipfiles crtsurfdata deletefiles
sleep 3
killall -9 gzipfiles crtsurfdata deletefiles
#首先强制杀死调度程序,如果不先杀死调度程序,其他服务程序可能会被再次启动。接着结束其他服务程序,给3秒时间来退出。3秒钟之后进行强制杀死。
本章完更。