分类: 其他平台
2015-04-29 15:08:27
在linux编程中,经常有这样的要求:特定进程(尤其是daemon进程)有且只有一个,即特定资源只能由一进程拥有。问题是:如何保证特定进程间的“互斥”关系(只有一个实例)?当检测到“互斥(锁定)”时,其余进程可直接退出,而无需同步。
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
(以上摘自百度知道)
Linux提供的同步机制:信号量、文件锁(文件记录锁和文件锁)、互斥量、条件变量。其中后两者需要依赖于共享内存才能用于进程间同步,因此只有文件锁是进程生存期的资源,其他的都属于内核生存期资源。除此之外,信号也可用于进程同步。
如果进程需要监听特定的端口(如60000),那么在进程起来之后,可直接尝试连接该特定端口,只要能够连上,即可说明该端口已被使用,进程退出。由于listen/connect均是原子操作,故该判断过程不存在竞态。这种方法极其简单且可靠。
既然端口可用于判断,自然会想利用unix socket来作为替代技术(unix socket远大于65535)。但是由于unix socket将在文件系统上创建一个文件,该文件必须被显式删除,后续的bind方能正常工作,故该方法存在缺陷:没有可靠的办法保证文件必定被删除。(后面分析)
另一种很常见的方法是:在特定的路径(路径可为配置参数)下创建一个“众所皆知”的文件,并利用独占锁/写锁保证在进程生存期内有且只有一个进程拥有该文件锁。文件锁属于进程生存期资源,不管进程是否正常终止,进程终止后,文件锁一定被释放。
作为一个加强,可将拥有文件锁的进程PID写入文件,从而在删除锁文件时更“可靠”。问题是:若考虑删除文件,该方案将存在缺陷:删除文件和创建文件是两个系统调用,存在“竞态”。后面将讨论文件删除问题。
信号量和进程锁都属于内核生存期资源。若进程异常终止,信号量和进程锁可能处于“不确定状态”,加上进程无法“得知”是否有其他进程使用相同的信号量或进程锁,导致后续进程不能正常工作。不推荐。
linux系统编程中,经常会出现“竞态(race condition)”,即多进程的资源获取冲突或者访问时序问题。Linux提供的绝大多数系统调用函数保证函数调用过程是原子的(并非所有的系统调用均是原子的,见附录),即单函数调用在返回或终止之前,该函数的操作是原子的,不受其他系统调用影响。但很多系统调用往往需要配合使用,由多个系统调用组成的调用组合,操作系统是无法保证原子性的!这意味着:2个以上系统调用组合在多进程环境下将出现“竞态”。如何避免竞态是linux系统编程的一个大问题。
凡涉及多于2个的系统调用,必存在竞态:
示例1:lseek+read
off_t orig;
orig = lseek(fd, 0, SEEK_CUR); /* Save current offset */
lseek(fd, offset, SEEK_SET);
s = read(fd, buf, len);
lseek(fd, orig, SEEK_SET); /* Restore original file offset */
示例2:access+create
if(access(file, F_OK) !=0){
int fd = open((char*)arg, O_RDWR|O_CREAT, 0644);
}
示例3:删除nfs文件系统的文件夹
Cloes(fd);
Remove_Dir(path);
注:fd指向的文件已经被删除,在fd被close之前,该文件将被重命名为.nfs***的临时文件。
示例4:unit socket (TLPI 57-3)
struct sockaddr_un addr;
int sfd, cfd;
ssize_t numRead;
char buf[BUF_SIZE];
sfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sfd == -1)
errExit("socket");
/* Construct server socket address, bind socket to it,
and make this a listening socket */
if (remove(SV_SOCK_PATH) == -1 && errno != ENOENT)
errExit("remove-%s", SV_SOCK_PATH);
memset(&addr, 0, sizeof(struct sockaddr_un));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SV_SOCK_PATH, sizeof(addr.sun_path) - 1);
if (bind(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1)
errExit("bind");
if (listen(sfd, BACKLOG) == -1)
errExit("listen");
|
注:该程序在remove和bind之间存在竞态,即有可能另一程序删除该被刚创建的unix socket文件。对于其他的系统资源,如POSIX信号量,POSIX消息队列,POSIX共享内存,其本质也是文件(通常位于/dev/shm/),且这些文件和普通文件一样可“加锁”!
文件锁机制是一个可靠的进程间同步机制(信号量等机制存在缺陷)。使用该机制并不要求删除“锁文件”,不当的文件删除反而会引入潜在问题。
“锁文件”删除场景分析:
1) 创建后立马删除(create + unlink)
这种做法将导致其他进程“看不到”锁文件,从而创建另一个新文件。
2) 删除文件时未加锁
文件锁和文件记录锁若使用不当,锁会因其他操作而释放,从而导致删除文件时,删除进程并未锁定该文件。若此场景出现,则意味着锁文件的“创建+删除”并非原子操作,从而出现竞态。
3) 程序异常终止
删除文件这个美好的愿望可能因程序异常终止而无法实现。
4) “创建+删除”原子操作且正常执行
只有在这样的条件下,方能保证完美删除锁文件。(但谁能保证程序永远正确呢?)
总之,使用锁文件同步进程无需也不应该去删除锁文件。下面的例子是来自TLPI(The Linux Programming Interface) 55-4:
int createPidFile(const char *progName, const char *pidFile, int flags)
{
int fd;
char buf[BUF_SIZE];
fd = open(pidFile, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
if (fd == -1)
errExit("Could not open PID file %s", pidFile);
if (flags & CPF_CLOEXEC) {
/* Set the close-on-exec file descriptor flag */
flags = fcntl(fd, F_GETFD); /* Fetch flags */
if (flags == -1)
errExit("Could not get flags for PID file %s", pidFile);
flags |= FD_CLOEXEC; /* Turn on FD_CLOEXEC */
if (fcntl(fd, F_SETFD, flags) == -1) /* Update flags */
errExit("Could not set flags for PID file %s", pidFile);
}
if (lockRegion(fd, F_WRLCK, SEEK_SET, 0, 0) == -1) {
if (errno == EAGAIN || errno == EACCES)
fatal("PID file '%s' is locked; probably "
"'%s' is already running", pidFile, progName);
else
errExit("Unable to lock PID file '%s'", pidFile);
}
if (ftruncate(fd, 0) == -1)
errExit("Could not truncate PID file '%s'", pidFile);
snprintf(buf, BUF_SIZE, "%ld\n", (long) getpid());
if (write(fd, buf, strlen(buf)) != strlen(buf))
fatal("Writing to PID file '%s'", pidFile);
return fd;
}
|
几点说明:
1) O_CREAT的open方式将保证锁文件被创建或正确打开,即使多个进程同时执行也没有问题。Open是原子的,有且只有一个文件被创建。
2) lockRegion采用的是文件记录锁,也可以换成文件锁(flock)。只有fcntl才能用于NFS。
3) 将进程PID写入锁文件有助于其他程序判断该锁文件是否有效(和文件是否锁定无关),对安全删除锁文件有帮助,比如垃圾清理进程。
另一种实现:
int fd = open(lockfile.c_str(), O_RDWR|O_CREAT|O_EXCL, 0644);
if(fd < 0){
if(errno == EEXIST){
fd = open(lockfile.c_str(), O_RDWR);
}
}
if(fd < 0){
char buf[512] = {0};
strerror_r(errno, buf, 512);
exit(-1);
}
if(writelock(fd) < 0){ // only one process will get the lock.
char buf[512] = {0};
strerror_r(errno, buf, 512);
exit(-1);
}
|
几点说明:
1) O_CREAT|O_EXCL将保证有且只有一个进程能够创建锁文件。
2) 通过文件锁保证有且只有一个进程获得文件锁。
3) 第一种实现更为简单且优雅。
不保证原子性的系统调用:
1) write() -- write N bytes to PIPE,if N > PIPE_BUF, then write is not atomic!
2) flock() -- lock convert is not guarantee to be atomic. fcntl() guarantee all operators are atomic.