群():在没有竞争条件的情况下删除锁定的文件?

Posted

技术标签:

【中文标题】群():在没有竞争条件的情况下删除锁定的文件?【英文标题】:flock(): removing locked file without race condition? 【发布时间】:2013-07-16 13:02:01 【问题描述】:

我将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 !)

有没有办法在不引入这种竞争条件的情况下删除锁定文件?

【问题讨论】:

问题是您正在尝试实施细粒度锁定策略(文件代表资源),但您对共享粗粒度资源(文件系统)存在争用。您要么需要在更新细粒度锁之前对锁文件目录进行全局锁,要么重新设计您的锁策略。 您能详细说明您的需求吗?例如,如果程序都锁定相同的概念资源,为什么不让程序使用 1 个众所周知的文件名? 【参考方案1】:

如果您仅将这些文件用于锁定,而不实际写入它们,那么我建议您将目录条目本身的存在视为持有锁定的指示,并完全避免使用flock

为此,您需要构造一个操作,该操作创建一个目录条目并在它已经存在时报告错误。在 Linux 和 大多数 文件系统上,将 O_EXCL 传递给 open 将适用于此。但是某些平台和某些文件系统(尤其是较旧的 NFS)不支持此功能。因此,man page for open 提出了一个替代方案:

想要使用 lockfile 执行原子文件锁定并且需要避免依赖 NFS 对 O_EXCL 的支持的便携式程序可以在同一文件系统上创建唯一文件(例如,合并主机名和 PID),并使用link(2) 链接到锁定文件。如果link(2) 返回0,则锁定成功。否则,在唯一文件上使用stat(2) 来检查其链接数是否增加到2,这样锁也成功了。

所以这看起来像是一个正式记录的锁定方案,因此表明了一定程度的支持和最佳实践建议。但我也看到了其他方法。 bzr 例如在大多数地方使用目录而不是符号链接。引用its source code:

一个锁在磁盘上由一个特定名称的目录表示, 包含一个信息文件。通过重命名一个锁来完成 临时目录到位。我们使用临时目录是因为 对于所有已知的传输和文件系统,我们相信只有一个 尝试申领锁将成功,而其他尝试将失败。 (文件 不会这样做,因为某些文件系统或传输只有 重命名和覆盖,很难分辨谁赢了。)

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

【讨论】:

这种方法遇到的问题是,如果一个进程在持有锁时死掉了,那么就没有可靠的方法来自动获取锁。相反,在flock 方法中,如果一个进程在持有锁时死亡,该文件将保留在文件系统上,但不再是flock'ed;其他进程现在可以获取锁。 @davidg:这是一个有效的观点。一些实现将锁定过程的时间戳和/或 pid 写入链接到锁定文件名的文件,或锁定目录中的知名文件。这样,您可以检查具有该 ID 的进程是否仍然处于活动状态,并且您还可以在给定时间后使锁过期。【参考方案2】:

对不起,如果我回答了一个死问题:

锁定文件后,打开另一个副本,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 之后或在关键部分中,则它的描述符已锁定在文件系统上。

【讨论】:

取消链接关闭后可以解锁文件吗?手册说flock“在fd指定的打开文件上应用或删除咨询锁”。 你不应该在最后关闭fd以防止泄漏吗? 不可移植!例如。在 Windows 上 st_ino 始终为 0。 什么是状态机模型? 这个解决方案不起作用,问题是关于3个进程A、B、C。【参考方案3】:
    在 Unix 中,可以在打开文件时将其删除 - inode 将一直保留,直到所有在其文件描述符列表中包含它的进程结束为止 在 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 怎么样?这段代码从来没有unlinks 一个没有排他锁的文件,当检查完成后,没有其他进程可以拥有这样的排他锁,因为它自己拥有锁。

以上是关于群():在没有竞争条件的情况下删除锁定的文件?的主要内容,如果未能解决你的问题,请参考以下文章

创建锁定文件时防止竞争条件

使用数据库事务防止竞争条件 (Laravel)

死锁 - 在没有数据的情况下锁定列

mysql-innoDB-锁

如何在没有按键的情况下更改大写锁定状态

PHP 群()替代