作者: 守望,Linux应用开发者,目前在公众号【编程珠玑】 分享Linux/C/C++/数据结构与算法/工具等原创技术文章和学习资源。有些程序我们希望在一台机器上只有一个实例在运行,我在windows下也遇到过很多类似这样的程序,如QQ,它只允许同时运行一个。那么我们在Linux该如何实现这样的单例运行的程序呢?
思路
实现这样的程序方法很多,但是总体思路都是类似的:- 1.启动程序,检测标志,判断是否有同样的程序运行,是则2,否则3
- 2.程序退出
- 3.程序启动,并设置标志,以便下次启动时检测
实现方法
按照这种思路,实现的方法有很多种,例如使用ps等命令获取该进程的进程数,大于0 表示已有运行;启动后写一个临时文件,如果下次启动时发现有该文件,则直接退出;创建一个文件并加锁,退出时删除文件,新的程序启动时试图加锁,如果失败,则说明已有实例运行…… 除了上面说到的这些,可能还有一些其他的实际做法,但是本文介绍一种实用并且也是非常通用的做法,即文件锁的方法。基本原理
程序在启动后,打开一个program.pid文件(无则创建),然后试图去设置文件锁(如果还不理解锁的概念,可以简单理解为,一旦a写锁定了,b就无法进一步写操作了,除非a释放锁),如果设置成功,就将该程序的进程ID写入该文件;如果加锁失败,那么说明已经有另外一个实例在运行了,则退出此次启动。而当前已经运行的程序如果退出了,该文件会自动解除锁定。 实际上,我们观察一下/var/run/目录下,有很多类似这样的文件:$ ls -l /var/run/*.pid-rw-r--r-- 1 root root 5 11月 24 08:19 /var/run/acpid.pid-rw-r--r-- 1 root root 5 11月 24 08:19 /var/run/atd.pid-rw-r--r-- 1 root root 5 11月 24 08:19 /var/run/crond.pid-rw-r--r-- 1 root root 5 11月 24 09:08 /var/run/dhclient-wlp3s0.pid-rw-r--r-- 1 root root 4 11月 24 08:19 /var/run/docker.pid
不过这个位置通常只有root用户能够写入。
实现
在看代码实现之前,先看下文件锁(记录锁)和fcntl函数。 flock结构至少包含以下字段 :struct flock { short l_type; /*锁类型 F_RDLCK,F_WRLCK, F_UNLCK*/ short l_whence; /* 偏移开始的位置 SEEK_SET, SEEK_CUR, SEEK_END */ off_t l_start; /* 开始锁定区域*/ off_t l_len; /* 锁定字节数 */ pid_t l_pid; /* 记录锁的进程ID*/};
从结构体成员可以看到,它不仅可以锁整个文件,还可以锁某个区域,但是本文仅用于锁定整个文件。
int fcntl(int fd, int cmd, ... /* arg */
fcntl函数的cmd选项也很多,本文只用到F_SETLK(或F_SETLKW,),即设置锁。
结合以上两者,参考代码如下:
//来源:公众号【编程珠玑】//博客:https://www.yanbinghu.com#include #include #include #include #include #include #include #define PROC_NAME "single_instance"#define PID_FILE_PATH "/var/run/"static int lockFile(int fd);static int isRunning(const char *procname);int main(void){ /*判断是否已经有实例在运行*/ if(isRunning(PROC_NAME)) { exit(-1); } printf("run ok
"); sleep(20);//避免程序立即退出 return 0;}/*锁文件还可以使用flock,目的是类似的。不过是它是BSD系统调用,并且某些版本不支持NFS,出于移植性考虑,使用fcntl*/static int lockFile(int fd){ struct flock fl; fl.l_type = F_WRLCK;//设置写锁 fl.l_start = 0; fl.l_whence = SEEK_SET; fl.l_len = 0; fl.l_pid = -1;//锁定文件,设置为-1 return(fcntl(fd, F_SETLK, &fl));}static int isRunning(const char *procname){ char buf[16] = {0}; char filename[128] = {0}; sprintf(filename, "%s%s.pid",PID_FILE_PATH,procname); int fd = open(filename, O_CREAT|O_RDWR );//可读可写,不存在时创建 if (fd 0) { printf("open file %s failed!
", filename); return 1; } if (-1 == lockFile(fd)) /*尝试加锁*/ { printf("%s is already running
", procname); close(fd); return 1; } else { ftruncate(fd, 0);/*截断文件,重新写入pid*/ sprintf(buf, "%ld", (long)getpid()); write(fd, buf, strlen(buf) + 1); return 0; }}
编译运行
$ gcc -o single_instance single_instance.c$ ./single_instance #注意root权限运行,或者调整pid文件位置run ok
查看pid文件目录下已经有了pidfile:
$ ls -al /var/run/single_instance.pid-rw-r--r-- 1 root root 6 11月 24 11:36 /var/run/single_instance.pid
在另外一个终端再次尝试运行:
$ ./single_instancesingle_instance is already running
如果你想控制同一个目录下的bin文件只能运行一个,那么可以设置pid文件的位置为当前目录。
这种方式有什么特点呢:
- 简单可靠
- 可读可见,相比于信号量或共享内存,它更容易观察
- 无性能要求,启动时加锁,结束释放。
- 一旦出现异常没有释放,也可以手动删除文件
struct pidfh *pidfile_open(const char *path, mode_t mode, pid_t *pidptr);int pidfile_write(struct pidfh *pfh);int pidfile_close(struct pidfh *pfh);int pidfile_remove(struct pidfh *pfh);
本文就不过多介绍了。
总结
单例运行的基本原理是类似的,而pid文件是一种常见的单例运行的方式,很多知名的开源组件都是使用类似的方式。对于shell脚本,还可以使用flock命令进行类似的操作。 你一般用什么方式来实现单例运行呢?欢迎留言分享。●编号618,输入编号直达本文
●输入m获取文章目录
C语言与C++编程分享C/C++技术文章