心跳进程与守护进程(一)
一、进程心跳
日常服务程序运行过程中,一般进程的调度、进程的心跳、守护进程共同工作。本文介绍心跳进程的原理和实现。心跳进程就是服务程序在后台运行过程中表示自己还“活着”,表示服务进程还在健康地运行着。具体实现:每个服务程序启动的时候查找共享内存,把自己心跳信息填写进去,运行过程不断更新到共享内存,代表自己还活着。流程如下:
- 设置心跳进程结构体{进程pid,进程名,超时时间,存活时间}
- 处理程序的退出信号。
- 创建/获取共享内存。
- 将共享内存连接到当前进程的地址空间。
- 把当前进程的信息填充到结构体中。
- 在共享内存中寻找一个空的位置,把当前进程的结构体保存到共享内存中。
- 更新进程的心跳信息。
- 从共享内存中删除当前进程的心跳信息。
- 把共享内存从当前进程分离。
二、共享内存
知识点
🔴shmget函数
该函数用于创建/获取共享内存。
-
int shmget(key_t key, size_t size, int shmflg);
-
key 共享内存的键值,是一个整数(typedef unsigned int key_t),一般采用十六进制,例如0x5005,不同共享内存的key不能相同。
-
size 共享内存的大小,以字节为单位。
-
shmflg 共享内存的访问权限,与文件的权限一样,例如0666|IPC_CREAT,0666表示全部用户对它可读写,IPC_CREAT表示如果共享内存不存在,就创建它。
-
返回值:成功返回共享内存的id(一个非负的整数),失败返回-1(系统内存不足、没有权限)
-
用ipcs -m可以查看系统的共享内存,包括:键值(key),共享内存id(shmid),拥有者(owner),权限(perms),大小(bytes)。
-
用ipcrm -m 共享内存id 可以手工删除共享内存,如下:
🔴shmat函数
该函数用于把共享内存连接到当前进程的地址空间。
-
void *shmat(int shmid, const void *shmaddr, int shmflg);
-
shmid 由shmget()函数返回的共享内存标识。
-
shmaddr 指定共享内存连接到当前进程中的地址位置,通常填0,表示让系统来选择共享内存的地址。
-
shmflg 标志位,通常填0。
-
调用成功时返回共享内存起始地址,失败返回(void*)-1。
🔴shmdt函数
该函数用于将共享内存从当前进程中分离,相当于shmat()函数的反操作。
-
int shmdt(const void *shmaddr);
-
shmaddr shmat()函数返回的地址。
-
调用成功时返回0,失败时返回-1。
🔴shmctl函数
该函数用于操作共享内存,最常用的操作是删除共享内存。
-
int shmctl(int shmid, int command, struct shmid_ds *buf);
-
shmid shmget()函数返回的共享内存id。
-
command 操作共享内存的指令,如果要删除共享内存,填IPC_RMID。
-
buf 操作共享内存的数据结构的地址,如果要删除共享内存,填0。
-
调用成功时返回0,失败时返回-1。
三、心跳进程的代码实现
/** heartbeat.cpp 本程序是demo用于服务程序在共享内存记录自己的心跳,表示自己还活着。最终封装成进程心跳的类。* 作者:张咸武。
*/
#include<_public.h>
using namespace std;
using namespace idc;//进程心跳的结构体
struct stprocinfo
{int pid=0; //进程idchar pname[51]; //进程名称,可以为空。int timeout=0; //超时时间,单位:秒。time_t atime=0; //最后一次心跳的时间,用整数表示。stprocinfo()=default;//有了自定义的构造函数,编译器将不再提供默认构造函数,所以启用默认构造函数stprocinfo(const int in_pid,const string &in_pname,const int in_timeout,const time_t in_atime):pid(in_pid),timeout(in_timeout),atime(atime){strncpy(pname,in_pname.c_str(),50);}
};int m_shmid=-1; //共享内存的id。
stprocinfo *m_shm=nullptr; //指向共享内存的地址空间。
int m_pos=-1; //用于存放当前进程在数组中的下标。void EXIT(int sig);int main()
{//处理程序的退出信号。signal(SIGINT,EXIT);signal(SIGTERM,EXIT);//创建/获取共享内存。if((m_shmid=shmget((key_t)0x5095, 1000*sizeof(stprocinfo),0666|IPC_CREAT))==-1)//如果失败{printf("创建/获取共享内存(%x)失败。\n",0x5095);return -1;}//将共享内存连接到当前进程的地址空间。m_shm=(struct stprocinfo *)shmat(m_shmid,0,0);//把共享内存中的全部进程的信息显示出来,用于调试for(int ii=0;ii<1000;ii++){if(m_shm[ii].pid!=0) //只显示进程已使用的位置,空位置不显示。{printf("ii=%d,pid=%d,pname=%s,timeout=%d,atime=%d\n",ii,m_shm[ii].pid,m_shm[ii].pname,m_shm[ii].timeout,m_shm[ii].atime);}}stprocinfo procinfo(getpid(),"server1",30,time(0));//当前进程号 进程名 超时时间 当掐时间csemp semlock; //用于给共享内存加锁的信号量id。if(semlock.init(0x5095)==false){printf("创建/获取信号量(%x)失败。\n",0x5095); EXIT(-1);}//信号量加锁 P操作semlock.wait();//优化后添加的代码,重用异常退出残留共享内存的旧位置。
//进程id是循环使用的,如果曾经有一个进程异常退出,没有清理自己的心跳信息,
//他的进程信息将残留在共享内存中,不巧的是,如果当前进程重用了他的id,
//所以,如果共享内存中已存在当前进程编号,一定是其他进程残留的信息,当前进程应该重用这个位置。for(int ii=0;ii<1000;ii++){if(m_shm[ii].pid==procinfo.pid){m_pos=ii;printf("找到旧位置ii=%d\n",ii);break;}}if(m_pos==-1) //如果已经找到了一个旧位置并重用了,就不需要再找空位置了。{//在共享内存中寻找一个空的位置,把当前进程的结构体保存到共享内存中。for(int ii=0;ii<1000;ii++){if((m_shm+ii)->pid==0) //如果pid是空的,表示这是一个空位置。{m_pos=ii;printf("找到新位置ii=%d\n",ii);break;}}}//如果m_pos==-1,表示没找到空位置,说明共享内存的空间已用完。if(m_pos==-1){//共享空间用完需要解锁 V操作 semlock.post();("共享内存空间已用完。\n");EXIT(-1);}//把当前进程的结构体保存到共享内存中。// memcpy(m_shm+m_pos,&procinfo,sizeof(struct stprocinfo));memcpy(m_shm + m_pos, &procinfo, sizeof(struct stprocinfo));semlock.post(); //解锁 V操作while(true){printf("服务程序运行中...\n");//更新进程的心跳信息。sleep(25);m_shm[m_pos].atime=time(0);sleep(25);m_shm[m_pos].atime=time(0);}return 0;
}//程序退出和信号2、15的处理函数。
void EXIT(int sig)
{printf("sig=%d\n",sig);//从共享内存中删除当前进程的心跳信息。if(m_pos!=-1) memset(m_shm+m_pos,0,sizeof(struct stprocinfo));//把共享内存从当前进程分离。if(m_shm!=0) shmdt(m_shm);//这句是GPT4提示需要销毁的。“标记为”删除,我们的目的不需要启用。// 销毁共享内存// if (m_shmid != -1) shmctl(m_shmid, IPC_RMID, nullptr);exit(0);
}
四、一些细节
🔴第一个问题:当程序异常退出如kill -9 或 段错误,那么EXIT没机会执行,共享内存会残留(这(异常退出)是不可避免地)。因此在程序执行开始在共享内存寻找空位置的时候,共享内存中残留了进程的信息并且进程编号与当前进程相同,就重用这个旧位置而不再寻找共位置了!
🔴第二个问题:多个进程同时操作共享内存,会造成安全性的问题。需要对共享内存加锁(内存框架中的信号量csemp类)。程序对共享内存的操作由三个:1.在共享内存中寻找一个新的位置,把当前进程结构体写入共享内存 2.更新自己在共享内存的结构体的心跳时间。 3.从共享内存中删除自己。 通过分析可知只需要在1操作加锁即可,因为23操作只会操作自己那部分结构体,不会造成冲突!这里一定要搞明白。
🔴3.将heatbeat封装成进程心跳的类,便于在后续服务程序中使用。