在重要类应用进程中,经常有一种防止一个进程实例被重复启动的场景。这种场景大多是单实例处理一些资源,比如处理一个文件。如果没有一些控制手段,重复启动相同的进程实例,容易导致处理文件的意外数据错误发生。最近在阅读zookeeper源码,正巧从启动部分开到这种控制方式,特地结合以前开发应用框架时代码中采用文件锁来防止重复启动实例方式,总结一下。
1.zookeeper中脚本实现进程重复启动控制
zookeeper服务端启动先从启动的脚本阅读起。阅读任何一段源码除了理清楚它的结构以外,都要从其启动入口开始来理解整个流程。
zookeeper启动脚本为zkServer.sh,该脚本启动之前通常会要求创建一个zoo.cfg启动配置,里面记录了数据、日志路径,监听客户端端口信息。
zookeeper启动脚本核心段如下:
zookeeper启动方式包含两种,一种为后台nohup启动方式,一种为foreground前台打印启动。这里先看一下后台nohup启动方式,理解一下zookeeper脚本防止应用进程实例重复启动的方法。
start)
echo -n "Starting zookeeper ... "
#判断启动pid文件是否存在,如果不存在则启动应用
if [ -f "$ZOOPIDFILE" ]; then
#如果启动pid文件存在,通过kiil -0判断该文件中记录的pid进程是否存在
if kill -0 `cat "$ZOOPIDFILE"` > /dev/null 2>&1; then
#如果pid存在则输出进程已经在运行的提示
echo $command already running as process `cat "$ZOOPIDFILE"`.
exit 1
fi
fi
#后台方式启动应用进程
nohup "$JAVA" $ZOO_DATADIR_AUTOCREATE "-Dzookeeper.log.dir=${ZOO_LOG_DIR}" \
"-Dzookeeper.log.file=${ZOO_LOG_FILE}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}" \
-XX:+HeapDumpOnOutOfMemoryError -XX:OnOutOfMemoryError='kill -9 %p' \
-cp "$CLASSPATH" $JVMFLAGS $ZOOMAIN "$ZOOCFG" > "$_ZOO_DAEMON_OUT" 2>&1 < /dev/null &
if [ $? -eq 0 ]
then
case "$OSTYPE" in
#solaris操作系统启动成功后,将脚本启动最后一个进程的pid写入启动文件中
*solaris*)
/bin/echo "${!}\\c" > "$ZOOPIDFILE"
;;
#其它操作系统启动成功后,将脚本启动最后一个进程的pid写入启动文件中
*)
/bin/echo -n $! > "$ZOOPIDFILE"
;;
esac
if [ $? -eq 0 ];
then
sleep 1
pid=$(cat "${ZOOPIDFILE}")
if ps -p "${pid}" > /dev/null 2>&1; then
echo STARTED
else
echo FAILED TO START
exit 1
fi
else
echo FAILED TO WRITE PID
exit 1
fi
else
echo SERVER DID NOT START
exit 1
fi
;;
从zookeeper启动脚本中来看,zookeeper服务端进程启动为了防止重复,采用了脚本实现方式。
变量$ZOOPIDFILE为指定data目录下的pid文件路径名,zookeeper服务进程启动会将启动成功的pid记录在该文件中。
启动脚本中一开始有检查该$ZOOPIDFILE文件的判断处理,如果该文件存在,则通过kill -0判断文件中保存的pid是否存在。
1.如果pid不存在,则执行nohup命令后台启动zookeeper服务进程。
2.如果pid存在,则输出提示,zookeeper服务进程已经启动,“exit 1”直接退出。
如果pid文件不存在,同样启动zookeeper后台进程,以此来防止重复启动zookeeper服务进程。
“kill -0”不发送任何信号,但是系统会进行错误检查。所以经常用来检查一个进程是否存在,存在返回0;不存在返回1
2.通过文件锁方式实现重复启动进程控制
曾经开发应用框架,该框架重点处理文件和消息来源数据,应用进程启动实例需要严格防止重复启动,在代码层面实现采用文件锁控制机制来实现。
代码实现方式:
1)框架循环前预处理
框架采用主子进程模式,主进程启动后,会fork子进程,随后通过主进程退出,子进程进入后台循环处理方式来完成主框架进程执行。
void Application::preLoop()
{
char channel[10];
sprintf(channel, "%04d", m_channelNo);
m_lockpath = m_lockpath + m_name + channel;
if (m_asdaemon)
fork_child();
// child process
//判断启动命令后台运行
if (m_asdaemon || m_runinbkg)
to_background();
//如果设置不允许进程实例重复启动标记
//防重复启动判断
if (m_onlyone)
run_onlyone(m_lockpath.c_str());
// process SIGTERM signal
signal(SIGTERM, &Application::term_proc);
signal(SIGINT, &Application::term_proc);
signal(SIGSTOP, &Application::term_proc);
signal(SIGCONT, &Application::term_proc);
// get pid
m_pid = getpid();
}
2)防重复运行控制方法
利用了Linux文件控制机制,首先写方式打开或者创建指定的控制文件。同时赋予操作文件的读写权限。
借助Linux的flock文件锁,定义flock对象,同时设置文件锁类型为“写锁”,以文件的开头为锁位置。
通过Linux的fcntl控制文件方法,针对当前的文件设置获取“写锁”,如果写锁没有其他进程占用,则退出当前进程,保留子进程作为主进程运行。如果锁被其他进程占用,说明该应用进程实例已经被启动过,此时返回-1,当前启动进程退出。
通过write写文件方法,将最后存在的子进程pid写入启动控制文件。同时检查并关闭该文件描述符,返回0应用正常运行。
/**
* 防止进程重复执行控制方法
* @param filename 文件名
* @return int 返回-1则表明进程重复启动,退出;返回0则表示进程启动获取了启动锁文件,正常启动。
*/
int Application::run_onlyone(const char *filename)
{
int fd, val;
char buf[10];
//打开控制文件,控制文件打开方式:O_WRONLY | O_CREAT只写创建方式
//控制文件权限:S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH用户、用户组读写权限
if ((fd = open(filename, O_WRONLY | O_CREAT,
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) < 0)
{
return -1;
}
// try and set a write lock on the entire file
struct flock lock;
//建立一个供写入用的锁
lock.l_type = F_WRLCK;
lock.l_start = 0;
//以文件开头为锁定的起始位置
lock.l_whence = SEEK_SET;
lock.l_len = 0;
//结合lock中设置的锁类型,控制文件设置文件锁,此处设置写文件锁
if (fcntl(fd, F_SETLK, &lock) < 0)
{
//如果获取写文件锁成功,则退出当前进程,保留后台进程
if (errno == EACCES || errno == EAGAIN)
exit(0); // gracefully exit, daemon is already running
else
return -1; //如果锁被其他进程占用,返回 -1
}
// truncate to zero length, now that we have the lock
//改变文件大小为0
if (ftruncate(fd, 0) < 0)
return -1;
// and write our process ID
//获取当前进程pid
sprintf(buf, "%d\n", getpid());
//将启动成功的进程pid写入控制文件
if (write(fd, buf, strlen(buf)) != strlen(buf))
return -1;
// set close-on-exec flag for descriptor
// 获取当前文件描述符close-on-exec标记
if ( (val = fcntl(fd, F_GETFD, 0)) < 0)
return -1;
val |= FD_CLOEXEC;
//关闭进程无用文件描述符
if (fcntl(fd, F_SETFD, val) < 0)
return -1;
// leave file open until we terminate: lock will be held
return 0;
}