用 abstract unix socket 实现进程单实例运行 [2020-03-03]
一,问题背景
很多时候,我们需要确保进程只有一个实例运行。
有几种方法:
http://stackoverflow.com/questions/2964391/preventing-multiple-process-instances-on-linux
http://stackoverflow.com/questions/5339200/how-to-create-a-single-instance-application-in-c-or-c
https://github.com/qtproject/qt-solutions/tree/master/qtsingleapplication/src
比较常规的做法,是对一个文件加文件锁 flock,比如对 pid 文件 flock( LOCK_EX|LOCK_NB )
但是这种方法有些弊端:
- 如果文件被 mv 或者 rm,是会被绕过的。
- 如果磁盘故障比如磁盘满,目录没有写权限,会失败。
二,abstract namespace unix socket
http://linux.die.net/man/7/unix
unix socket 有3种:
- 基于文件的
- socketpair 创建的,匿名的
- abstract namespace 的,Linux特有
Linux 下, AF_UNIX socket 支持一种特殊的
abstract namespace unix socket 。
相比 普通的基于文件系统的 unix socket,abstract namespace unix socket :
- 没有磁盘文件
- 进程挂了以后自动删除,无残留文件
- 无需担心与 文件系统上的文件冲突,不需要关心文件系统上的绝对路径是否存在的问题
在 lsof 的结果里面看起来,就是有一些 类似 @test_abstract_ns 这样的 文件项
代码中使用也很简单, abstract namespace unix socket 在 bind 之前,sockaddr_un.sun_path[0] 设成 0x0 即可。
三,代码
于是我用 abstract unix socket 实现了一个 SysSem 工具类( 一个 system 范围的 semaphore ),
用来:
- 让一个程序只启动一个实例。
- 让 x 进程等待 y 进程执行完 yyy 操作后,才能执行 xxx 操作。
特点:
- 多进程/线程 并发安全。
- 当持有的进程被 kill ,OS自动释放,无残留。
- 没有磁盘文件,没有文件意外被删的各种情况。
- 不占用 tcp/udp 端口。
- 简单,不到 60行代码。
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <algorithm>
#include <string>
//
// a semaphore with system scope.
//
// 1. no race conditions between Post() / GetValue() , better than flock().
// 2. when a running process be killed, automatically release all.
// 3. no file on disk, no accidently delete .
// 4. no tcp/udp socket, no confliction, no port consumption.
//
class SysSem {
public:
SysSem() : _fd(-1) { memset(&_addr, 0, sizeof(_addr)); }
~SysSem();
void Init(std::string id);
bool Post();
bool GetValue();
const char* GetID() const;
private:
struct sockaddr_un _addr;
int _fd;
};
void SysSem::Init(std::string id) {
_addr.sun_family = AF_UNIX;
const size_t len = std::min(id.size(), sizeof(_addr.sun_path) - 2); // 2 = start null and end null byte
// abstract namespace socket address , _addr.sun_path[0] is a null byte ('\0')
memcpy(_addr.sun_path + 1, id.c_str(), len);
// memcpy(_addr.sun_path + 0, id.c_str(), len);
}
const char* SysSem::GetID() const { return &_addr.sun_path[1]; }
SysSem::~SysSem() {
if (_fd >= 0) {
::close(_fd);
_fd = -1;
}
}
bool SysSem::Post() {
_fd = ::socket(AF_UNIX, SOCK_STREAM, 0);
if (_fd < 0) {
return false;
}
if ((0 != ::bind(_fd, (struct sockaddr*)&_addr, sizeof(_addr))) || (0 != listen(_fd, 65536))) {
return false;
}
return true;
}
bool SysSem::GetValue() {
const int clientFD = ::socket(AF_UNIX, SOCK_STREAM, 0);
if (clientFD < 0) {
return false;
}
const bool ret = (0 == ::connect(clientFD, (struct sockaddr*)&_addr, sizeof(_addr)));
::close(clientFD);
return ret;
}
#include <assert.h>
#include <stdio.h>
int main(int argc, char** argv) {
if (argc != 3) {
fprintf(stderr, "usage: %s abstract-path post/get\n", argv[0]);
exit(1);
}
SysSem inst;
inst.Init(argv[1]);
if (0 == strcasecmp(argv[2], "post")) {
assert(inst.Post());
SysSem check;
check.Init(argv[1]);
assert(check.GetValue());
printf("ok, i am the only one under %s. running ...\n", inst.GetID());
pause();
} else if (0 == strcasecmp(argv[2], "get")) {
assert(inst.GetValue());
printf("a process is running under %s. \n", inst.GetID());
} else {
printf("unknown cmd \n");
}
return 0;
}