flock(): 如何在无竞态条件下删除锁定的文件?

35

我正在使用flock()实现进程间的命名互斥锁(即某些进程可以决定在“some_name”上持有锁,这是通过在临时目录中锁定名为“some_name”的文件来实现的):

lockfile = "/tmp/some_name.lock";
fd = open(lockfile, O_CREAT);
flock(fd, LOCK_EX);

do_something();

unlink(lockfile);
flock(fd, LOCK_UN);

应该在某个时候删除锁定文件,以避免将临时目录填满数百个文件。

但是,这段代码中存在明显的竞争条件;例如,使用进程 A、B 和 C 的示例:

A opens file
A locks file
B opens file
A unlinks file
A unlocks file
B locks file (B holds a lock on the deleted file)
C opens file (a new file one is created)
C locks file (two processes hold the same named mutex !)

有没有一种方法在不引入竞争条件的情况下,在某个时间点上删除锁定文件?


3
问题在于你试图实现细粒度锁定策略(文件表示资源),但在共享的粗粒度资源(文件系统)上存在争用。你需要在更新细粒度锁之前对锁文件目录进行全局锁定,或者完全重新设计你的锁定策略。 - jxh
你能详细说明一下你的需求吗?例如,如果所有程序都锁定相同的概念资源,为什么不让它们使用一个众所周知的文件名呢? - Art Swri
3个回答

37

如果我回答了一个已经过期的问题,那么请见谅:

在锁定文件之后,打开它的另一个副本,对两个副本进行fstat操作并检查inode号码,就像这样:

lockfile = "/tmp/some_name.lock";

    while(1) {
        fd = open(lockfile, O_CREAT);
        flock(fd, LOCK_EX);

        fstat(fd, &st0);
        stat(lockfile, &st1);
        if(st0.st_ino == st1.st_ino) break;

        close(fd);
    }

    do_something();

    unlink(lockfile);
    flock(fd, LOCK_UN);

这样做可以避免竞态条件,因为如果一个程序持有一个文件上的锁,而该文件仍然存在于文件系统中,则所有留下文件的其他程序都将具有错误的inode号码。

我在状态机模型中实际证明了这一点,使用以下属性:

如果P_i在文件系统上锁定了一个描述符,那么没有其他进程处于临界区。

如果P_i在正确的inode上执行stat或在临界区中,则它在文件系统上锁定了描述符。


2
文件在解除链接并关闭后可以解锁吗?手册上说flock“在fd指定的打开文件上应用或删除一个咨询锁”。 - sheerun
3
你不应该在最后关闭fd以防止泄漏吗? - Amro Younes
2
不可移植!例如,在Windows上,st_ino始终为0。 - rustyx
2
状态机模型是什么? - piotr
@sheerun — 在文件被删除后,您可以解锁该文件。因为(a)关闭文件描述符会释放对其的锁定,(b)您需要文件描述符来进行解锁,但是在close()之后文件描述符不再有效,尽管文件描述符可能会被重用,导致可追踪性问题。请注意,循环不通过底部退出——它通过close()之前的break退出。 - Jonathan Leffler
显示剩余4条评论

9
  1. 在Unix中,尽管文件已打开,也可以删除它 - 直到所有拥有该文件的进程在其文件描述符列表中全部结束时,inode才会保留
  2. 在Unix中,通过检查链接计数变为零,可以检查文件是否已从所有目录中删除

因此,您可以简单地检查已经打开的文件上的nlink计数,而不是比较旧/新文件路径的ino值。它假定它只是一个短暂的锁文件,而不是真正的互斥资源或设备。

lockfile = "/tmp/some_name.lock";

for(int attempt; attempt < timeout; ++attempt) {
    int fd = open(lockfile, O_CREAT, 0444);
    int done = flock(fd, LOCK_EX | LOCK_NB);
    if (done != 0) { 
        close(fd);
        sleep(1);     // lock held by another proc
        continue;
    }
    struct stat st0;
    fstat(fd, &st0);
    if(st0.st_nlink == 0) {
       close(fd);     // lockfile deleted, create a new one
       continue;
    }
    do_something();
    unlink(lockfile); // nlink :=0 before releasing the lock
    flock(fd, LOCK_UN);
    close(fd);        // release the ino if no other proc 
    return true;
}
return false;

@Bob 怎么了?这段代码从未在没有独占锁的情况下unlink文件,而且当进行此检查时,没有其他进程可以拥有这样的独占锁,因为它本身已经拥有该锁。 - Joseph Sible-Reinstate Monica
@JosephSible-ReinstateMonica 这段代码永远不会在没有独占锁的情况下unlink一个文件 flock()是通过文件描述符(实际上是文件描述符)进行的,而unlink()是通过名称进行的。我看不出有什么可以阻止对文件进行rename()调用,即使flock()对文件描述符进行了独占锁定。 - Andrew Henle
@JosephSible-ReinstateMonica 这段代码永远不会对没有独占锁的文件执行unlink操作 flock()是通过文件描述符(实际上是文件描述符)进行操作,而unlink()是通过文件名进行操作。我没有看到任何阻止在flock()对文件描述符设置了独占锁的情况下对文件进行rename()调用的内容。 - undefined
@AndrewHenle 当然,如果有人进来执行 rename() 操作,会导致程序出错。但是这段代码并没有这样做,如果你在背后擅自更改锁文件,那么破坏其他程序也不应该让你感到惊讶。 - Joseph Sible-Reinstate Monica

4
如果您仅使用这些文件进行锁定,并且实际上不写入它们,那么我建议您将目录条目本身的存在视为持有锁的指示,并完全避免使用 flock
要这样做,您需要构建一个操作,该操作创建一个目录条目并在其已存在时报告错误。在Linux和大多数文件系统上,将 O_EXCL 传递给 open 将起作用。但是某些平台和某些文件系统(特别是旧版NFS)不支持此功能。因此,man page for open 提供了一种替代方法:
可移植程序想要使用锁文件执行原子文件锁定,并需要避免依赖NFS支持的O_EXCL,可以在同一文件系统上创建一个唯一的文件(例如,包含主机名和PID),并使用link(2)创建一个指向锁文件的链接。如果link(2)返回0,则锁定成功。否则,在唯一文件上使用stat(2)检查其链接计数是否增加到2,此时锁定也将成功。
这似乎是一种官方记录的锁定方案,因此表明了某种程度的支持和最佳实践建议。但我也看到了其他方法。例如,bzr在大多数地方使用目录而不是符号链接。引用其源代码中的内容:

锁定通过特定名称的目录包含信息文件来在磁盘上表示。将临时目录重命名为该目录即可完成锁定。我们使用临时目录,因为对于所有已知的传输和文件系统,我们认为只有一个尝试获得锁定会成功,而其他尝试都会失败。(文件行不通,因为某些文件系统或传输只有重命名和覆盖,很难确定谁赢了。)

上述方法的一个缺点是它们不会阻塞:尝试失败的锁定会导致错误,但不会等待锁定可用。您将不得不轮询锁定,这在锁争用情况下可能会有问题。在这种情况下,您可能希望进一步远离基于文件系统的方法,而改为使用第三方实现。但是关于如何进行ipc互斥锁的一般问题已经被问过了,所以我建议您搜索[ipc] [mutex]并查看结果,特别是这个。顺便说一句,这些标签对您的帖子也可能有用。

11
这种方法面临的问题是,如果一个进程在持有锁的同时死亡,就没有可靠的方式自动释放该锁。相比之下,在“flock”方法中,如果一个进程在持有锁时死亡,文件将保留在文件系统中,但不再被“flock”,其他进程现在可以获取该锁。 - davidg
@davidg:这是一个有效的观点。一些实现会将时间戳和/或锁定进程的pid写入文件,该文件与锁定文件名链接,或写入锁定目录中的一个众所周知的文件。这样,您可以检查具有该ID的进程是否仍然存在,并且您还可以在给定时间后过期锁定。 - MvG

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接